feat: 搭建后端多租户名片服务
- 初始化 Spring Boot 多模块工程与通用基础能力 - 增加租户、组织、用户、名片、文件、统计等业务模块 - 补充数据库迁移脚本与基础测试
This commit is contained in:
42
backend/Dockerfile
Normal file
42
backend/Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM --platform=${BUILDPLATFORM} maven:3.9.9-eclipse-temurin-21 AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY backend/pom.xml ./pom.xml
|
||||||
|
COPY backend/easycard-common/pom.xml ./easycard-common/pom.xml
|
||||||
|
COPY backend/easycard-module-system/pom.xml ./easycard-module-system/pom.xml
|
||||||
|
COPY backend/easycard-module-tenant/pom.xml ./easycard-module-tenant/pom.xml
|
||||||
|
COPY backend/easycard-module-org/pom.xml ./easycard-module-org/pom.xml
|
||||||
|
COPY backend/easycard-module-user/pom.xml ./easycard-module-user/pom.xml
|
||||||
|
COPY backend/easycard-module-card/pom.xml ./easycard-module-card/pom.xml
|
||||||
|
COPY backend/easycard-module-file/pom.xml ./easycard-module-file/pom.xml
|
||||||
|
COPY backend/easycard-module-stat/pom.xml ./easycard-module-stat/pom.xml
|
||||||
|
COPY backend/easycard-boot/pom.xml ./easycard-boot/pom.xml
|
||||||
|
|
||||||
|
RUN mvn -q -DskipTests dependency:go-offline
|
||||||
|
|
||||||
|
COPY backend/. .
|
||||||
|
|
||||||
|
RUN mvn -q -DskipTests -pl easycard-boot -am clean package \
|
||||||
|
&& cp /build/easycard-boot/target/easycard-boot-0.1.0-SNAPSHOT.jar /tmp/app.jar \
|
||||||
|
&& rm -rf /tmp/manifest-check \
|
||||||
|
&& mkdir -p /tmp/manifest-check \
|
||||||
|
&& cd /tmp/manifest-check \
|
||||||
|
&& jar xf /tmp/app.jar META-INF/MANIFEST.MF \
|
||||||
|
&& grep -q "Main-Class: org.springframework.boot.loader.launch.JarLauncher" META-INF/MANIFEST.MF \
|
||||||
|
&& grep -q "Start-Class: com.easycard.boot.EasycardBootApplication" META-INF/MANIFEST.MF
|
||||||
|
|
||||||
|
FROM --platform=${TARGETPLATFORM} eclipse-temurin:21-jre
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
ENV JAVA_OPTS=""
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/app.jar /app/app.jar
|
||||||
|
|
||||||
|
EXPOSE 8112
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
|
||||||
119
backend/easycard-boot/pom.xml
Normal file
119
backend/easycard-boot/pom.xml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-boot</artifactId>
|
||||||
|
<name>easycard-boot</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-system</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-tenant</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-org</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-user</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-card</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-file</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-stat</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-mysql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.easycard.boot.EasycardBootApplication</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.easycard.boot;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.easycard")
|
||||||
|
@MapperScan(basePackages = "com.easycard.module")
|
||||||
|
public class EasycardBootApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(EasycardBootApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.easycard.boot.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "easycard.web.cors")
|
||||||
|
public class EasycardCorsProperties {
|
||||||
|
|
||||||
|
private List<String> allowedOrigins = new ArrayList<>(List.of(
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:8081",
|
||||||
|
"http://127.0.0.1:8081"
|
||||||
|
));
|
||||||
|
|
||||||
|
private List<String> allowedOriginPatterns = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<String> allowedMethods = new ArrayList<>(List.of(
|
||||||
|
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
|
||||||
|
));
|
||||||
|
|
||||||
|
private List<String> allowedHeaders = new ArrayList<>(List.of("*"));
|
||||||
|
|
||||||
|
private List<String> exposedHeaders = new ArrayList<>(List.of(HttpHeaders.AUTHORIZATION));
|
||||||
|
|
||||||
|
private boolean allowCredentials = true;
|
||||||
|
|
||||||
|
private long maxAge = 1800;
|
||||||
|
|
||||||
|
public List<String> getAllowedOrigins() {
|
||||||
|
return allowedOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedOrigins(List<String> allowedOrigins) {
|
||||||
|
this.allowedOrigins = allowedOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedOriginPatterns() {
|
||||||
|
return allowedOriginPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedOriginPatterns(List<String> allowedOriginPatterns) {
|
||||||
|
this.allowedOriginPatterns = allowedOriginPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedMethods() {
|
||||||
|
return allowedMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedMethods(List<String> allowedMethods) {
|
||||||
|
this.allowedMethods = allowedMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedHeaders() {
|
||||||
|
return allowedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedHeaders(List<String> allowedHeaders) {
|
||||||
|
this.allowedHeaders = allowedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getExposedHeaders() {
|
||||||
|
return exposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExposedHeaders(List<String> exposedHeaders) {
|
||||||
|
this.exposedHeaders = exposedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowCredentials() {
|
||||||
|
return allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowCredentials(boolean allowCredentials) {
|
||||||
|
this.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxAge() {
|
||||||
|
return maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxAge(long maxAge) {
|
||||||
|
this.maxAge = maxAge;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.easycard.boot.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI openAPI() {
|
||||||
|
return new OpenAPI().info(new Info()
|
||||||
|
.title("Easycard Backend API")
|
||||||
|
.version("v0.1.0")
|
||||||
|
.description("电子名片系统后端接口文档"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package com.easycard.boot.config;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.auth.JwtTokenService;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.tenant.TenantContext;
|
||||||
|
import com.easycard.common.tenant.TenantContextHolder;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(EasycardCorsProperties.class)
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource(EasycardCorsProperties corsProperties) {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
List<String> allowedOrigins = sanitize(corsProperties.getAllowedOrigins());
|
||||||
|
List<String> allowedOriginPatterns = sanitize(corsProperties.getAllowedOriginPatterns());
|
||||||
|
if (!allowedOrigins.isEmpty()) {
|
||||||
|
configuration.setAllowedOrigins(allowedOrigins);
|
||||||
|
}
|
||||||
|
if (!allowedOriginPatterns.isEmpty()) {
|
||||||
|
configuration.setAllowedOriginPatterns(allowedOriginPatterns);
|
||||||
|
}
|
||||||
|
configuration.setAllowedMethods(sanitize(corsProperties.getAllowedMethods()));
|
||||||
|
configuration.setAllowedHeaders(sanitize(corsProperties.getAllowedHeaders()));
|
||||||
|
configuration.setAllowCredentials(corsProperties.isAllowCredentials());
|
||||||
|
configuration.setExposedHeaders(sanitize(corsProperties.getExposedHeaders()));
|
||||||
|
configuration.setMaxAge(corsProperties.getMaxAge());
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> sanitize(List<String> values) {
|
||||||
|
if (values == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return values.stream()
|
||||||
|
.filter(StringUtils::hasText)
|
||||||
|
.map(String::trim)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(
|
||||||
|
HttpSecurity http,
|
||||||
|
JwtAuthenticationFilter jwtAuthenticationFilter
|
||||||
|
) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
|
.requestMatchers(
|
||||||
|
"/actuator/health",
|
||||||
|
"/actuator/info",
|
||||||
|
"/v3/api-docs/**",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/swagger-ui.html",
|
||||||
|
"/api/v1/system/ping",
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
"/api/open/**"
|
||||||
|
).permitAll()
|
||||||
|
.anyRequest().authenticated())
|
||||||
|
.exceptionHandling(configurer -> configurer.authenticationEntryPoint((request, response, exception) -> {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"未登录或登录已失效\",\"data\":null}");
|
||||||
|
}))
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.cors(Customizer.withDefaults());
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
JwtAuthenticationFilter(JwtTokenService jwtTokenService, ObjectMapper objectMapper) {
|
||||||
|
this.jwtTokenService = jwtTokenService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
return uri.startsWith("/api/open/") || "/api/v1/auth/login".equals(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
if (authorization != null && authorization.startsWith("Bearer ")) {
|
||||||
|
String token = authorization.substring(7);
|
||||||
|
LoginUser loginUser = jwtTokenService.parseToken(token).orElse(null);
|
||||||
|
if (loginUser != null) {
|
||||||
|
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||||
|
loginUser,
|
||||||
|
null,
|
||||||
|
loginUser.roleCodes().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||||
|
if (loginUser.tenantId() != null && loginUser.tenantId() > 0) {
|
||||||
|
TenantContextHolder.set(new TenantContext(loginUser.tenantId(), null, null));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeUnauthorized(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
TenantContextHolder.clear();
|
||||||
|
org.springframework.security.core.context.SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeUnauthorized(HttpServletResponse response) throws IOException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.fail("UNAUTHORIZED", "令牌无效或已过期")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
spring:
|
||||||
|
flyway:
|
||||||
|
connect-retries: 30
|
||||||
|
|
||||||
|
logging:
|
||||||
|
file:
|
||||||
|
path: ${LOG_PATH:/app/logs}
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
com.easycard: INFO
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
com.easycard: INFO
|
||||||
|
org.flywaydb: INFO
|
||||||
78
backend/easycard-boot/src/main/resources/application.yml
Normal file
78
backend/easycard-boot/src/main/resources/application.yml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: easycard-backend
|
||||||
|
profiles:
|
||||||
|
active: local
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 8MB
|
||||||
|
max-request-size: 8MB
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:23306}/${DB_NAME:easycard}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||||
|
username: ${DB_USERNAME:root}
|
||||||
|
password: ${DB_PASSWORD:root}
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:127.0.0.1}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
database: ${REDIS_DATABASE:3}
|
||||||
|
password: ${REDIS_PASSWORD:123456}
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/migration/mysql
|
||||||
|
baseline-on-migrate: true
|
||||||
|
encoding: UTF-8
|
||||||
|
jackson:
|
||||||
|
time-zone: Asia/Shanghai
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: ${SERVER_PORT:8112}
|
||||||
|
forward-headers-strategy: framework
|
||||||
|
servlet:
|
||||||
|
context-path: /
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
enabled: true
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
global-config:
|
||||||
|
db-config:
|
||||||
|
id-type: auto
|
||||||
|
logic-delete-field: deleted
|
||||||
|
logic-delete-value: 1
|
||||||
|
logic-not-delete-value: 0
|
||||||
|
|
||||||
|
easycard:
|
||||||
|
security:
|
||||||
|
jwt-secret: ${JWT_SECRET:change-me-in-production}
|
||||||
|
jwt-expire-hours: ${JWT_EXPIRE_HOURS:24}
|
||||||
|
web:
|
||||||
|
cors:
|
||||||
|
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://127.0.0.1:5173,http://localhost:8081,http://127.0.0.1:8081}
|
||||||
|
allowed-origin-patterns: ${CORS_ALLOWED_ORIGIN_PATTERNS:}
|
||||||
|
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,PATCH,DELETE,OPTIONS}
|
||||||
|
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
||||||
|
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization}
|
||||||
|
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
|
||||||
|
max-age: ${CORS_MAX_AGE:1800}
|
||||||
|
storage:
|
||||||
|
endpoint: ${MINIO_ENDPOINT:http://127.0.0.1:9000}
|
||||||
|
public-endpoint: ${MINIO_PUBLIC_ENDPOINT:${MINIO_ENDPOINT:http://127.0.0.1:9000}}
|
||||||
|
access-key: ${MINIO_ACCESS_KEY:minioadmin}
|
||||||
|
secret-key: ${MINIO_SECRET_KEY:minioadmin}
|
||||||
|
bucket: ${MINIO_BUCKET:easycard}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Flyway 迁移说明
|
||||||
|
|
||||||
|
## 目录说明
|
||||||
|
|
||||||
|
当前项目约定 MySQL 迁移脚本放在:
|
||||||
|
|
||||||
|
`backend/easycard-boot/src/main/resources/db/migration/mysql`
|
||||||
|
|
||||||
|
建议 Spring Boot 配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
locations: classpath:db/migration/mysql
|
||||||
|
baseline-on-migrate: true
|
||||||
|
encoding: UTF-8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命名规则
|
||||||
|
|
||||||
|
- 版本迁移:`V{版本号}__{说明}.sql`
|
||||||
|
- 可重复执行脚本:`R__{说明}.sql`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
- `V1__create_core_schema.sql`
|
||||||
|
- `V2__seed_platform_base_data.sql`
|
||||||
|
- `V3__add_card_audit_table.sql`
|
||||||
|
- `R__refresh_report_view.sql`
|
||||||
|
|
||||||
|
## 当前脚本职责
|
||||||
|
|
||||||
|
- `V1__create_core_schema.sql`
|
||||||
|
创建第一阶段核心表结构
|
||||||
|
|
||||||
|
- `V2__seed_platform_base_data.sql`
|
||||||
|
初始化平台角色、基础菜单、字典类型和字典项
|
||||||
|
|
||||||
|
## 后续迁移规则
|
||||||
|
|
||||||
|
- 不要回改已执行的版本脚本
|
||||||
|
- 新字段、新索引、新表一律追加新版本
|
||||||
|
- 租户初始化数据不要写进平台全局迁移,改由业务代码在“创建租户”流程中生成
|
||||||
|
- 大批量修复历史数据时,单独建迁移脚本,不要混入结构变更
|
||||||
|
|
||||||
|
## 与初始化脚本关系
|
||||||
|
|
||||||
|
仓库中的 [easycard_init.sql](/Users/slience/postgraduate/easycard/database/mysql/easycard_init.sql) 当前只负责建库。
|
||||||
|
|
||||||
|
表结构、平台初始化数据、演示数据统一以当前目录下的 Flyway 版本脚本为准,不再额外维护一份完整的本地建表脚本。
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
CREATE TABLE `sys_tenant` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_code` VARCHAR(64) NOT NULL COMMENT '租户编码',
|
||||||
|
`tenant_name` VARCHAR(128) NOT NULL COMMENT '租户名称',
|
||||||
|
`tenant_short_name` VARCHAR(64) DEFAULT NULL COMMENT '租户简称',
|
||||||
|
`contact_name` VARCHAR(64) DEFAULT NULL COMMENT '联系人',
|
||||||
|
`contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系人电话',
|
||||||
|
`contact_email` VARCHAR(128) DEFAULT NULL COMMENT '联系人邮箱',
|
||||||
|
`tenant_status` VARCHAR(32) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED/EXPIRED',
|
||||||
|
`expire_at` DATETIME(3) DEFAULT NULL COMMENT '到期时间',
|
||||||
|
`user_limit` INT UNSIGNED NOT NULL DEFAULT 20 COMMENT '用户数量上限',
|
||||||
|
`storage_limit_mb` INT UNSIGNED NOT NULL DEFAULT 1024 COMMENT '存储空间上限,单位MB',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_tenant_code_deleted` (`tenant_code`, `deleted`),
|
||||||
|
KEY `idx_sys_tenant_status` (`tenant_status`),
|
||||||
|
KEY `idx_sys_tenant_expire_at` (`expire_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户表';
|
||||||
|
|
||||||
|
CREATE TABLE `tenant_miniapp_config` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`env_code` VARCHAR(16) NOT NULL DEFAULT 'PROD' COMMENT '环境:DEV/TEST/PROD',
|
||||||
|
`miniapp_app_id` VARCHAR(64) NOT NULL COMMENT '小程序AppID',
|
||||||
|
`miniapp_app_secret` VARCHAR(255) NOT NULL COMMENT '小程序AppSecret,建议应用层加密后存储',
|
||||||
|
`miniapp_name` VARCHAR(128) NOT NULL COMMENT '小程序名称',
|
||||||
|
`miniapp_original_id` VARCHAR(64) DEFAULT NULL COMMENT '小程序原始ID',
|
||||||
|
`request_domain` VARCHAR(255) DEFAULT NULL COMMENT '接口请求域名',
|
||||||
|
`upload_domain` VARCHAR(255) DEFAULT NULL COMMENT '上传域名',
|
||||||
|
`download_domain` VARCHAR(255) DEFAULT NULL COMMENT '下载域名',
|
||||||
|
`version_tag` VARCHAR(64) DEFAULT NULL COMMENT '当前版本号',
|
||||||
|
`publish_status` VARCHAR(32) NOT NULL DEFAULT 'UNCONFIGURED' COMMENT '发布状态:UNCONFIGURED/DRAFT/PUBLISHED/DISABLED',
|
||||||
|
`last_published_at` DATETIME(3) DEFAULT NULL COMMENT '最近发布时间',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_miniapp_app_id_deleted` (`miniapp_app_id`, `deleted`),
|
||||||
|
UNIQUE KEY `uk_tenant_miniapp_env_deleted` (`tenant_id`, `env_code`, `deleted`),
|
||||||
|
KEY `idx_tenant_miniapp_status` (`tenant_id`, `publish_status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='租户小程序配置';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_user` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台用户固定为0',
|
||||||
|
`user_type` VARCHAR(16) NOT NULL COMMENT '用户类型:PLATFORM/TENANT',
|
||||||
|
`username` VARCHAR(64) NOT NULL COMMENT '登录账号',
|
||||||
|
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希',
|
||||||
|
`real_name` VARCHAR(64) NOT NULL COMMENT '真实姓名',
|
||||||
|
`nick_name` VARCHAR(64) DEFAULT NULL COMMENT '昵称',
|
||||||
|
`gender` VARCHAR(16) DEFAULT NULL COMMENT '性别:MALE/FEMALE/UNKNOWN',
|
||||||
|
`mobile` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
|
||||||
|
`email` VARCHAR(128) DEFAULT NULL COMMENT '邮箱',
|
||||||
|
`avatar_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '头像素材ID',
|
||||||
|
`dept_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '所属组织ID',
|
||||||
|
`job_title` VARCHAR(128) DEFAULT NULL COMMENT '岗位/职务',
|
||||||
|
`user_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED/LOCKED',
|
||||||
|
`must_update_password` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否首次登录强制改密',
|
||||||
|
`last_login_at` DATETIME(3) DEFAULT NULL COMMENT '最近登录时间',
|
||||||
|
`last_login_ip` VARCHAR(64) DEFAULT NULL COMMENT '最近登录IP',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_user_tenant_username_deleted` (`tenant_id`, `username`, `deleted`),
|
||||||
|
KEY `idx_sys_user_tenant_status` (`tenant_id`, `user_status`),
|
||||||
|
KEY `idx_sys_user_dept` (`tenant_id`, `dept_id`),
|
||||||
|
KEY `idx_sys_user_mobile` (`mobile`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_role` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台角色固定为0',
|
||||||
|
`role_scope` VARCHAR(16) NOT NULL COMMENT '角色范围:PLATFORM/TENANT',
|
||||||
|
`role_code` VARCHAR(64) NOT NULL COMMENT '角色编码',
|
||||||
|
`role_name` VARCHAR(128) NOT NULL COMMENT '角色名称',
|
||||||
|
`data_scope` VARCHAR(16) NOT NULL DEFAULT 'SELF' COMMENT '数据范围:ALL/TENANT/DEPT/SELF',
|
||||||
|
`is_builtin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否内置角色',
|
||||||
|
`role_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_role_tenant_code_deleted` (`tenant_id`, `role_code`, `deleted`),
|
||||||
|
KEY `idx_sys_role_scope_status` (`role_scope`, `role_status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_menu` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父级菜单ID',
|
||||||
|
`menu_scope` VARCHAR(16) NOT NULL DEFAULT 'ALL' COMMENT '适用范围:PLATFORM/TENANT/ALL',
|
||||||
|
`menu_type` VARCHAR(16) NOT NULL COMMENT '类型:DIRECTORY/MENU/BUTTON',
|
||||||
|
`menu_name` VARCHAR(128) NOT NULL COMMENT '菜单名称',
|
||||||
|
`route_path` VARCHAR(255) DEFAULT NULL COMMENT '前端路由路径',
|
||||||
|
`component_path` VARCHAR(255) DEFAULT NULL COMMENT '前端组件路径',
|
||||||
|
`permission_code` VARCHAR(128) DEFAULT NULL COMMENT '权限标识',
|
||||||
|
`icon` VARCHAR(64) DEFAULT NULL COMMENT '图标',
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`is_visible` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见',
|
||||||
|
`is_keep_alive` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否缓存',
|
||||||
|
`menu_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_menu_permission_deleted` (`permission_code`, `deleted`),
|
||||||
|
KEY `idx_sys_menu_parent` (`parent_id`),
|
||||||
|
KEY `idx_sys_menu_scope_status` (`menu_scope`, `menu_status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单权限表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_role_menu` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
|
||||||
|
`menu_id` BIGINT UNSIGNED NOT NULL COMMENT '菜单ID',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_role_menu` (`role_id`, `menu_id`),
|
||||||
|
KEY `idx_sys_role_menu_menu` (`menu_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色菜单关联表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_user_role` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台用户固定为0',
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
|
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_user_role` (`user_id`, `role_id`),
|
||||||
|
KEY `idx_sys_user_role_tenant` (`tenant_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_dict_type` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`dict_type_code` VARCHAR(64) NOT NULL COMMENT '字典类型编码',
|
||||||
|
`dict_type_name` VARCHAR(128) NOT NULL COMMENT '字典类型名称',
|
||||||
|
`dict_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_dict_type_code_deleted` (`dict_type_code`, `deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='字典类型表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_dict_item` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`dict_type_id` BIGINT UNSIGNED NOT NULL COMMENT '字典类型ID',
|
||||||
|
`item_label` VARCHAR(128) NOT NULL COMMENT '字典标签',
|
||||||
|
`item_value` VARCHAR(128) NOT NULL COMMENT '字典值',
|
||||||
|
`item_sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`item_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_dict_item_type_value_deleted` (`dict_type_id`, `item_value`, `deleted`),
|
||||||
|
KEY `idx_sys_dict_item_type_sort` (`dict_type_id`, `item_sort`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='字典项表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_config` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`scope_type` VARCHAR(16) NOT NULL COMMENT '配置范围:PLATFORM/TENANT',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台配置固定为0',
|
||||||
|
`config_key` VARCHAR(128) NOT NULL COMMENT '配置键',
|
||||||
|
`config_value` TEXT COMMENT '配置值',
|
||||||
|
`value_type` VARCHAR(16) NOT NULL DEFAULT 'STRING' COMMENT '值类型:STRING/NUMBER/BOOLEAN/JSON',
|
||||||
|
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_sys_config_scope_key_deleted` (`scope_type`, `tenant_id`, `config_key`, `deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统参数配置表';
|
||||||
|
|
||||||
|
CREATE TABLE `org_department` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父级组织ID',
|
||||||
|
`dept_code` VARCHAR(64) NOT NULL COMMENT '组织编码',
|
||||||
|
`dept_name` VARCHAR(128) NOT NULL COMMENT '组织名称',
|
||||||
|
`dept_type` VARCHAR(32) NOT NULL COMMENT '组织类型:HEADQUARTERS/BRANCH/DEPARTMENT/GROUP',
|
||||||
|
`leader_user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '负责人用户ID',
|
||||||
|
`contact_phone` VARCHAR(32) DEFAULT NULL COMMENT '联系电话',
|
||||||
|
`address` VARCHAR(255) DEFAULT NULL COMMENT '组织地址',
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`dept_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_org_department_code_deleted` (`tenant_id`, `dept_code`, `deleted`),
|
||||||
|
KEY `idx_org_department_parent` (`tenant_id`, `parent_id`),
|
||||||
|
KEY `idx_org_department_type_status` (`tenant_id`, `dept_type`, `dept_status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='组织架构表';
|
||||||
|
|
||||||
|
CREATE TABLE `org_firm_profile` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`firm_name` VARCHAR(128) NOT NULL COMMENT '事务所名称',
|
||||||
|
`firm_short_name` VARCHAR(64) DEFAULT NULL COMMENT '事务所简称',
|
||||||
|
`english_name` VARCHAR(128) DEFAULT NULL COMMENT '英文名称',
|
||||||
|
`logo_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT 'Logo素材ID',
|
||||||
|
`hero_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '封面图素材ID',
|
||||||
|
`intro` TEXT COMMENT '事务所简介',
|
||||||
|
`hotline_phone` VARCHAR(32) DEFAULT NULL COMMENT '咨询电话',
|
||||||
|
`website_url` VARCHAR(255) DEFAULT NULL COMMENT '官网地址',
|
||||||
|
`wechat_official_account` VARCHAR(128) DEFAULT NULL COMMENT '公众号名称',
|
||||||
|
`hq_address` VARCHAR(255) DEFAULT NULL COMMENT '总部地址',
|
||||||
|
`hq_latitude` DECIMAL(10, 7) DEFAULT NULL COMMENT '总部纬度',
|
||||||
|
`hq_longitude` DECIMAL(10, 7) DEFAULT NULL COMMENT '总部经度',
|
||||||
|
`display_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '展示状态:ENABLED/DISABLED',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_org_firm_profile_tenant_deleted` (`tenant_id`, `deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='事务所主页信息表';
|
||||||
|
|
||||||
|
CREATE TABLE `org_firm_practice_area` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`area_code` VARCHAR(64) NOT NULL COMMENT '专业领域编码',
|
||||||
|
`area_name` VARCHAR(128) NOT NULL COMMENT '专业领域名称',
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`area_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_org_practice_area_code_deleted` (`tenant_id`, `area_code`, `deleted`),
|
||||||
|
KEY `idx_org_practice_area_status_sort` (`tenant_id`, `area_status`, `display_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='事务所专业领域表';
|
||||||
|
|
||||||
|
CREATE TABLE `card_profile` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
|
`dept_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '组织ID',
|
||||||
|
`card_name` VARCHAR(64) NOT NULL COMMENT '名片展示姓名',
|
||||||
|
`card_title` VARCHAR(128) DEFAULT NULL COMMENT '职务/头衔',
|
||||||
|
`mobile` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
|
||||||
|
`telephone` VARCHAR(32) DEFAULT NULL COMMENT '座机',
|
||||||
|
`email` VARCHAR(128) DEFAULT NULL COMMENT '邮箱',
|
||||||
|
`office_address` VARCHAR(255) DEFAULT NULL COMMENT '办公地址',
|
||||||
|
`avatar_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '头像素材ID',
|
||||||
|
`cover_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '封面素材ID',
|
||||||
|
`wechat_qr_asset_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '微信二维码素材ID',
|
||||||
|
`bio` TEXT COMMENT '个人简介',
|
||||||
|
`certificate_no` VARCHAR(128) DEFAULT NULL COMMENT '执业证号',
|
||||||
|
`education_info` TEXT COMMENT '教育经历',
|
||||||
|
`honor_info` TEXT COMMENT '荣誉信息',
|
||||||
|
`is_public` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否公开',
|
||||||
|
`is_recommended` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
|
||||||
|
`publish_status` VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '发布状态:DRAFT/PUBLISHED/OFFLINE',
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`view_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '累计浏览次数',
|
||||||
|
`share_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '累计分享次数',
|
||||||
|
`last_published_at` DATETIME(3) DEFAULT NULL COMMENT '最近发布时间',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_card_profile_tenant_user_deleted` (`tenant_id`, `user_id`, `deleted`),
|
||||||
|
KEY `idx_card_profile_publish` (`tenant_id`, `publish_status`, `is_public`),
|
||||||
|
KEY `idx_card_profile_dept` (`tenant_id`, `dept_id`),
|
||||||
|
KEY `idx_card_profile_sort` (`tenant_id`, `display_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='电子名片主表';
|
||||||
|
|
||||||
|
CREATE TABLE `card_profile_specialty` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`card_id` BIGINT UNSIGNED NOT NULL COMMENT '名片ID',
|
||||||
|
`practice_area_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '专业领域ID',
|
||||||
|
`specialty_name` VARCHAR(128) NOT NULL COMMENT '专业领域名称快照',
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_card_specialty_name_deleted` (`card_id`, `specialty_name`, `deleted`),
|
||||||
|
KEY `idx_card_specialty_practice_area` (`tenant_id`, `practice_area_id`),
|
||||||
|
KEY `idx_card_specialty_card_sort` (`tenant_id`, `card_id`, `display_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='名片专业领域关联表';
|
||||||
|
|
||||||
|
CREATE TABLE `file_asset` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`upload_user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '上传人ID',
|
||||||
|
`storage_provider` VARCHAR(32) NOT NULL DEFAULT 'MINIO' COMMENT '存储提供方',
|
||||||
|
`bucket_name` VARCHAR(128) NOT NULL COMMENT '桶名称',
|
||||||
|
`object_key` VARCHAR(255) NOT NULL COMMENT '对象存储Key',
|
||||||
|
`original_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
|
||||||
|
`file_ext` VARCHAR(32) DEFAULT NULL COMMENT '扩展名',
|
||||||
|
`mime_type` VARCHAR(128) DEFAULT NULL COMMENT 'MIME类型',
|
||||||
|
`file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小',
|
||||||
|
`file_hash` VARCHAR(128) DEFAULT NULL COMMENT '文件哈希',
|
||||||
|
`access_url` VARCHAR(500) DEFAULT NULL COMMENT '访问地址',
|
||||||
|
`asset_status` VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:UPLOADED/ACTIVE/ARCHIVED/DELETED',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_file_asset_object_key_deleted` (`object_key`, `deleted`),
|
||||||
|
KEY `idx_file_asset_tenant_status` (`tenant_id`, `asset_status`),
|
||||||
|
KEY `idx_file_asset_hash` (`tenant_id`, `file_hash`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文件素材表';
|
||||||
|
|
||||||
|
CREATE TABLE `file_asset_usage` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`asset_id` BIGINT UNSIGNED NOT NULL COMMENT '素材ID',
|
||||||
|
`biz_type` VARCHAR(64) NOT NULL COMMENT '业务类型,如FIRM_PROFILE/CARD_PROFILE/USER_AVATAR',
|
||||||
|
`biz_id` BIGINT UNSIGNED NOT NULL COMMENT '业务主键ID',
|
||||||
|
`field_name` VARCHAR(64) NOT NULL COMMENT '业务字段名',
|
||||||
|
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_file_asset_usage_deleted` (`asset_id`, `biz_type`, `biz_id`, `field_name`, `deleted`),
|
||||||
|
KEY `idx_file_asset_usage_biz` (`tenant_id`, `biz_type`, `biz_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文件素材引用表';
|
||||||
|
|
||||||
|
CREATE TABLE `card_view_log` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`miniapp_app_id` VARCHAR(64) NOT NULL COMMENT '小程序AppID',
|
||||||
|
`card_id` BIGINT UNSIGNED NOT NULL COMMENT '名片ID',
|
||||||
|
`viewer_open_id` VARCHAR(128) DEFAULT NULL COMMENT '访客OpenID',
|
||||||
|
`viewer_ip` VARCHAR(64) DEFAULT NULL COMMENT '访客IP',
|
||||||
|
`source_type` VARCHAR(32) DEFAULT NULL COMMENT '来源:DIRECT/SHARE/QRCODE/HISTORY',
|
||||||
|
`share_from_card_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '分享来源名片ID',
|
||||||
|
`page_path` VARCHAR(255) DEFAULT NULL COMMENT '页面路径',
|
||||||
|
`viewed_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '浏览时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_card_view_log_tenant_card_time` (`tenant_id`, `card_id`, `viewed_at`),
|
||||||
|
KEY `idx_card_view_log_appid_time` (`miniapp_app_id`, `viewed_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='名片浏览日志';
|
||||||
|
|
||||||
|
CREATE TABLE `card_share_log` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`miniapp_app_id` VARCHAR(64) NOT NULL COMMENT '小程序AppID',
|
||||||
|
`card_id` BIGINT UNSIGNED NOT NULL COMMENT '名片ID',
|
||||||
|
`share_channel` VARCHAR(32) DEFAULT NULL COMMENT '分享渠道:WECHAT_FRIEND/WECHAT_GROUP/POSTER/QRCODE',
|
||||||
|
`share_path` VARCHAR(255) DEFAULT NULL COMMENT '分享路径',
|
||||||
|
`share_by_open_id` VARCHAR(128) DEFAULT NULL COMMENT '分享人OpenID',
|
||||||
|
`shared_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '分享时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_card_share_log_tenant_card_time` (`tenant_id`, `card_id`, `shared_at`),
|
||||||
|
KEY `idx_card_share_log_appid_time` (`miniapp_app_id`, `shared_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='名片分享日志';
|
||||||
|
|
||||||
|
CREATE TABLE `card_stat_daily` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||||
|
`card_id` BIGINT UNSIGNED NOT NULL COMMENT '名片ID',
|
||||||
|
`stat_date` DATE NOT NULL COMMENT '统计日期',
|
||||||
|
`view_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '浏览次数',
|
||||||
|
`share_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '分享次数',
|
||||||
|
`unique_visitor_count` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '独立访客数',
|
||||||
|
`created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_card_stat_daily` (`tenant_id`, `card_id`, `stat_date`),
|
||||||
|
KEY `idx_card_stat_daily_date` (`tenant_id`, `stat_date`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='名片按日统计表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_login_log` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台登录为0',
|
||||||
|
`user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`user_type` VARCHAR(16) NOT NULL COMMENT '用户类型:PLATFORM/TENANT',
|
||||||
|
`login_type` VARCHAR(32) NOT NULL COMMENT '登录类型:PASSWORD/SMS/MINIAPP_BIND',
|
||||||
|
`login_status` VARCHAR(16) NOT NULL COMMENT '结果:SUCCESS/FAIL',
|
||||||
|
`client_ip` VARCHAR(64) DEFAULT NULL COMMENT '客户端IP',
|
||||||
|
`user_agent` VARCHAR(500) DEFAULT NULL COMMENT '客户端UA',
|
||||||
|
`fail_reason` VARCHAR(500) DEFAULT NULL COMMENT '失败原因',
|
||||||
|
`login_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '登录时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_sys_login_log_tenant_user_time` (`tenant_id`, `user_id`, `login_at`),
|
||||||
|
KEY `idx_sys_login_log_status_time` (`login_status`, `login_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='登录日志表';
|
||||||
|
|
||||||
|
CREATE TABLE `sys_operation_log` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID,平台操作为0',
|
||||||
|
`user_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '操作人ID',
|
||||||
|
`user_name` VARCHAR(64) DEFAULT NULL COMMENT '操作人名称',
|
||||||
|
`module_name` VARCHAR(64) NOT NULL COMMENT '模块名称',
|
||||||
|
`biz_type` VARCHAR(64) DEFAULT NULL COMMENT '业务类型',
|
||||||
|
`biz_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '业务ID',
|
||||||
|
`operation_type` VARCHAR(32) NOT NULL COMMENT '操作类型:CREATE/UPDATE/DELETE/PUBLISH/LOGIN/EXPORT等',
|
||||||
|
`request_method` VARCHAR(16) DEFAULT NULL COMMENT '请求方法',
|
||||||
|
`request_uri` VARCHAR(255) DEFAULT NULL COMMENT '请求地址',
|
||||||
|
`request_body` LONGTEXT COMMENT '请求参数',
|
||||||
|
`response_code` VARCHAR(32) DEFAULT NULL COMMENT '响应码',
|
||||||
|
`response_message` VARCHAR(500) DEFAULT NULL COMMENT '响应信息',
|
||||||
|
`client_ip` VARCHAR(64) DEFAULT NULL COMMENT '客户端IP',
|
||||||
|
`operated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_sys_operation_log_tenant_time` (`tenant_id`, `operated_at`),
|
||||||
|
KEY `idx_sys_operation_log_user_time` (`user_id`, `operated_at`),
|
||||||
|
KEY `idx_sys_operation_log_module_time` (`module_name`, `operated_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='操作日志表';
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
INSERT INTO `sys_role`
|
||||||
|
(`tenant_id`, `role_scope`, `role_code`, `role_name`, `data_scope`, `is_builtin`, `role_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 0, 'PLATFORM', 'PLATFORM_SUPER_ADMIN', '超级管理员', 'ALL', 1, 'ENABLED', '平台内置超级管理员角色', 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role` WHERE `tenant_id` = 0 AND `role_code` = 'PLATFORM_SUPER_ADMIN' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`parent_id`, `menu_scope`, `menu_type`, `menu_name`, `route_path`, `component_path`, `permission_code`, `icon`, `display_order`, `is_visible`, `is_keep_alive`, `menu_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 0, 'PLATFORM', 'DIRECTORY', '平台管理', '/platform', NULL, 'platform:root', 'Setting', 10, 1, 0, 'ENABLED', 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` WHERE `permission_code` = 'platform:root' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`parent_id`, `menu_scope`, `menu_type`, `menu_name`, `route_path`, `component_path`, `permission_code`, `icon`, `display_order`, `is_visible`, `is_keep_alive`, `menu_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 0, 'TENANT', 'DIRECTORY', '租户管理台', '/tenant', NULL, 'tenant:root', 'OfficeBuilding', 20, 1, 0, 'ENABLED', 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_menu` WHERE `permission_code` = 'tenant:root' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_type`
|
||||||
|
(`dict_type_code`, `dict_type_name`, `dict_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 'tenant_status', '租户状态', 'ENABLED', '租户状态字典', 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_type` WHERE `dict_type_code` = 'tenant_status' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_type`
|
||||||
|
(`dict_type_code`, `dict_type_name`, `dict_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 'card_publish_status', '名片发布状态', 'ENABLED', '名片发布状态字典', 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_type` WHERE `dict_type_code` = 'card_publish_status' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '启用', 'ENABLED', 10, 'ENABLED', '租户启用状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'tenant_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'ENABLED' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '停用', 'DISABLED', 20, 'ENABLED', '租户停用状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'tenant_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'DISABLED' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '已过期', 'EXPIRED', 30, 'ENABLED', '租户过期状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'tenant_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'EXPIRED' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '草稿', 'DRAFT', 10, 'ENABLED', '名片草稿状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'card_publish_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'DRAFT' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '已发布', 'PUBLISHED', 20, 'ENABLED', '名片发布状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'card_publish_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'PUBLISHED' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_dict_item`
|
||||||
|
(`dict_type_id`, `item_label`, `item_value`, `item_sort`, `item_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT dt.id, '已下架', 'OFFLINE', 30, 'ENABLED', '名片下架状态', 0, 0, 0
|
||||||
|
FROM `sys_dict_type` dt
|
||||||
|
WHERE dt.`dict_type_code` = 'card_publish_status'
|
||||||
|
AND dt.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_dict_item` di
|
||||||
|
WHERE di.`dict_type_id` = dt.`id` AND di.`item_value` = 'OFFLINE' AND di.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 租户内置角色建议在创建租户时自动初始化:
|
||||||
|
-- 1. TENANT_ADMIN:租户管理员,数据范围 TENANT
|
||||||
|
-- 2. TENANT_USER:普通用户,数据范围 SELF
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
INSERT INTO `sys_user`
|
||||||
|
(`tenant_id`, `user_type`, `username`, `password_hash`, `real_name`, `nick_name`, `gender`, `mobile`, `email`, `job_title`, `user_status`, `must_update_password`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 0, 'PLATFORM', 'admin', '$2a$10$h21lXc21EZ7U8PWklTLdFeXKNI23.e8R3KyERgnF4lDsu2duzoBsG', '平台管理员', '平台管理员', 'UNKNOWN', '13800000000', 'platform@easycard.local', '平台超级管理员', 'ENABLED', 0, 0, 0, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user` WHERE `tenant_id` = 0 AND `username` = 'admin' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user_role`
|
||||||
|
(`tenant_id`, `user_id`, `role_id`, `created_by`)
|
||||||
|
SELECT 0, u.id, r.id, 0
|
||||||
|
FROM `sys_user` u
|
||||||
|
JOIN `sys_role` r ON r.`tenant_id` = 0 AND r.`role_code` = 'PLATFORM_SUPER_ADMIN' AND r.`deleted` = 0
|
||||||
|
WHERE u.`tenant_id` = 0 AND u.`username` = 'admin' AND u.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user_role` ur WHERE ur.`user_id` = u.`id` AND ur.`role_id` = r.`id`
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_tenant`
|
||||||
|
(`tenant_code`, `tenant_name`, `tenant_short_name`, `contact_name`, `contact_phone`, `contact_email`, `tenant_status`, `expire_at`, `user_limit`, `storage_limit_mb`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT 'hengzhi-law', '衡知律师事务所', '衡知律所', '李主任', '13800138000', 'admin@hengzhi-law.com', 'ENABLED', '2027-03-31 23:59:59.000', 20, 10240, '演示租户', 1, 1, 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_tenant` WHERE `tenant_code` = 'hengzhi-law' AND `deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `tenant_miniapp_config`
|
||||||
|
(`tenant_id`, `env_code`, `miniapp_app_id`, `miniapp_app_secret`, `miniapp_name`, `miniapp_original_id`, `request_domain`, `upload_domain`, `download_domain`, `version_tag`, `publish_status`, `last_published_at`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'PROD', 'wx8b1c6d1f0a1e0001', 'demo-app-secret', '衡知律师事务所电子名片', 'gh_hengzhi_demo', 'http://127.0.0.1:8112', 'http://127.0.0.1:8112', 'http://127.0.0.1:8112', 'v1.0.0', 'PUBLISHED', CURRENT_TIMESTAMP(3), '演示小程序配置', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `tenant_miniapp_config` c WHERE c.`tenant_id` = t.`id` AND c.`env_code` = 'PROD' AND c.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role`
|
||||||
|
(`tenant_id`, `role_scope`, `role_code`, `role_name`, `data_scope`, `is_builtin`, `role_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'TENANT', 'TENANT_ADMIN', '租户管理员', 'TENANT', 1, 'ENABLED', '租户内置管理员角色', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role` r WHERE r.`tenant_id` = t.`id` AND r.`role_code` = 'TENANT_ADMIN' AND r.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_role`
|
||||||
|
(`tenant_id`, `role_scope`, `role_code`, `role_name`, `data_scope`, `is_builtin`, `role_status`, `remark`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'TENANT', 'TENANT_USER', '普通用户', 'SELF', 1, 'ENABLED', '租户内置普通用户角色', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role` r WHERE r.`tenant_id` = t.`id` AND r.`role_code` = 'TENANT_USER' AND r.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_department`
|
||||||
|
(`tenant_id`, `parent_id`, `dept_code`, `dept_name`, `dept_type`, `contact_phone`, `address`, `display_order`, `dept_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 0, 'nj-headquarters', '南京总部', 'HEADQUARTERS', '025-88886666', '南京市江宁区紫金研创中心', 10, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_department` d WHERE d.`tenant_id` = t.`id` AND d.`dept_code` = 'nj-headquarters' AND d.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_department`
|
||||||
|
(`tenant_id`, `parent_id`, `dept_code`, `dept_name`, `dept_type`, `contact_phone`, `address`, `display_order`, `dept_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 0, 'sh-branch', '上海分所', 'BRANCH', '021-66668888', '上海市浦东新区陆家嘴', 20, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_department` d WHERE d.`tenant_id` = t.`id` AND d.`dept_code` = 'sh-branch' AND d.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_firm_profile`
|
||||||
|
(`tenant_id`, `firm_name`, `firm_short_name`, `english_name`, `intro`, `hotline_phone`, `website_url`, `wechat_official_account`, `hq_address`, `hq_latitude`, `hq_longitude`, `display_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, '衡知律师事务所', '衡知律所', 'Hengzhi Law Firm', '衡知律师事务所是一家聚焦企业合规、民商事争议解决与知识产权服务的综合性律师事务所。', '13800138000', 'https://hengzhi-law.example.com', '衡知律师事务所', '南京市江宁区紫金研创中心', 31.9320000, 118.8250000, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_firm_profile` p WHERE p.`tenant_id` = t.`id` AND p.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_firm_practice_area`
|
||||||
|
(`tenant_id`, `area_code`, `area_name`, `display_order`, `area_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'corporate-compliance', '企业合规', 10, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_firm_practice_area` a WHERE a.`tenant_id` = t.`id` AND a.`area_code` = 'corporate-compliance' AND a.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_firm_practice_area`
|
||||||
|
(`tenant_id`, `area_code`, `area_name`, `display_order`, `area_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'civil-commercial', '民商事争议', 20, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_firm_practice_area` a WHERE a.`tenant_id` = t.`id` AND a.`area_code` = 'civil-commercial' AND a.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `org_firm_practice_area`
|
||||||
|
(`tenant_id`, `area_code`, `area_name`, `display_order`, `area_status`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'intellectual-property', '知识产权', 30, 'ENABLED', 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `org_firm_practice_area` a WHERE a.`tenant_id` = t.`id` AND a.`area_code` = 'intellectual-property' AND a.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user`
|
||||||
|
(`tenant_id`, `user_type`, `username`, `password_hash`, `real_name`, `nick_name`, `gender`, `mobile`, `email`, `dept_id`, `job_title`, `user_status`, `must_update_password`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'TENANT', 'tenant.admin', '$2a$10$jlXVQhz5FRZwl8VETGJbP.0ZwsaeRhR0tW/HGvLGinR1YhYSl7Fo2', '王律师', '王律师', 'UNKNOWN', '13800138001', 'tenant.admin@hengzhi-law.com', d.id, '租户管理员', 'ENABLED', 0, 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
JOIN `org_department` d ON d.`tenant_id` = t.`id` AND d.`dept_code` = 'nj-headquarters' AND d.`deleted` = 0
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user` u WHERE u.`tenant_id` = t.`id` AND u.`username` = 'tenant.admin' AND u.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user`
|
||||||
|
(`tenant_id`, `user_type`, `username`, `password_hash`, `real_name`, `nick_name`, `gender`, `mobile`, `email`, `dept_id`, `job_title`, `user_status`, `must_update_password`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'TENANT', 'li.qin', '$2a$10$jlXVQhz5FRZwl8VETGJbP.0ZwsaeRhR0tW/HGvLGinR1YhYSl7Fo2', '李勤', '李勤', 'UNKNOWN', '13800138002', 'li.qin@hengzhi-law.com', d.id, '高级合伙人', 'ENABLED', 0, 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
JOIN `org_department` d ON d.`tenant_id` = t.`id` AND d.`dept_code` = 'nj-headquarters' AND d.`deleted` = 0
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user` u WHERE u.`tenant_id` = t.`id` AND u.`username` = 'li.qin' AND u.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user`
|
||||||
|
(`tenant_id`, `user_type`, `username`, `password_hash`, `real_name`, `nick_name`, `gender`, `mobile`, `email`, `dept_id`, `job_title`, `user_status`, `must_update_password`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT t.id, 'TENANT', 'zhou.lin', '$2a$10$jlXVQhz5FRZwl8VETGJbP.0ZwsaeRhR0tW/HGvLGinR1YhYSl7Fo2', '周林', '周林', 'UNKNOWN', '13800138003', 'zhou.lin@hengzhi-law.com', d.id, '律师', 'ENABLED', 0, 1, 1, 0
|
||||||
|
FROM `sys_tenant` t
|
||||||
|
JOIN `org_department` d ON d.`tenant_id` = t.`id` AND d.`dept_code` = 'sh-branch' AND d.`deleted` = 0
|
||||||
|
WHERE t.`tenant_code` = 'hengzhi-law' AND t.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user` u WHERE u.`tenant_id` = t.`id` AND u.`username` = 'zhou.lin' AND u.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user_role`
|
||||||
|
(`tenant_id`, `user_id`, `role_id`, `created_by`)
|
||||||
|
SELECT u.`tenant_id`, u.`id`, r.`id`, 1
|
||||||
|
FROM `sys_user` u
|
||||||
|
JOIN `sys_role` r ON r.`tenant_id` = u.`tenant_id` AND r.`role_code` = 'TENANT_ADMIN' AND r.`deleted` = 0
|
||||||
|
WHERE u.`username` = 'tenant.admin' AND u.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user_role` ur WHERE ur.`user_id` = u.`id` AND ur.`role_id` = r.`id`
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sys_user_role`
|
||||||
|
(`tenant_id`, `user_id`, `role_id`, `created_by`)
|
||||||
|
SELECT u.`tenant_id`, u.`id`, r.`id`, 1
|
||||||
|
FROM `sys_user` u
|
||||||
|
JOIN `sys_role` r ON r.`tenant_id` = u.`tenant_id` AND r.`role_code` = 'TENANT_USER' AND r.`deleted` = 0
|
||||||
|
WHERE u.`username` IN ('li.qin', 'zhou.lin') AND u.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_user_role` ur WHERE ur.`user_id` = u.`id` AND ur.`role_id` = r.`id`
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile`
|
||||||
|
(`tenant_id`, `user_id`, `dept_id`, `card_name`, `card_title`, `mobile`, `email`, `office_address`, `bio`, `certificate_no`, `education_info`, `honor_info`, `is_public`, `is_recommended`, `publish_status`, `display_order`, `view_count`, `share_count`, `last_published_at`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT u.`tenant_id`, u.`id`, u.`dept_id`, '李勤', '高级合伙人', u.`mobile`, u.`email`, '南京市江宁区紫金研创中心', '长期服务于企业客户,擅长企业合规、公司治理与争议解决。', 'A123456789', '中国政法大学 法学硕士', '南京市优秀律师', 1, 1, 'PUBLISHED', 10, 18, 6, CURRENT_TIMESTAMP(3), 1, 1, 0
|
||||||
|
FROM `sys_user` u
|
||||||
|
WHERE u.`username` = 'li.qin' AND u.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile` c WHERE c.`tenant_id` = u.`tenant_id` AND c.`user_id` = u.`id` AND c.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile`
|
||||||
|
(`tenant_id`, `user_id`, `dept_id`, `card_name`, `card_title`, `mobile`, `email`, `office_address`, `bio`, `certificate_no`, `education_info`, `honor_info`, `is_public`, `is_recommended`, `publish_status`, `display_order`, `view_count`, `share_count`, `last_published_at`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT u.`tenant_id`, u.`id`, u.`dept_id`, '周林', '律师', u.`mobile`, u.`email`, '上海市浦东新区陆家嘴', '专注民商事争议与知识产权合规,常年服务成长型企业。', 'B987654321', '华东政法大学 法学学士', '青年律师业务能手', 1, 0, 'PUBLISHED', 20, 9, 2, CURRENT_TIMESTAMP(3), 1, 1, 0
|
||||||
|
FROM `sys_user` u
|
||||||
|
WHERE u.`username` = 'zhou.lin' AND u.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile` c WHERE c.`tenant_id` = u.`tenant_id` AND c.`user_id` = u.`id` AND c.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile_specialty`
|
||||||
|
(`tenant_id`, `card_id`, `practice_area_id`, `specialty_name`, `display_order`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT c.`tenant_id`, c.`id`, a.`id`, a.`area_name`, 10, 1, 1, 0
|
||||||
|
FROM `card_profile` c
|
||||||
|
JOIN `sys_user` u ON u.`id` = c.`user_id` AND u.`deleted` = 0
|
||||||
|
JOIN `org_firm_practice_area` a ON a.`tenant_id` = c.`tenant_id` AND a.`area_code` = 'corporate-compliance' AND a.`deleted` = 0
|
||||||
|
WHERE u.`username` = 'li.qin' AND c.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile_specialty` s WHERE s.`card_id` = c.`id` AND s.`specialty_name` = a.`area_name` AND s.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile_specialty`
|
||||||
|
(`tenant_id`, `card_id`, `practice_area_id`, `specialty_name`, `display_order`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT c.`tenant_id`, c.`id`, a.`id`, a.`area_name`, 20, 1, 1, 0
|
||||||
|
FROM `card_profile` c
|
||||||
|
JOIN `sys_user` u ON u.`id` = c.`user_id` AND u.`deleted` = 0
|
||||||
|
JOIN `org_firm_practice_area` a ON a.`tenant_id` = c.`tenant_id` AND a.`area_code` = 'civil-commercial' AND a.`deleted` = 0
|
||||||
|
WHERE u.`username` = 'li.qin' AND c.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile_specialty` s WHERE s.`card_id` = c.`id` AND s.`specialty_name` = a.`area_name` AND s.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile_specialty`
|
||||||
|
(`tenant_id`, `card_id`, `practice_area_id`, `specialty_name`, `display_order`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT c.`tenant_id`, c.`id`, a.`id`, a.`area_name`, 10, 1, 1, 0
|
||||||
|
FROM `card_profile` c
|
||||||
|
JOIN `sys_user` u ON u.`id` = c.`user_id` AND u.`deleted` = 0
|
||||||
|
JOIN `org_firm_practice_area` a ON a.`tenant_id` = c.`tenant_id` AND a.`area_code` = 'civil-commercial' AND a.`deleted` = 0
|
||||||
|
WHERE u.`username` = 'zhou.lin' AND c.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile_specialty` s WHERE s.`card_id` = c.`id` AND s.`specialty_name` = a.`area_name` AND s.`deleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `card_profile_specialty`
|
||||||
|
(`tenant_id`, `card_id`, `practice_area_id`, `specialty_name`, `display_order`, `created_by`, `updated_by`, `deleted`)
|
||||||
|
SELECT c.`tenant_id`, c.`id`, a.`id`, a.`area_name`, 20, 1, 1, 0
|
||||||
|
FROM `card_profile` c
|
||||||
|
JOIN `sys_user` u ON u.`id` = c.`user_id` AND u.`deleted` = 0
|
||||||
|
JOIN `org_firm_practice_area` a ON a.`tenant_id` = c.`tenant_id` AND a.`area_code` = 'intellectual-property' AND a.`deleted` = 0
|
||||||
|
WHERE u.`username` = 'zhou.lin' AND c.`deleted` = 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `card_profile_specialty` s WHERE s.`card_id` = c.`id` AND s.`specialty_name` = a.`area_name` AND s.`deleted` = 0
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `tenant_miniapp_config`
|
||||||
|
MODIFY COLUMN `miniapp_app_id` VARCHAR(64) NULL COMMENT '小程序AppID';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `org_firm_profile`
|
||||||
|
DROP COLUMN `display_status`;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
ALTER TABLE `sys_user`
|
||||||
|
ADD COLUMN `member_sort` INT NOT NULL DEFAULT 0 COMMENT '成员展示排序值' AFTER `user_status`,
|
||||||
|
ADD KEY `idx_sys_user_tenant_member_sort` (`tenant_id`, `member_sort`);
|
||||||
|
|
||||||
|
ALTER TABLE `sys_user`
|
||||||
|
MODIFY COLUMN `user_status` VARCHAR(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态:ENABLED/DISABLED';
|
||||||
|
|
||||||
|
UPDATE `sys_user`
|
||||||
|
SET `user_status` = 'DISABLED'
|
||||||
|
WHERE `deleted` = 0
|
||||||
|
AND `user_status` = 'LOCKED';
|
||||||
|
|
||||||
|
UPDATE `sys_user` u
|
||||||
|
LEFT JOIN `card_profile` c
|
||||||
|
ON c.`tenant_id` = u.`tenant_id`
|
||||||
|
AND c.`user_id` = u.`id`
|
||||||
|
AND c.`deleted` = 0
|
||||||
|
SET u.`member_sort` = COALESCE(c.`display_order`, 0)
|
||||||
|
WHERE u.`deleted` = 0;
|
||||||
|
|
||||||
|
WITH tenant_sort_base AS (
|
||||||
|
SELECT
|
||||||
|
u.`tenant_id`,
|
||||||
|
COALESCE(MAX(u.`member_sort`), 0) AS base_sort
|
||||||
|
FROM `sys_user` u
|
||||||
|
WHERE u.`deleted` = 0
|
||||||
|
GROUP BY u.`tenant_id`
|
||||||
|
),
|
||||||
|
users_without_card AS (
|
||||||
|
SELECT
|
||||||
|
u.`id`,
|
||||||
|
u.`tenant_id`,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY u.`tenant_id` ORDER BY u.`id` DESC) AS rn
|
||||||
|
FROM `sys_user` u
|
||||||
|
LEFT JOIN `card_profile` c
|
||||||
|
ON c.`tenant_id` = u.`tenant_id`
|
||||||
|
AND c.`user_id` = u.`id`
|
||||||
|
AND c.`deleted` = 0
|
||||||
|
WHERE u.`deleted` = 0
|
||||||
|
AND c.`id` IS NULL
|
||||||
|
)
|
||||||
|
UPDATE `sys_user` u
|
||||||
|
JOIN users_without_card w ON w.`id` = u.`id`
|
||||||
|
JOIN tenant_sort_base b ON b.`tenant_id` = w.`tenant_id`
|
||||||
|
SET u.`member_sort` = b.base_sort + (w.rn * 10);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.easycard.boot;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.SpringBootConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
classes = EasycardBootApplicationTests.TestApplication.class,
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||||||
|
)
|
||||||
|
class EasycardBootApplicationTests {
|
||||||
|
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@EnableAutoConfiguration(exclude = {
|
||||||
|
DataSourceAutoConfiguration.class,
|
||||||
|
RedisAutoConfiguration.class,
|
||||||
|
FlywayAutoConfiguration.class,
|
||||||
|
MybatisPlusAutoConfiguration.class
|
||||||
|
})
|
||||||
|
static class TestApplication {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.easycard.boot.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class EasycardCorsPropertiesTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withUserConfiguration(CorsPropertiesTestConfiguration.class)
|
||||||
|
.withPropertyValues(
|
||||||
|
"easycard.web.cors.allowed-origins=http://admin.example.com,http://114.66.22.180:3112",
|
||||||
|
"easycard.web.cors.allowed-origin-patterns=https://*.example.com",
|
||||||
|
"easycard.web.cors.allowed-methods=GET,POST,OPTIONS",
|
||||||
|
"easycard.web.cors.allowed-headers=Authorization,Content-Type",
|
||||||
|
"easycard.web.cors.exposed-headers=Authorization,X-Trace-Id",
|
||||||
|
"easycard.web.cors.allow-credentials=true",
|
||||||
|
"easycard.web.cors.max-age=600"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBindCorsPropertiesFromCommaSeparatedValues() {
|
||||||
|
contextRunner.run(context -> {
|
||||||
|
EasycardCorsProperties properties = context.getBean(EasycardCorsProperties.class);
|
||||||
|
|
||||||
|
assertThat(properties.getAllowedOrigins())
|
||||||
|
.containsExactly("http://admin.example.com", "http://114.66.22.180:3112");
|
||||||
|
assertThat(properties.getAllowedOriginPatterns())
|
||||||
|
.containsExactly("https://*.example.com");
|
||||||
|
assertThat(properties.getAllowedMethods())
|
||||||
|
.containsExactly("GET", "POST", "OPTIONS");
|
||||||
|
assertThat(properties.getAllowedHeaders())
|
||||||
|
.containsExactly("Authorization", "Content-Type");
|
||||||
|
assertThat(properties.getExposedHeaders())
|
||||||
|
.containsExactly("Authorization", "X-Trace-Id");
|
||||||
|
assertThat(properties.isAllowCredentials()).isTrue();
|
||||||
|
assertThat(properties.getMaxAge()).isEqualTo(600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBuildCorsConfigurationFromProperties() {
|
||||||
|
contextRunner.run(context -> {
|
||||||
|
EasycardCorsProperties properties = context.getBean(EasycardCorsProperties.class);
|
||||||
|
CorsConfigurationSource source = new SecurityConfig().corsConfigurationSource(properties);
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/api/v1/auth/login");
|
||||||
|
|
||||||
|
CorsConfiguration configuration = source.getCorsConfiguration(request);
|
||||||
|
|
||||||
|
assertThat(configuration).isNotNull();
|
||||||
|
assertThat(configuration.getAllowedOrigins())
|
||||||
|
.containsExactly("http://admin.example.com", "http://114.66.22.180:3112");
|
||||||
|
assertThat(configuration.getAllowedOriginPatterns())
|
||||||
|
.containsExactly("https://*.example.com");
|
||||||
|
assertThat(configuration.getAllowedMethods())
|
||||||
|
.containsExactly("GET", "POST", "OPTIONS");
|
||||||
|
assertThat(configuration.getAllowedHeaders())
|
||||||
|
.containsExactly("Authorization", "Content-Type");
|
||||||
|
assertThat(configuration.getExposedHeaders())
|
||||||
|
.containsExactly("Authorization", "X-Trace-Id");
|
||||||
|
assertThat(configuration.getAllowCredentials()).isTrue();
|
||||||
|
assertThat(configuration.getMaxAge()).isEqualTo(600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(EasycardCorsProperties.class)
|
||||||
|
static class CorsPropertiesTestConfiguration {
|
||||||
|
}
|
||||||
|
}
|
||||||
50
backend/easycard-common/pom.xml
Normal file
50
backend/easycard-common/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<name>easycard-common</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.6</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.easycard.common.api;
|
||||||
|
|
||||||
|
public record ApiResponse<T>(String code, String message, T data) {
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return new ApiResponse<>("0", "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(String message, T data) {
|
||||||
|
return new ApiResponse<>("0", message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> fail(String code, String message) {
|
||||||
|
return new ApiResponse<>(code, message, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.easycard.common.auth;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtTokenService {
|
||||||
|
|
||||||
|
private final String jwtSecret;
|
||||||
|
private SecretKey secretKey;
|
||||||
|
|
||||||
|
public JwtTokenService(@Value("${easycard.security.jwt-secret}") String jwtSecret) {
|
||||||
|
this.jwtSecret = jwtSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
String normalized = jwtSecret;
|
||||||
|
if (jwtSecret.length() < 32) {
|
||||||
|
normalized = String.format("%-32s", jwtSecret).replace(' ', '0');
|
||||||
|
}
|
||||||
|
byte[] secretBytes = normalized.getBytes(StandardCharsets.UTF_8);
|
||||||
|
this.secretKey = Keys.hmacShaKeyFor(secretBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(LoginUser loginUser, Duration ttl) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(String.valueOf(loginUser.userId()))
|
||||||
|
.issuedAt(Date.from(now))
|
||||||
|
.expiration(Date.from(now.plus(ttl)))
|
||||||
|
.claims(Map.of(
|
||||||
|
"tenantId", loginUser.tenantId(),
|
||||||
|
"username", loginUser.username(),
|
||||||
|
"realName", loginUser.realName(),
|
||||||
|
"userType", loginUser.userType(),
|
||||||
|
"roles", loginUser.roleCodes()
|
||||||
|
))
|
||||||
|
.signWith(secretKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Optional<LoginUser> parseToken(String token) {
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Claims claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload();
|
||||||
|
Object rolesObject = claims.get("roles");
|
||||||
|
List<String> roles = rolesObject instanceof List<?> list
|
||||||
|
? list.stream().map(String::valueOf).toList()
|
||||||
|
: List.of();
|
||||||
|
return Optional.of(new LoginUser(
|
||||||
|
Long.valueOf(claims.getSubject()),
|
||||||
|
Long.valueOf(String.valueOf(claims.get("tenantId"))),
|
||||||
|
String.valueOf(claims.get("username")),
|
||||||
|
String.valueOf(claims.get("realName")),
|
||||||
|
String.valueOf(claims.get("userType")),
|
||||||
|
roles
|
||||||
|
));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.easycard.common.auth;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record LoginUser(
|
||||||
|
Long userId,
|
||||||
|
Long tenantId,
|
||||||
|
String username,
|
||||||
|
String realName,
|
||||||
|
String userType,
|
||||||
|
List<String> roleCodes
|
||||||
|
) implements Serializable {
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.easycard.common.auth;
|
||||||
|
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
public final class SecurityUtils {
|
||||||
|
|
||||||
|
private SecurityUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoginUser getRequiredLoginUser() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
|
||||||
|
throw new BusinessException("UNAUTHORIZED", "登录状态已失效");
|
||||||
|
}
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasRole(String roleCode) {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return authentication != null
|
||||||
|
&& authentication.getAuthorities().stream().anyMatch(item -> roleCode.equals(item.getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.easycard.common.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
|
||||||
|
return builder -> builder
|
||||||
|
.timeZone(TimeZone.getTimeZone("Asia/Shanghai"))
|
||||||
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.easycard.common.exception;
|
||||||
|
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
public BusinessException(String code, String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.easycard.common.storage;
|
||||||
|
|
||||||
|
public final class StorageUrlUtils {
|
||||||
|
|
||||||
|
private StorageUrlUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildPublicUrl(String publicEndpoint, String bucketName, String objectKey, String fallbackUrl) {
|
||||||
|
if (isBlank(bucketName) || isBlank(objectKey)) {
|
||||||
|
return fallbackUrl == null ? "" : fallbackUrl;
|
||||||
|
}
|
||||||
|
String endpoint = trimTrailingSlash(publicEndpoint);
|
||||||
|
if (endpoint.isEmpty()) {
|
||||||
|
return fallbackUrl == null ? "" : fallbackUrl;
|
||||||
|
}
|
||||||
|
return endpoint + "/" + trimSlashes(bucketName) + "/" + trimLeadingSlash(objectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimTrailingSlash(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int end = value.length();
|
||||||
|
while (end > 0 && value.charAt(end - 1) == '/') {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
return value.substring(0, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimLeadingSlash(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int start = 0;
|
||||||
|
while (start < value.length() && value.charAt(start) == '/') {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
return value.substring(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimSlashes(String value) {
|
||||||
|
return trimTrailingSlash(trimLeadingSlash(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.easycard.common.tenant;
|
||||||
|
|
||||||
|
public record TenantContext(Long tenantId, String tenantCode, String miniappAppId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.easycard.common.tenant;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class TenantContextHolder {
|
||||||
|
|
||||||
|
private static final ThreadLocal<TenantContext> CONTEXT = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private TenantContextHolder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set(TenantContext tenantContext) {
|
||||||
|
CONTEXT.set(tenantContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<TenantContext> getOptional() {
|
||||||
|
return Optional.ofNullable(CONTEXT.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TenantContext getRequired() {
|
||||||
|
TenantContext tenantContext = CONTEXT.get();
|
||||||
|
if (tenantContext == null) {
|
||||||
|
throw new IllegalStateException("租户上下文不存在");
|
||||||
|
}
|
||||||
|
return tenantContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
CONTEXT.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.easycard.common.web;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
public final class ClientRequestUtils {
|
||||||
|
|
||||||
|
private ClientRequestUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientIp(HttpServletRequest request) {
|
||||||
|
String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||||
|
if (StringUtils.hasText(forwardedFor)) {
|
||||||
|
return forwardedFor.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
String realIp = request.getHeader("X-Real-IP");
|
||||||
|
if (StringUtils.hasText(realIp)) {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.easycard.common.web;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ApiResponse<Void> handleBusinessException(BusinessException exception) {
|
||||||
|
return ApiResponse.fail(exception.getCode(), exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({
|
||||||
|
MethodArgumentNotValidException.class,
|
||||||
|
BindException.class,
|
||||||
|
ConstraintViolationException.class,
|
||||||
|
HttpMessageNotReadableException.class
|
||||||
|
})
|
||||||
|
public ApiResponse<Void> handleValidationException(Exception exception) {
|
||||||
|
return ApiResponse.fail("VALIDATION_ERROR", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||||
|
public ApiResponse<Void> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException exception) {
|
||||||
|
return ApiResponse.fail("FILE_TOO_LARGE", "上传图片不能超过 5MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ApiResponse<Void> handleException(Exception exception) {
|
||||||
|
return ApiResponse.fail("INTERNAL_SERVER_ERROR", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
50
backend/easycard-module-card/pom.xml
Normal file
50
backend/easycard-module-card/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-card</artifactId>
|
||||||
|
<name>easycard-module-card</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-org</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-user</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-stat</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-file</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.easycard.module.card.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.web.ClientRequestUtils;
|
||||||
|
import com.easycard.module.card.service.CardProfileService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/open")
|
||||||
|
public class OpenMiniappController {
|
||||||
|
|
||||||
|
private final CardProfileService cardProfileService;
|
||||||
|
|
||||||
|
public OpenMiniappController(CardProfileService cardProfileService) {
|
||||||
|
this.cardProfileService = cardProfileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/firm/profile")
|
||||||
|
public ApiResponse<CardProfileService.OpenFirmView> getFirmProfile() {
|
||||||
|
return ApiResponse.success(cardProfileService.getOpenFirmProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cards")
|
||||||
|
public ApiResponse<List<CardProfileService.OpenCardListItem>> listCards(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String office,
|
||||||
|
@RequestParam(required = false) String practiceArea
|
||||||
|
) {
|
||||||
|
return ApiResponse.success(cardProfileService.listOpenCards(keyword, office, practiceArea));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cards/{cardId}")
|
||||||
|
public ApiResponse<CardProfileService.OpenCardDetailView> getCardDetail(
|
||||||
|
@PathVariable Long cardId,
|
||||||
|
@RequestParam(defaultValue = "DIRECT") String sourceType,
|
||||||
|
@RequestParam(required = false) Long shareFromCardId,
|
||||||
|
HttpServletRequest request
|
||||||
|
) {
|
||||||
|
return ApiResponse.success(cardProfileService.getOpenCardDetail(
|
||||||
|
cardId,
|
||||||
|
sourceType,
|
||||||
|
shareFromCardId,
|
||||||
|
ClientRequestUtils.getClientIp(request),
|
||||||
|
request.getRequestURI()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cards/{cardId}/share")
|
||||||
|
public ApiResponse<Void> share(@PathVariable Long cardId, @RequestBody(required = false) ShareRequest request) {
|
||||||
|
String shareChannel = request == null || request.shareChannel() == null ? "WECHAT_FRIEND" : request.shareChannel();
|
||||||
|
String sharePath = request == null ? null : request.sharePath();
|
||||||
|
cardProfileService.recordShare(cardId, shareChannel, sharePath);
|
||||||
|
return ApiResponse.success("记录成功", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ShareRequest(String shareChannel, String sharePath) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.easycard.module.card.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.card.service.CardProfileService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/tenant")
|
||||||
|
public class TenantCardController {
|
||||||
|
|
||||||
|
private final CardProfileService cardProfileService;
|
||||||
|
|
||||||
|
public TenantCardController(CardProfileService cardProfileService) {
|
||||||
|
this.cardProfileService = cardProfileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cards")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<CardProfileService.CardSummaryView>> listCards(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.success(cardProfileService.listTenantCards(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cards")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.CardDetailView> createCard(@Valid @RequestBody UpsertCardCommand request) {
|
||||||
|
return ApiResponse.success(cardProfileService.createCard(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cards/{cardId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.CardDetailView> getCard(@PathVariable Long cardId) {
|
||||||
|
return ApiResponse.success(cardProfileService.getCardDetail(cardId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cards/{cardId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.CardDetailView> updateCard(@PathVariable Long cardId, @Valid @RequestBody UpsertCardCommand request) {
|
||||||
|
return ApiResponse.success(cardProfileService.saveCard(cardId, request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/cards/{cardId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<Void> deleteCard(@PathVariable Long cardId) {
|
||||||
|
cardProfileService.deleteCard(cardId);
|
||||||
|
return ApiResponse.success("删除成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cards/sort")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<Void> sortCards(@Valid @RequestBody SortCardsRequest request) {
|
||||||
|
cardProfileService.sortCards(request.cardIds());
|
||||||
|
return ApiResponse.success("排序已生效", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cards/me")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.CardDetailView> getMyCard() {
|
||||||
|
return ApiResponse.success(cardProfileService.getMyCard());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/cards/me")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.CardDetailView> saveMyCard(@Valid @RequestBody UpsertCardCommand request) {
|
||||||
|
return ApiResponse.success(cardProfileService.saveMyCard(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats/overview")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<CardProfileService.DashboardStatsView> overview() {
|
||||||
|
return ApiResponse.success(cardProfileService.getDashboardStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats/trend")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<CardProfileService.CardTrendView>> trend(@RequestParam(defaultValue = "7") int days) {
|
||||||
|
return ApiResponse.success(cardProfileService.getTrend(days));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpsertCardCommand(
|
||||||
|
Long userId,
|
||||||
|
Long deptId,
|
||||||
|
@NotBlank(message = "名片姓名不能为空") String cardName,
|
||||||
|
String cardTitle,
|
||||||
|
String mobile,
|
||||||
|
String telephone,
|
||||||
|
String email,
|
||||||
|
String officeAddress,
|
||||||
|
Long avatarAssetId,
|
||||||
|
Long coverAssetId,
|
||||||
|
Long wechatQrAssetId,
|
||||||
|
String bio,
|
||||||
|
String certificateNo,
|
||||||
|
String educationInfo,
|
||||||
|
String honorInfo,
|
||||||
|
@NotNull(message = "是否公开不能为空") Integer isPublic,
|
||||||
|
@NotNull(message = "是否推荐不能为空") Integer isRecommended,
|
||||||
|
@NotBlank(message = "发布状态不能为空") String publishStatus,
|
||||||
|
Integer displayOrder,
|
||||||
|
List<String> specialties
|
||||||
|
) {
|
||||||
|
CardProfileService.UpsertCardRequest toServiceRequest() {
|
||||||
|
return new CardProfileService.UpsertCardRequest(
|
||||||
|
userId, deptId, cardName, cardTitle, mobile, telephone, email, officeAddress,
|
||||||
|
avatarAssetId, coverAssetId, wechatQrAssetId, bio, certificateNo, educationInfo,
|
||||||
|
honorInfo, isPublic, isRecommended, publishStatus, displayOrder == null ? 0 : displayOrder, specialties
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record SortCardsRequest(
|
||||||
|
@Size(min = 1, message = "排序名片不能为空") List<@NotNull(message = "名片ID不能为空") Long> cardIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.easycard.module.card.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("card_profile")
|
||||||
|
public class CardProfileDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long userId;
|
||||||
|
private Long deptId;
|
||||||
|
private String cardName;
|
||||||
|
private String cardTitle;
|
||||||
|
private String mobile;
|
||||||
|
private String telephone;
|
||||||
|
private String email;
|
||||||
|
private String officeAddress;
|
||||||
|
private Long avatarAssetId;
|
||||||
|
private Long coverAssetId;
|
||||||
|
private Long wechatQrAssetId;
|
||||||
|
private String bio;
|
||||||
|
private String certificateNo;
|
||||||
|
private String educationInfo;
|
||||||
|
private String honorInfo;
|
||||||
|
private Integer isPublic;
|
||||||
|
private Integer isRecommended;
|
||||||
|
private String publishStatus;
|
||||||
|
private Integer displayOrder;
|
||||||
|
private Long viewCount;
|
||||||
|
private Long shareCount;
|
||||||
|
private LocalDateTime lastPublishedAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.easycard.module.card.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("card_profile_specialty")
|
||||||
|
public class CardProfileSpecialtyDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long cardId;
|
||||||
|
private Long practiceAreaId;
|
||||||
|
private String specialtyName;
|
||||||
|
private Integer displayOrder;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.card.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileDO;
|
||||||
|
|
||||||
|
public interface CardProfileMapper extends BaseMapper<CardProfileDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.easycard.module.card.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileSpecialtyDO;
|
||||||
|
import org.apache.ibatis.annotations.Delete;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
public interface CardProfileSpecialtyMapper extends BaseMapper<CardProfileSpecialtyDO> {
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM card_profile_specialty
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
AND card_id = #{cardId}
|
||||||
|
""")
|
||||||
|
int deleteForceByTenantIdAndCardId(@Param("tenantId") Long tenantId, @Param("cardId") Long cardId);
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM card_profile_specialty
|
||||||
|
WHERE tenant_id = #{tenantId}
|
||||||
|
""")
|
||||||
|
int deleteForceByTenantId(@Param("tenantId") Long tenantId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
package com.easycard.module.card.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.common.storage.StorageUrlUtils;
|
||||||
|
import com.easycard.common.tenant.TenantContext;
|
||||||
|
import com.easycard.common.tenant.TenantContextHolder;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileDO;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileSpecialtyDO;
|
||||||
|
import com.easycard.module.card.dal.mapper.CardProfileMapper;
|
||||||
|
import com.easycard.module.card.dal.mapper.CardProfileSpecialtyMapper;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||||
|
import com.easycard.module.file.dal.mapper.FileAssetMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmProfileDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgFirmProfileMapper;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardStatDailyDO;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardStatDailyMapper;
|
||||||
|
import com.easycard.module.stat.service.CardEventService;
|
||||||
|
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||||
|
import com.easycard.module.user.service.UserAuditService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CardProfileService {
|
||||||
|
|
||||||
|
private static final String AUTO_MANAGED_LAWYER_REMARK = "AUTO_MANAGED_LAWYER";
|
||||||
|
private static final String AUTO_MANAGED_ROLE_CODE = "TENANT_USER";
|
||||||
|
|
||||||
|
@Value("${easycard.storage.public-endpoint:${easycard.storage.endpoint}}")
|
||||||
|
private String publicEndpoint;
|
||||||
|
|
||||||
|
private final CardProfileMapper cardProfileMapper;
|
||||||
|
private final CardProfileSpecialtyMapper cardProfileSpecialtyMapper;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||||
|
private final OrgFirmProfileMapper orgFirmProfileMapper;
|
||||||
|
private final FileAssetMapper fileAssetMapper;
|
||||||
|
private final CardStatDailyMapper cardStatDailyMapper;
|
||||||
|
private final CardEventService cardEventService;
|
||||||
|
private final SysRoleMapper sysRoleMapper;
|
||||||
|
private final SysUserRoleMapper sysUserRoleMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserAuditService userAuditService;
|
||||||
|
|
||||||
|
public List<CardSummaryView> listTenantCards(String keyword) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
List<CardProfileDO> cards = cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.like(StringUtils.hasText(keyword), CardProfileDO::getCardName, keyword)
|
||||||
|
.orderByAsc(CardProfileDO::getDisplayOrder, CardProfileDO::getId));
|
||||||
|
return filterLawyerCards(loginUser.tenantId(), cards).stream().map(this::toSummaryView).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CardDetailView createCard(UpsertCardRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||||
|
SysUserDO hiddenUser = createHiddenLawyerUser(loginUser, request);
|
||||||
|
|
||||||
|
CardProfileDO card = new CardProfileDO();
|
||||||
|
card.setTenantId(loginUser.tenantId());
|
||||||
|
card.setUserId(hiddenUser.getId());
|
||||||
|
card.setCreatedBy(loginUser.userId());
|
||||||
|
card.setDeleted(0);
|
||||||
|
card.setViewCount(0L);
|
||||||
|
card.setShareCount(0L);
|
||||||
|
applyUpsertRequest(card, request, loginUser.userId(), true);
|
||||||
|
card.setDisplayOrder(nextCardDisplayOrder(loginUser.tenantId()));
|
||||||
|
cardProfileMapper.insert(card);
|
||||||
|
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||||
|
return toDetailView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardDetailView getCardDetail(Long cardId) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||||
|
return toDetailView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CardDetailView getMyCard() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardProfileDO::getUserId, loginUser.userId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (card == null) {
|
||||||
|
SysUserDO user = sysUserMapper.selectById(loginUser.userId());
|
||||||
|
card = new CardProfileDO();
|
||||||
|
card.setTenantId(loginUser.tenantId());
|
||||||
|
card.setUserId(loginUser.userId());
|
||||||
|
card.setDeptId(user == null ? null : user.getDeptId());
|
||||||
|
card.setCardName(user == null ? loginUser.realName() : user.getRealName());
|
||||||
|
card.setCardTitle(user == null ? "" : user.getJobTitle());
|
||||||
|
card.setMobile(user == null ? "" : user.getMobile());
|
||||||
|
card.setEmail(user == null ? "" : user.getEmail());
|
||||||
|
card.setOfficeAddress("");
|
||||||
|
card.setIsPublic(1);
|
||||||
|
card.setIsRecommended(0);
|
||||||
|
card.setPublishStatus("DRAFT");
|
||||||
|
card.setDisplayOrder(999);
|
||||||
|
card.setViewCount(0L);
|
||||||
|
card.setShareCount(0L);
|
||||||
|
card.setCreatedBy(loginUser.userId());
|
||||||
|
card.setUpdatedBy(loginUser.userId());
|
||||||
|
card.setDeleted(0);
|
||||||
|
cardProfileMapper.insert(card);
|
||||||
|
}
|
||||||
|
return toDetailView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CardDetailView saveMyCard(UpsertCardRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardProfileDO::getUserId, loginUser.userId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (card == null) {
|
||||||
|
card = new CardProfileDO();
|
||||||
|
card.setTenantId(loginUser.tenantId());
|
||||||
|
card.setUserId(loginUser.userId());
|
||||||
|
card.setCreatedBy(loginUser.userId());
|
||||||
|
card.setDeleted(0);
|
||||||
|
card.setViewCount(0L);
|
||||||
|
card.setShareCount(0L);
|
||||||
|
}
|
||||||
|
applyUpsertRequest(card, request, loginUser.userId(), false);
|
||||||
|
if (card.getId() == null) {
|
||||||
|
cardProfileMapper.insert(card);
|
||||||
|
} else {
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
}
|
||||||
|
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||||
|
return toDetailView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CardDetailView saveCard(Long cardId, UpsertCardRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||||
|
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||||
|
applyUpsertRequest(card, request, loginUser.userId(), true);
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
replaceSpecialties(card.getTenantId(), card.getId(), request.specialties(), loginUser.userId());
|
||||||
|
return toDetailView(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteCard(Long cardId) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
CardProfileDO card = getRequiredTenantCard(cardId, loginUser.tenantId());
|
||||||
|
Long userId = card.getUserId();
|
||||||
|
|
||||||
|
card.setDeleted(1);
|
||||||
|
card.setUpdatedBy(loginUser.userId());
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
cardProfileSpecialtyMapper.deleteForceByTenantIdAndCardId(loginUser.tenantId(), cardId);
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
SysUserDO user = sysUserMapper.selectById(userId);
|
||||||
|
if (user != null
|
||||||
|
&& loginUser.tenantId().equals(user.getTenantId())
|
||||||
|
&& Integer.valueOf(0).equals(user.getDeleted())
|
||||||
|
&& AUTO_MANAGED_LAWYER_REMARK.equals(user.getRemark())) {
|
||||||
|
sysUserRoleMapper.delete(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||||
|
.eq(SysUserRoleDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(SysUserRoleDO::getUserId, userId));
|
||||||
|
user.setDeleted(1);
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "CARD", "CARD_PROFILE", cardId, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void sortCards(List<Long> cardIds) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
if (cardIds == null || cardIds.isEmpty()) {
|
||||||
|
throw new BusinessException("CARD_SORT_INVALID", "排序数据不能为空");
|
||||||
|
}
|
||||||
|
Set<Long> uniqueCardIds = new HashSet<>(cardIds);
|
||||||
|
if (uniqueCardIds.size() != cardIds.size()) {
|
||||||
|
throw new BusinessException("CARD_SORT_INVALID", "排序数据存在重复名片");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CardProfileDO> tenantCards = filterLawyerCards(loginUser.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.select(CardProfileDO::getId, CardProfileDO::getUserId)));
|
||||||
|
Set<Long> expectedCardIds = tenantCards.stream().map(CardProfileDO::getId).collect(Collectors.toSet());
|
||||||
|
if (expectedCardIds.size() != uniqueCardIds.size() || !expectedCardIds.equals(uniqueCardIds)) {
|
||||||
|
throw new BusinessException("CARD_SORT_INVALID", "排序数据必须包含当前租户全部律师名片");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, CardProfileDO> cardMap = cardProfileMapper.selectBatchIds(cardIds).stream()
|
||||||
|
.filter(item -> loginUser.tenantId().equals(item.getTenantId()) && Integer.valueOf(0).equals(item.getDeleted()))
|
||||||
|
.collect(Collectors.toMap(CardProfileDO::getId, Function.identity()));
|
||||||
|
for (int i = 0; i < cardIds.size(); i++) {
|
||||||
|
Long cardId = cardIds.get(i);
|
||||||
|
CardProfileDO card = cardMap.get(cardId);
|
||||||
|
if (card == null) {
|
||||||
|
throw new BusinessException("CARD_SORT_INVALID", "律师名片不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
card.setDisplayOrder((i + 1) * 10);
|
||||||
|
card.setUpdatedBy(loginUser.userId());
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "CARD", "CARD_PROFILE", 0L, "SORT");
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashboardStatsView getDashboardStats() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
List<CardProfileDO> cards = filterLawyerCards(loginUser.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)));
|
||||||
|
long publishedCount = cards.stream().filter(item -> "PUBLISHED".equals(item.getPublishStatus()) && Integer.valueOf(1).equals(item.getIsPublic())).count();
|
||||||
|
long totalViews = cards.stream().mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount()).sum();
|
||||||
|
long totalShares = cards.stream().mapToLong(item -> item.getShareCount() == null ? 0L : item.getShareCount()).sum();
|
||||||
|
Set<Long> lawyerCardIds = cards.stream().map(CardProfileDO::getId).collect(Collectors.toSet());
|
||||||
|
long todayViews = cardStatDailyMapper.selectList(Wrappers.<CardStatDailyDO>lambdaQuery()
|
||||||
|
.eq(CardStatDailyDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(CardStatDailyDO::getStatDate, LocalDate.now()))
|
||||||
|
.stream()
|
||||||
|
.filter(item -> lawyerCardIds.contains(item.getCardId()))
|
||||||
|
.mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount())
|
||||||
|
.sum();
|
||||||
|
return new DashboardStatsView(cards.size(), publishedCount, totalViews, totalShares, todayViews);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CardTrendView> getTrend(int days) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
LocalDate start = LocalDate.now().minusDays(Math.max(days - 1, 0));
|
||||||
|
return cardStatDailyMapper.selectList(Wrappers.<CardStatDailyDO>lambdaQuery()
|
||||||
|
.eq(CardStatDailyDO::getTenantId, loginUser.tenantId())
|
||||||
|
.ge(CardStatDailyDO::getStatDate, start)
|
||||||
|
.orderByAsc(CardStatDailyDO::getStatDate))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(CardStatDailyDO::getStatDate))
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.map(entry -> new CardTrendView(
|
||||||
|
entry.getKey().toString(),
|
||||||
|
entry.getValue().stream().mapToLong(item -> item.getViewCount() == null ? 0L : item.getViewCount()).sum(),
|
||||||
|
entry.getValue().stream().mapToLong(item -> item.getShareCount() == null ? 0L : item.getShareCount()).sum()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenFirmView getOpenFirmProfile() {
|
||||||
|
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||||
|
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (profile == null) {
|
||||||
|
throw new BusinessException("FIRM_PROFILE_NOT_FOUND", "事务所主页未配置");
|
||||||
|
}
|
||||||
|
List<String> offices = orgDepartmentMapper.selectList(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||||
|
.eq(OrgDepartmentDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(OrgDepartmentDO::getDeleted, 0)
|
||||||
|
.eq(OrgDepartmentDO::getDeptStatus, "ENABLED")
|
||||||
|
.orderByAsc(OrgDepartmentDO::getDisplayOrder, OrgDepartmentDO::getId))
|
||||||
|
.stream()
|
||||||
|
.map(OrgDepartmentDO::getDeptName)
|
||||||
|
.toList();
|
||||||
|
List<String> areas = cardProfileSpecialtyMapper.selectList(Wrappers.<CardProfileSpecialtyDO>lambdaQuery()
|
||||||
|
.eq(CardProfileSpecialtyDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(CardProfileSpecialtyDO::getDeleted, 0))
|
||||||
|
.stream()
|
||||||
|
.map(CardProfileSpecialtyDO::getSpecialtyName)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
long lawyerCount = filterLawyerCards(tenantContext.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||||
|
.eq(CardProfileDO::getIsPublic, 1)))
|
||||||
|
.size();
|
||||||
|
return new OpenFirmView(
|
||||||
|
profile.getFirmName(),
|
||||||
|
resolveAssetUrl(profile.getLogoAssetId()),
|
||||||
|
resolveAssetUrl(profile.getHeroAssetId()),
|
||||||
|
profile.getIntro(),
|
||||||
|
profile.getHotlinePhone(),
|
||||||
|
profile.getHqAddress(),
|
||||||
|
profile.getHqLatitude() == null ? null : profile.getHqLatitude().doubleValue(),
|
||||||
|
profile.getHqLongitude() == null ? null : profile.getHqLongitude().doubleValue(),
|
||||||
|
(long) offices.size(),
|
||||||
|
lawyerCount,
|
||||||
|
offices,
|
||||||
|
areas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OpenCardListItem> listOpenCards(String keyword, String office, String practiceArea) {
|
||||||
|
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||||
|
List<CardProfileDO> cards = filterLawyerCards(tenantContext.tenantId(), cardProfileMapper.selectList(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||||
|
.eq(CardProfileDO::getIsPublic, 1)
|
||||||
|
.orderByAsc(CardProfileDO::getDisplayOrder, CardProfileDO::getId)));
|
||||||
|
if (cards.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Long> deptIds = cards.stream().map(CardProfileDO::getDeptId).filter(id -> id != null && id > 0).distinct().toList();
|
||||||
|
Map<Long, OrgDepartmentDO> deptMap = deptIds.isEmpty() ? Map.of() : orgDepartmentMapper.selectBatchIds(deptIds)
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(OrgDepartmentDO::getId, Function.identity()));
|
||||||
|
Map<Long, List<String>> specialtyMap = loadSpecialtyMap(cards.stream().map(CardProfileDO::getId).toList());
|
||||||
|
return cards.stream()
|
||||||
|
.filter(card -> {
|
||||||
|
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||||
|
List<String> specialties = specialtyMap.getOrDefault(card.getId(), List.of());
|
||||||
|
boolean keywordMatched = !StringUtils.hasText(keyword)
|
||||||
|
|| card.getCardName().contains(keyword)
|
||||||
|
|| deptName.contains(keyword)
|
||||||
|
|| specialties.stream().anyMatch(item -> item.contains(keyword));
|
||||||
|
boolean officeMatched = !StringUtils.hasText(office) || office.equals(deptName);
|
||||||
|
boolean areaMatched = !StringUtils.hasText(practiceArea) || specialties.stream().anyMatch(item -> item.equals(practiceArea));
|
||||||
|
return keywordMatched && officeMatched && areaMatched;
|
||||||
|
})
|
||||||
|
.map(card -> {
|
||||||
|
String deptName = deptMap.containsKey(card.getDeptId()) ? deptMap.get(card.getDeptId()).getDeptName() : "";
|
||||||
|
return new OpenCardListItem(
|
||||||
|
card.getId(),
|
||||||
|
card.getCardName(),
|
||||||
|
card.getCardTitle(),
|
||||||
|
deptName,
|
||||||
|
card.getMobile(),
|
||||||
|
card.getEmail(),
|
||||||
|
resolveAssetUrl(card.getAvatarAssetId()),
|
||||||
|
specialtyMap.getOrDefault(card.getId(), List.of())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public OpenCardDetailView getOpenCardDetail(Long cardId, String sourceType, Long shareFromCardId, String viewerIp, String pagePath) {
|
||||||
|
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||||
|
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(CardProfileDO::getId, cardId)
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.eq(CardProfileDO::getPublishStatus, "PUBLISHED")
|
||||||
|
.eq(CardProfileDO::getIsPublic, 1)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (card == null || !hasLawyerRole(tenantContext.tenantId(), card.getUserId())) {
|
||||||
|
throw new BusinessException("CARD_NOT_FOUND", "名片不存在或未公开");
|
||||||
|
}
|
||||||
|
card.setViewCount((card.getViewCount() == null ? 0L : card.getViewCount()) + 1);
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
cardEventService.recordView(tenantContext.tenantId(), tenantContext.miniappAppId(), card.getId(), sourceType, shareFromCardId, viewerIp, pagePath);
|
||||||
|
OrgDepartmentDO department = card.getDeptId() == null ? null : orgDepartmentMapper.selectById(card.getDeptId());
|
||||||
|
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
return new OpenCardDetailView(
|
||||||
|
card.getId(),
|
||||||
|
card.getCardName(),
|
||||||
|
card.getCardTitle(),
|
||||||
|
department == null ? "" : department.getDeptName(),
|
||||||
|
card.getMobile(),
|
||||||
|
card.getEmail(),
|
||||||
|
card.getOfficeAddress(),
|
||||||
|
resolveAssetUrl(card.getAvatarAssetId()),
|
||||||
|
resolveAssetUrl(card.getCoverAssetId()),
|
||||||
|
resolveAssetUrl(card.getWechatQrAssetId()),
|
||||||
|
card.getBio(),
|
||||||
|
loadSpecialtyMap(List.of(card.getId())).getOrDefault(card.getId(), List.of()),
|
||||||
|
profile == null ? "" : profile.getFirmName(),
|
||||||
|
profile == null ? "" : profile.getHqAddress(),
|
||||||
|
profile == null || profile.getHqLatitude() == null ? null : profile.getHqLatitude().doubleValue(),
|
||||||
|
profile == null || profile.getHqLongitude() == null ? null : profile.getHqLongitude().doubleValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void recordShare(Long cardId, String shareChannel, String sharePath) {
|
||||||
|
TenantContext tenantContext = TenantContextHolder.getRequired();
|
||||||
|
CardProfileDO card = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, tenantContext.tenantId())
|
||||||
|
.eq(CardProfileDO::getId, cardId)
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (card == null || !hasLawyerRole(tenantContext.tenantId(), card.getUserId())) {
|
||||||
|
throw new BusinessException("CARD_NOT_FOUND", "名片不存在");
|
||||||
|
}
|
||||||
|
card.setShareCount((card.getShareCount() == null ? 0L : card.getShareCount()) + 1);
|
||||||
|
cardProfileMapper.updateById(card);
|
||||||
|
cardEventService.recordShare(tenantContext.tenantId(), tenantContext.miniappAppId(), cardId, shareChannel, sharePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardProfileDO getRequiredTenantCard(Long cardId, Long tenantId) {
|
||||||
|
CardProfileDO card = cardProfileMapper.selectById(cardId);
|
||||||
|
if (card == null || Integer.valueOf(1).equals(card.getDeleted()) || !tenantId.equals(card.getTenantId())) {
|
||||||
|
throw new BusinessException("CARD_NOT_FOUND", "名片不存在");
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDepartment(Long tenantId, Long deptId) {
|
||||||
|
if (deptId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OrgDepartmentDO department = orgDepartmentMapper.selectById(deptId);
|
||||||
|
if (department == null || Integer.valueOf(1).equals(department.getDeleted()) || !tenantId.equals(department.getTenantId())) {
|
||||||
|
throw new BusinessException("DEPARTMENT_NOT_FOUND", "所属组织不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CardProfileDO> filterLawyerCards(Long tenantId, List<CardProfileDO> cards) {
|
||||||
|
if (cards == null || cards.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<Long, String> roleCodeMap = loadUserRoleCodeMap(
|
||||||
|
tenantId,
|
||||||
|
cards.stream()
|
||||||
|
.map(CardProfileDO::getUserId)
|
||||||
|
.filter(userId -> userId != null && userId > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
return cards.stream()
|
||||||
|
.filter(card -> isLawyerRoleCode(roleCodeMap.get(card.getUserId())))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasLawyerRole(Long tenantId, Long userId) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isLawyerRoleCode(loadUserRoleCodeMap(tenantId, List.of(userId)).get(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, String> loadUserRoleCodeMap(Long tenantId, List<Long> userIds) {
|
||||||
|
if (userIds == null || userIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||||
|
.eq(SysUserRoleDO::getTenantId, tenantId)
|
||||||
|
.in(SysUserRoleDO::getUserId, userIds));
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).distinct().toList();
|
||||||
|
Map<Long, String> roleIdCodeMap = sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.eq(SysRoleDO::getTenantId, tenantId)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.in(SysRoleDO::getId, roleIds))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(SysRoleDO::getId, SysRoleDO::getRoleCode));
|
||||||
|
return userRoles.stream().collect(Collectors.toMap(
|
||||||
|
SysUserRoleDO::getUserId,
|
||||||
|
item -> roleIdCodeMap.getOrDefault(item.getRoleId(), ""),
|
||||||
|
(left, right) -> left
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLawyerRoleCode(String roleCode) {
|
||||||
|
return AUTO_MANAGED_ROLE_CODE.equals(roleCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserDO createHiddenLawyerUser(LoginUser loginUser, UpsertCardRequest request) {
|
||||||
|
SysRoleDO role = getRequiredTenantRole(loginUser.tenantId(), AUTO_MANAGED_ROLE_CODE);
|
||||||
|
SysUserDO user = new SysUserDO();
|
||||||
|
user.setTenantId(loginUser.tenantId());
|
||||||
|
user.setUserType("TENANT");
|
||||||
|
user.setUsername(generateHiddenLawyerUsername(loginUser.tenantId()));
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(UUID.randomUUID().toString()));
|
||||||
|
user.setRealName(request.cardName());
|
||||||
|
user.setNickName(request.cardName());
|
||||||
|
user.setGender("UNKNOWN");
|
||||||
|
user.setMobile(request.mobile());
|
||||||
|
user.setEmail(request.email());
|
||||||
|
user.setDeptId(request.deptId());
|
||||||
|
user.setJobTitle(request.cardTitle());
|
||||||
|
user.setUserStatus("DISABLED");
|
||||||
|
user.setMustUpdatePassword(0);
|
||||||
|
user.setRemark(AUTO_MANAGED_LAWYER_REMARK);
|
||||||
|
user.setCreatedBy(loginUser.userId());
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
user.setDeleted(0);
|
||||||
|
sysUserMapper.insert(user);
|
||||||
|
|
||||||
|
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||||
|
userRole.setTenantId(loginUser.tenantId());
|
||||||
|
userRole.setUserId(user.getId());
|
||||||
|
userRole.setRoleId(role.getId());
|
||||||
|
userRole.setCreatedBy(loginUser.userId());
|
||||||
|
sysUserRoleMapper.insert(userRole);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateHiddenLawyerUsername(Long tenantId) {
|
||||||
|
String base = "lawyer_" + tenantId + "_" + System.currentTimeMillis();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
String candidate = i == 0 ? base : base + "_" + i;
|
||||||
|
Long count = sysUserMapper.selectCount(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, tenantId)
|
||||||
|
.eq(SysUserDO::getUsername, candidate)
|
||||||
|
.eq(SysUserDO::getDeleted, 0));
|
||||||
|
if (count == 0) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysRoleDO getRequiredTenantRole(Long tenantId, String roleCode) {
|
||||||
|
SysRoleDO role = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.eq(SysRoleDO::getTenantId, tenantId)
|
||||||
|
.eq(SysRoleDO::getRoleCode, roleCode)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.eq(SysRoleDO::getRoleStatus, "ENABLED")
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (role == null) {
|
||||||
|
throw new BusinessException("ROLE_NOT_FOUND", "角色不存在");
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer nextCardDisplayOrder(Long tenantId) {
|
||||||
|
CardProfileDO lastCard = cardProfileMapper.selectOne(Wrappers.<CardProfileDO>lambdaQuery()
|
||||||
|
.eq(CardProfileDO::getTenantId, tenantId)
|
||||||
|
.eq(CardProfileDO::getDeleted, 0)
|
||||||
|
.orderByDesc(CardProfileDO::getDisplayOrder)
|
||||||
|
.orderByDesc(CardProfileDO::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
int baseSort = lastCard == null || lastCard.getDisplayOrder() == null ? 0 : lastCard.getDisplayOrder();
|
||||||
|
return baseSort + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyUpsertRequest(CardProfileDO card, UpsertCardRequest request, Long operatorId, boolean allowChangeUser) {
|
||||||
|
if (allowChangeUser && request.userId() != null) {
|
||||||
|
card.setUserId(request.userId());
|
||||||
|
}
|
||||||
|
card.setDeptId(request.deptId());
|
||||||
|
card.setCardName(request.cardName());
|
||||||
|
card.setCardTitle(request.cardTitle());
|
||||||
|
card.setMobile(request.mobile());
|
||||||
|
card.setTelephone(request.telephone());
|
||||||
|
card.setEmail(request.email());
|
||||||
|
card.setOfficeAddress(request.officeAddress());
|
||||||
|
card.setAvatarAssetId(request.avatarAssetId());
|
||||||
|
card.setCoverAssetId(request.coverAssetId());
|
||||||
|
card.setWechatQrAssetId(request.wechatQrAssetId());
|
||||||
|
card.setBio(request.bio());
|
||||||
|
card.setCertificateNo(request.certificateNo());
|
||||||
|
card.setEducationInfo(request.educationInfo());
|
||||||
|
card.setHonorInfo(request.honorInfo());
|
||||||
|
card.setIsPublic(request.isPublic());
|
||||||
|
card.setIsRecommended(request.isRecommended());
|
||||||
|
card.setPublishStatus(request.publishStatus());
|
||||||
|
card.setDisplayOrder(request.displayOrder());
|
||||||
|
if ("PUBLISHED".equals(request.publishStatus())) {
|
||||||
|
card.setLastPublishedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
card.setUpdatedBy(operatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceSpecialties(Long tenantId, Long cardId, List<String> specialties, Long operatorId) {
|
||||||
|
cardProfileSpecialtyMapper.deleteForceByTenantIdAndCardId(tenantId, cardId);
|
||||||
|
List<String> normalizedSpecialties = normalizeSpecialties(specialties);
|
||||||
|
if (normalizedSpecialties.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < normalizedSpecialties.size(); i++) {
|
||||||
|
String specialty = normalizedSpecialties.get(i);
|
||||||
|
CardProfileSpecialtyDO specialtyDO = new CardProfileSpecialtyDO();
|
||||||
|
specialtyDO.setTenantId(tenantId);
|
||||||
|
specialtyDO.setCardId(cardId);
|
||||||
|
specialtyDO.setSpecialtyName(specialty);
|
||||||
|
specialtyDO.setDisplayOrder((i + 1) * 10);
|
||||||
|
specialtyDO.setCreatedBy(operatorId);
|
||||||
|
specialtyDO.setUpdatedBy(operatorId);
|
||||||
|
specialtyDO.setDeleted(0);
|
||||||
|
cardProfileSpecialtyMapper.insert(specialtyDO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeSpecialties(List<String> specialties) {
|
||||||
|
if (specialties == null || specialties.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Set<String> normalized = new LinkedHashSet<>();
|
||||||
|
for (String specialty : specialties) {
|
||||||
|
if (!StringUtils.hasText(specialty)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.add(specialty.trim());
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardSummaryView toSummaryView(CardProfileDO card) {
|
||||||
|
return new CardSummaryView(
|
||||||
|
card.getId(),
|
||||||
|
card.getUserId(),
|
||||||
|
card.getCardName(),
|
||||||
|
card.getCardTitle(),
|
||||||
|
card.getMobile(),
|
||||||
|
card.getEmail(),
|
||||||
|
card.getPublishStatus(),
|
||||||
|
card.getDisplayOrder(),
|
||||||
|
card.getCreatedTime(),
|
||||||
|
card.getUpdatedTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CardDetailView toDetailView(CardProfileDO card) {
|
||||||
|
SysUserDO user = sysUserMapper.selectById(card.getUserId());
|
||||||
|
OrgDepartmentDO department = card.getDeptId() == null ? null : orgDepartmentMapper.selectById(card.getDeptId());
|
||||||
|
return new CardDetailView(
|
||||||
|
card.getId(),
|
||||||
|
card.getUserId(),
|
||||||
|
user == null ? "" : user.getUsername(),
|
||||||
|
card.getDeptId(),
|
||||||
|
department == null ? "" : department.getDeptName(),
|
||||||
|
card.getCardName(),
|
||||||
|
card.getCardTitle(),
|
||||||
|
card.getMobile(),
|
||||||
|
card.getTelephone(),
|
||||||
|
card.getEmail(),
|
||||||
|
card.getOfficeAddress(),
|
||||||
|
card.getAvatarAssetId(),
|
||||||
|
card.getCoverAssetId(),
|
||||||
|
card.getWechatQrAssetId(),
|
||||||
|
resolveAssetUrl(card.getAvatarAssetId()),
|
||||||
|
resolveAssetUrl(card.getCoverAssetId()),
|
||||||
|
resolveAssetUrl(card.getWechatQrAssetId()),
|
||||||
|
card.getBio(),
|
||||||
|
card.getCertificateNo(),
|
||||||
|
card.getEducationInfo(),
|
||||||
|
card.getHonorInfo(),
|
||||||
|
card.getIsPublic(),
|
||||||
|
card.getIsRecommended(),
|
||||||
|
card.getPublishStatus(),
|
||||||
|
card.getDisplayOrder(),
|
||||||
|
loadSpecialtyMap(List.of(card.getId())).getOrDefault(card.getId(), List.of())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, List<String>> loadSpecialtyMap(List<Long> cardIds) {
|
||||||
|
if (cardIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return cardProfileSpecialtyMapper.selectList(Wrappers.<CardProfileSpecialtyDO>lambdaQuery()
|
||||||
|
.in(CardProfileSpecialtyDO::getCardId, cardIds)
|
||||||
|
.eq(CardProfileSpecialtyDO::getDeleted, 0)
|
||||||
|
.orderByAsc(CardProfileSpecialtyDO::getDisplayOrder, CardProfileSpecialtyDO::getId))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
CardProfileSpecialtyDO::getCardId,
|
||||||
|
Collectors.mapping(CardProfileSpecialtyDO::getSpecialtyName, Collectors.toList())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveAssetUrl(Long assetId) {
|
||||||
|
if (assetId == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
FileAssetDO asset = fileAssetMapper.selectById(assetId);
|
||||||
|
return asset == null ? "" : StorageUrlUtils.buildPublicUrl(publicEndpoint, asset.getBucketName(), asset.getObjectKey(), asset.getAccessUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpsertCardRequest(
|
||||||
|
Long userId,
|
||||||
|
Long deptId,
|
||||||
|
String cardName,
|
||||||
|
String cardTitle,
|
||||||
|
String mobile,
|
||||||
|
String telephone,
|
||||||
|
String email,
|
||||||
|
String officeAddress,
|
||||||
|
Long avatarAssetId,
|
||||||
|
Long coverAssetId,
|
||||||
|
Long wechatQrAssetId,
|
||||||
|
String bio,
|
||||||
|
String certificateNo,
|
||||||
|
String educationInfo,
|
||||||
|
String honorInfo,
|
||||||
|
Integer isPublic,
|
||||||
|
Integer isRecommended,
|
||||||
|
String publishStatus,
|
||||||
|
Integer displayOrder,
|
||||||
|
List<String> specialties
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CardSummaryView(
|
||||||
|
Long id,
|
||||||
|
Long userId,
|
||||||
|
String cardName,
|
||||||
|
String cardTitle,
|
||||||
|
String mobile,
|
||||||
|
String email,
|
||||||
|
String publishStatus,
|
||||||
|
Integer displayOrder,
|
||||||
|
LocalDateTime createdTime,
|
||||||
|
LocalDateTime updatedTime
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CardDetailView(
|
||||||
|
Long id,
|
||||||
|
Long userId,
|
||||||
|
String username,
|
||||||
|
Long deptId,
|
||||||
|
String deptName,
|
||||||
|
String cardName,
|
||||||
|
String cardTitle,
|
||||||
|
String mobile,
|
||||||
|
String telephone,
|
||||||
|
String email,
|
||||||
|
String officeAddress,
|
||||||
|
Long avatarAssetId,
|
||||||
|
Long coverAssetId,
|
||||||
|
Long wechatQrAssetId,
|
||||||
|
String avatarUrl,
|
||||||
|
String coverUrl,
|
||||||
|
String wechatQrUrl,
|
||||||
|
String bio,
|
||||||
|
String certificateNo,
|
||||||
|
String educationInfo,
|
||||||
|
String honorInfo,
|
||||||
|
Integer isPublic,
|
||||||
|
Integer isRecommended,
|
||||||
|
String publishStatus,
|
||||||
|
Integer displayOrder,
|
||||||
|
List<String> specialties
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DashboardStatsView(Integer totalCards, Long publishedCards, Long totalViews, Long totalShares, Long todayViews) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CardTrendView(String statDate, Long viewCount, Long shareCount) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenFirmView(
|
||||||
|
String name,
|
||||||
|
String logo,
|
||||||
|
String heroImage,
|
||||||
|
String intro,
|
||||||
|
String hotlinePhone,
|
||||||
|
String hqAddress,
|
||||||
|
Double hqLatitude,
|
||||||
|
Double hqLongitude,
|
||||||
|
Long officeCount,
|
||||||
|
Long lawyerCount,
|
||||||
|
List<String> officeList,
|
||||||
|
List<String> practiceAreas
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenCardListItem(
|
||||||
|
Long id,
|
||||||
|
String name,
|
||||||
|
String title,
|
||||||
|
String office,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String avatar,
|
||||||
|
List<String> specialties
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OpenCardDetailView(
|
||||||
|
Long id,
|
||||||
|
String name,
|
||||||
|
String title,
|
||||||
|
String office,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String address,
|
||||||
|
String avatar,
|
||||||
|
String coverImage,
|
||||||
|
String wechatQrImage,
|
||||||
|
String bio,
|
||||||
|
List<String> specialties,
|
||||||
|
String firmName,
|
||||||
|
String firmAddress,
|
||||||
|
Double firmLatitude,
|
||||||
|
Double firmLongitude
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
40
backend/easycard-module-file/pom.xml
Normal file
40
backend/easycard-module-file/pom.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-file</artifactId>
|
||||||
|
<name>easycard-module-file</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-user</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.5.17</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.easycard.module.file.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.file.service.FileAssetService;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/files")
|
||||||
|
public class FileAssetController {
|
||||||
|
|
||||||
|
private final FileAssetService fileAssetService;
|
||||||
|
|
||||||
|
public FileAssetController(FileAssetService fileAssetService) {
|
||||||
|
this.fileAssetService = fileAssetService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<FileAssetService.FileAssetView> upload(@RequestParam("file") MultipartFile file) {
|
||||||
|
return ApiResponse.success(fileAssetService.upload(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<FileAssetService.FileAssetView>> list() {
|
||||||
|
return ApiResponse.success(fileAssetService.listAssets());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.easycard.module.file.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("file_asset")
|
||||||
|
public class FileAssetDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long uploadUserId;
|
||||||
|
private String storageProvider;
|
||||||
|
private String bucketName;
|
||||||
|
private String objectKey;
|
||||||
|
private String originalName;
|
||||||
|
private String fileExt;
|
||||||
|
private String mimeType;
|
||||||
|
private Long fileSize;
|
||||||
|
private String fileHash;
|
||||||
|
private String accessUrl;
|
||||||
|
private String assetStatus;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.easycard.module.file.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("file_asset_usage")
|
||||||
|
public class FileAssetUsageDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long assetId;
|
||||||
|
private String bizType;
|
||||||
|
private Long bizId;
|
||||||
|
private String fieldName;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.file.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||||
|
|
||||||
|
public interface FileAssetMapper extends BaseMapper<FileAssetDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.file.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetUsageDO;
|
||||||
|
|
||||||
|
public interface FileAssetUsageMapper extends BaseMapper<FileAssetUsageDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package com.easycard.module.file.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.common.storage.StorageUrlUtils;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||||
|
import com.easycard.module.file.dal.mapper.FileAssetMapper;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.PutObjectArgs;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileAssetService {
|
||||||
|
|
||||||
|
private final FileAssetMapper fileAssetMapper;
|
||||||
|
|
||||||
|
@Value("${easycard.storage.endpoint}")
|
||||||
|
private String endpoint;
|
||||||
|
|
||||||
|
@Value("${easycard.storage.public-endpoint:${easycard.storage.endpoint}}")
|
||||||
|
private String publicEndpoint;
|
||||||
|
|
||||||
|
@Value("${easycard.storage.access-key}")
|
||||||
|
private String accessKey;
|
||||||
|
|
||||||
|
@Value("${easycard.storage.secret-key}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
@Value("${easycard.storage.bucket}")
|
||||||
|
private String bucket;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public FileAssetView upload(MultipartFile file) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new BusinessException("FILE_EMPTY", "上传文件不能为空");
|
||||||
|
}
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024L) {
|
||||||
|
throw new BusinessException("FILE_TOO_LARGE", "图片大小不能超过 5MB");
|
||||||
|
}
|
||||||
|
String contentType = file.getContentType() == null ? "" : file.getContentType().toLowerCase(Locale.ROOT);
|
||||||
|
if (!contentType.startsWith("image/")) {
|
||||||
|
throw new BusinessException("FILE_TYPE_INVALID", "仅支持图片上传");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = resolveExtension(file.getOriginalFilename());
|
||||||
|
String objectKey = loginUser.tenantId() + "/" + UUID.randomUUID() + extension;
|
||||||
|
try (InputStream inputStream = file.getInputStream()) {
|
||||||
|
MinioClient minioClient = MinioClient.builder()
|
||||||
|
.endpoint(endpoint)
|
||||||
|
.credentials(accessKey, secretKey)
|
||||||
|
.build();
|
||||||
|
minioClient.putObject(PutObjectArgs.builder()
|
||||||
|
.bucket(bucket)
|
||||||
|
.object(objectKey)
|
||||||
|
.stream(inputStream, file.getSize(), -1)
|
||||||
|
.contentType(file.getContentType())
|
||||||
|
.build());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new BusinessException("FILE_UPLOAD_FAILED", "上传文件失败: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
FileAssetDO asset = new FileAssetDO();
|
||||||
|
asset.setTenantId(loginUser.tenantId());
|
||||||
|
asset.setUploadUserId(loginUser.userId());
|
||||||
|
asset.setStorageProvider("MINIO");
|
||||||
|
asset.setBucketName(bucket);
|
||||||
|
asset.setObjectKey(objectKey);
|
||||||
|
asset.setOriginalName(file.getOriginalFilename());
|
||||||
|
asset.setFileExt(extension);
|
||||||
|
asset.setMimeType(file.getContentType());
|
||||||
|
asset.setFileSize(file.getSize());
|
||||||
|
asset.setAccessUrl(StorageUrlUtils.buildPublicUrl(publicEndpoint, bucket, objectKey, ""));
|
||||||
|
asset.setAssetStatus("ACTIVE");
|
||||||
|
asset.setCreatedBy(loginUser.userId());
|
||||||
|
asset.setUpdatedBy(loginUser.userId());
|
||||||
|
asset.setDeleted(0);
|
||||||
|
fileAssetMapper.insert(asset);
|
||||||
|
return toView(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileAssetView> listAssets() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
return fileAssetMapper.selectList(Wrappers.<FileAssetDO>lambdaQuery()
|
||||||
|
.eq(FileAssetDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(FileAssetDO::getDeleted, 0)
|
||||||
|
.orderByDesc(FileAssetDO::getId))
|
||||||
|
.stream()
|
||||||
|
.map(this::toView)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileAssetView getAsset(Long assetId) {
|
||||||
|
FileAssetDO asset = fileAssetMapper.selectById(assetId);
|
||||||
|
if (asset == null || Integer.valueOf(1).equals(asset.getDeleted())) {
|
||||||
|
throw new BusinessException("ASSET_NOT_FOUND", "素材不存在");
|
||||||
|
}
|
||||||
|
return toView(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileAssetView toView(FileAssetDO asset) {
|
||||||
|
return new FileAssetView(
|
||||||
|
asset.getId(),
|
||||||
|
asset.getOriginalName(),
|
||||||
|
asset.getMimeType(),
|
||||||
|
asset.getFileSize(),
|
||||||
|
StorageUrlUtils.buildPublicUrl(publicEndpoint, asset.getBucketName(), asset.getObjectKey(), asset.getAccessUrl()),
|
||||||
|
asset.getCreatedTime() == null ? LocalDateTime.now() : asset.getCreatedTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveExtension(String originalName) {
|
||||||
|
if (originalName == null || !originalName.contains(".")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return originalName.substring(originalName.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileAssetView(Long id, String originalName, String mimeType, Long fileSize, String accessUrl, LocalDateTime createdTime) {
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/easycard-module-org/pom.xml
Normal file
30
backend/easycard-module-org/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-org</artifactId>
|
||||||
|
<name>easycard-module-org</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.easycard.module.org.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.org.service.TenantOrgService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/tenant")
|
||||||
|
public class TenantOrgController {
|
||||||
|
|
||||||
|
private final TenantOrgService tenantOrgService;
|
||||||
|
|
||||||
|
public TenantOrgController(TenantOrgService tenantOrgService) {
|
||||||
|
this.tenantOrgService = tenantOrgService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/firm-profile")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.FirmProfileView> getFirmProfile() {
|
||||||
|
return ApiResponse.success(tenantOrgService.getFirmProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/firm-profile")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.FirmProfileView> saveFirmProfile(@Valid @RequestBody UpsertFirmProfileCommand request) {
|
||||||
|
return ApiResponse.success(tenantOrgService.saveFirmProfile(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/practice-areas")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<TenantOrgService.PracticeAreaView>> listPracticeAreas() {
|
||||||
|
return ApiResponse.success(tenantOrgService.listPracticeAreas());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/practice-areas")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.PracticeAreaView> createPracticeArea(@Valid @RequestBody UpsertPracticeAreaCommand request) {
|
||||||
|
return ApiResponse.success(tenantOrgService.createPracticeArea(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/practice-areas/{areaId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.PracticeAreaView> updatePracticeArea(@PathVariable Long areaId, @Valid @RequestBody UpsertPracticeAreaCommand request) {
|
||||||
|
return ApiResponse.success(tenantOrgService.updatePracticeArea(areaId, request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/departments")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<TenantOrgService.DepartmentView>> listDepartments() {
|
||||||
|
return ApiResponse.success(tenantOrgService.listDepartments());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/departments")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.DepartmentView> createDepartment(@Valid @RequestBody UpsertDepartmentCommand request) {
|
||||||
|
return ApiResponse.success(tenantOrgService.createDepartment(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/departments/{deptId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantOrgService.DepartmentView> updateDepartment(@PathVariable Long deptId, @Valid @RequestBody UpsertDepartmentCommand request) {
|
||||||
|
return ApiResponse.success(tenantOrgService.updateDepartment(deptId, request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpsertFirmProfileCommand(
|
||||||
|
@NotBlank(message = "事务所名称不能为空") String firmName,
|
||||||
|
String firmShortName,
|
||||||
|
String englishName,
|
||||||
|
Long logoAssetId,
|
||||||
|
Long heroAssetId,
|
||||||
|
String intro,
|
||||||
|
String hotlinePhone,
|
||||||
|
String websiteUrl,
|
||||||
|
String wechatOfficialAccount,
|
||||||
|
String hqAddress,
|
||||||
|
String hqLatitude,
|
||||||
|
String hqLongitude
|
||||||
|
) {
|
||||||
|
TenantOrgService.UpsertFirmProfileRequest toServiceRequest() {
|
||||||
|
return new TenantOrgService.UpsertFirmProfileRequest(
|
||||||
|
firmName, firmShortName, englishName, logoAssetId, heroAssetId, intro, hotlinePhone,
|
||||||
|
websiteUrl, wechatOfficialAccount, hqAddress, hqLatitude, hqLongitude
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpsertPracticeAreaCommand(
|
||||||
|
@NotBlank(message = "专业编码不能为空") String areaCode,
|
||||||
|
@NotBlank(message = "专业名称不能为空") String areaName,
|
||||||
|
Integer displayOrder,
|
||||||
|
@NotBlank(message = "状态不能为空") String areaStatus
|
||||||
|
) {
|
||||||
|
TenantOrgService.UpsertPracticeAreaRequest toServiceRequest() {
|
||||||
|
return new TenantOrgService.UpsertPracticeAreaRequest(areaCode, areaName, displayOrder == null ? 0 : displayOrder, areaStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpsertDepartmentCommand(
|
||||||
|
Long parentId,
|
||||||
|
@NotBlank(message = "组织编码不能为空") String deptCode,
|
||||||
|
@NotBlank(message = "组织名称不能为空") String deptName,
|
||||||
|
@NotBlank(message = "组织类型不能为空") String deptType,
|
||||||
|
String contactPhone,
|
||||||
|
String address,
|
||||||
|
Integer displayOrder,
|
||||||
|
@NotBlank(message = "状态不能为空") String deptStatus
|
||||||
|
) {
|
||||||
|
TenantOrgService.UpsertDepartmentRequest toServiceRequest() {
|
||||||
|
return new TenantOrgService.UpsertDepartmentRequest(parentId, deptCode, deptName, deptType, contactPhone, address, displayOrder == null ? 0 : displayOrder, deptStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.easycard.module.org.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("file_asset")
|
||||||
|
public class FileAssetLiteDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private String bucketName;
|
||||||
|
private String objectKey;
|
||||||
|
private String accessUrl;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.easycard.module.org.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("org_department")
|
||||||
|
public class OrgDepartmentDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long parentId;
|
||||||
|
private String deptCode;
|
||||||
|
private String deptName;
|
||||||
|
private String deptType;
|
||||||
|
private Long leaderUserId;
|
||||||
|
private String contactPhone;
|
||||||
|
private String address;
|
||||||
|
private Integer displayOrder;
|
||||||
|
private String deptStatus;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.easycard.module.org.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("org_firm_practice_area")
|
||||||
|
public class OrgFirmPracticeAreaDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String areaCode;
|
||||||
|
private String areaName;
|
||||||
|
private Integer displayOrder;
|
||||||
|
private String areaStatus;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.easycard.module.org.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("org_firm_profile")
|
||||||
|
public class OrgFirmProfileDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String firmName;
|
||||||
|
private String firmShortName;
|
||||||
|
private String englishName;
|
||||||
|
private Long logoAssetId;
|
||||||
|
private Long heroAssetId;
|
||||||
|
private String intro;
|
||||||
|
private String hotlinePhone;
|
||||||
|
private String websiteUrl;
|
||||||
|
private String wechatOfficialAccount;
|
||||||
|
private String hqAddress;
|
||||||
|
private BigDecimal hqLatitude;
|
||||||
|
private BigDecimal hqLongitude;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.org.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.FileAssetLiteDO;
|
||||||
|
|
||||||
|
public interface FileAssetLiteMapper extends BaseMapper<FileAssetLiteDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.org.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
|
||||||
|
public interface OrgDepartmentMapper extends BaseMapper<OrgDepartmentDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.org.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmPracticeAreaDO;
|
||||||
|
|
||||||
|
public interface OrgFirmPracticeAreaMapper extends BaseMapper<OrgFirmPracticeAreaDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.org.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmProfileDO;
|
||||||
|
|
||||||
|
public interface OrgFirmProfileMapper extends BaseMapper<OrgFirmProfileDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
package com.easycard.module.org.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.common.storage.StorageUrlUtils;
|
||||||
|
import com.easycard.module.org.dal.entity.FileAssetLiteDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.FileAssetLiteMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmPracticeAreaDO;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmProfileDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgFirmPracticeAreaMapper;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgFirmProfileMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TenantOrgService {
|
||||||
|
|
||||||
|
@Value("${easycard.storage.public-endpoint:${easycard.storage.endpoint}}")
|
||||||
|
private String publicEndpoint;
|
||||||
|
|
||||||
|
private final OrgFirmProfileMapper orgFirmProfileMapper;
|
||||||
|
private final OrgFirmPracticeAreaMapper orgFirmPracticeAreaMapper;
|
||||||
|
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||||
|
private final FileAssetLiteMapper fileAssetLiteMapper;
|
||||||
|
|
||||||
|
public FirmProfileView getFirmProfile() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (profile == null) {
|
||||||
|
return new FirmProfileView(null, "", "", "", null, "", null, "", "", "", "", "", "", null, null);
|
||||||
|
}
|
||||||
|
return toView(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public FirmProfileView saveFirmProfile(UpsertFirmProfileRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgFirmProfileDO profile = orgFirmProfileMapper.selectOne(Wrappers.<OrgFirmProfileDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmProfileDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgFirmProfileDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (profile == null) {
|
||||||
|
profile = new OrgFirmProfileDO();
|
||||||
|
profile.setTenantId(loginUser.tenantId());
|
||||||
|
profile.setCreatedBy(loginUser.userId());
|
||||||
|
profile.setDeleted(0);
|
||||||
|
}
|
||||||
|
profile.setFirmName(request.firmName());
|
||||||
|
profile.setFirmShortName(request.firmShortName());
|
||||||
|
profile.setEnglishName(request.englishName());
|
||||||
|
profile.setLogoAssetId(request.logoAssetId());
|
||||||
|
profile.setHeroAssetId(request.heroAssetId());
|
||||||
|
profile.setIntro(request.intro());
|
||||||
|
profile.setHotlinePhone(request.hotlinePhone());
|
||||||
|
profile.setWebsiteUrl(request.websiteUrl());
|
||||||
|
profile.setWechatOfficialAccount(request.wechatOfficialAccount());
|
||||||
|
profile.setHqAddress(request.hqAddress());
|
||||||
|
profile.setHqLatitude(toBigDecimal(request.hqLatitude()));
|
||||||
|
profile.setHqLongitude(toBigDecimal(request.hqLongitude()));
|
||||||
|
profile.setUpdatedBy(loginUser.userId());
|
||||||
|
if (profile.getId() == null) {
|
||||||
|
orgFirmProfileMapper.insert(profile);
|
||||||
|
} else {
|
||||||
|
orgFirmProfileMapper.updateById(profile);
|
||||||
|
}
|
||||||
|
return toView(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PracticeAreaView> listPracticeAreas() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
return orgFirmPracticeAreaMapper.selectList(Wrappers.<OrgFirmPracticeAreaDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmPracticeAreaDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgFirmPracticeAreaDO::getDeleted, 0)
|
||||||
|
.orderByAsc(OrgFirmPracticeAreaDO::getDisplayOrder, OrgFirmPracticeAreaDO::getId))
|
||||||
|
.stream()
|
||||||
|
.map(item -> new PracticeAreaView(item.getId(), item.getAreaCode(), item.getAreaName(), item.getDisplayOrder(), item.getAreaStatus()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PracticeAreaView createPracticeArea(UpsertPracticeAreaRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgFirmPracticeAreaDO existed = orgFirmPracticeAreaMapper.selectOne(Wrappers.<OrgFirmPracticeAreaDO>lambdaQuery()
|
||||||
|
.eq(OrgFirmPracticeAreaDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgFirmPracticeAreaDO::getAreaCode, request.areaCode())
|
||||||
|
.eq(OrgFirmPracticeAreaDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existed != null) {
|
||||||
|
throw new BusinessException("AREA_CODE_DUPLICATED", "专业领域编码已存在");
|
||||||
|
}
|
||||||
|
OrgFirmPracticeAreaDO area = new OrgFirmPracticeAreaDO();
|
||||||
|
area.setTenantId(loginUser.tenantId());
|
||||||
|
area.setAreaCode(request.areaCode());
|
||||||
|
area.setAreaName(request.areaName());
|
||||||
|
area.setDisplayOrder(request.displayOrder());
|
||||||
|
area.setAreaStatus(request.areaStatus());
|
||||||
|
area.setCreatedBy(loginUser.userId());
|
||||||
|
area.setUpdatedBy(loginUser.userId());
|
||||||
|
area.setDeleted(0);
|
||||||
|
orgFirmPracticeAreaMapper.insert(area);
|
||||||
|
return new PracticeAreaView(area.getId(), area.getAreaCode(), area.getAreaName(), area.getDisplayOrder(), area.getAreaStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PracticeAreaView updatePracticeArea(Long areaId, UpsertPracticeAreaRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgFirmPracticeAreaDO area = getPracticeArea(areaId, loginUser.tenantId());
|
||||||
|
area.setAreaName(request.areaName());
|
||||||
|
area.setDisplayOrder(request.displayOrder());
|
||||||
|
area.setAreaStatus(request.areaStatus());
|
||||||
|
area.setUpdatedBy(loginUser.userId());
|
||||||
|
orgFirmPracticeAreaMapper.updateById(area);
|
||||||
|
return new PracticeAreaView(area.getId(), area.getAreaCode(), area.getAreaName(), area.getDisplayOrder(), area.getAreaStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DepartmentView> listDepartments() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
return orgDepartmentMapper.selectList(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||||
|
.eq(OrgDepartmentDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgDepartmentDO::getDeleted, 0)
|
||||||
|
.orderByAsc(OrgDepartmentDO::getDisplayOrder, OrgDepartmentDO::getId))
|
||||||
|
.stream()
|
||||||
|
.map(item -> new DepartmentView(item.getId(), item.getParentId(), item.getDeptCode(), item.getDeptName(), item.getDeptType(), item.getContactPhone(), item.getAddress(), item.getDisplayOrder(), item.getDeptStatus()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DepartmentView createDepartment(UpsertDepartmentRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgDepartmentDO existed = orgDepartmentMapper.selectOne(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||||
|
.eq(OrgDepartmentDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(OrgDepartmentDO::getDeptCode, request.deptCode())
|
||||||
|
.eq(OrgDepartmentDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existed != null) {
|
||||||
|
throw new BusinessException("DEPT_CODE_DUPLICATED", "组织编码已存在");
|
||||||
|
}
|
||||||
|
OrgDepartmentDO department = new OrgDepartmentDO();
|
||||||
|
department.setTenantId(loginUser.tenantId());
|
||||||
|
department.setParentId(request.parentId() == null ? 0L : request.parentId());
|
||||||
|
department.setDeptCode(request.deptCode());
|
||||||
|
department.setDeptName(request.deptName());
|
||||||
|
department.setDeptType(request.deptType());
|
||||||
|
department.setContactPhone(request.contactPhone());
|
||||||
|
department.setAddress(request.address());
|
||||||
|
department.setDisplayOrder(request.displayOrder());
|
||||||
|
department.setDeptStatus(request.deptStatus());
|
||||||
|
department.setCreatedBy(loginUser.userId());
|
||||||
|
department.setUpdatedBy(loginUser.userId());
|
||||||
|
department.setDeleted(0);
|
||||||
|
orgDepartmentMapper.insert(department);
|
||||||
|
return new DepartmentView(department.getId(), department.getParentId(), department.getDeptCode(), department.getDeptName(), department.getDeptType(), department.getContactPhone(), department.getAddress(), department.getDisplayOrder(), department.getDeptStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public DepartmentView updateDepartment(Long deptId, UpsertDepartmentRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
OrgDepartmentDO department = getDepartment(deptId, loginUser.tenantId());
|
||||||
|
department.setParentId(request.parentId() == null ? 0L : request.parentId());
|
||||||
|
department.setDeptName(request.deptName());
|
||||||
|
department.setDeptType(request.deptType());
|
||||||
|
department.setContactPhone(request.contactPhone());
|
||||||
|
department.setAddress(request.address());
|
||||||
|
department.setDisplayOrder(request.displayOrder());
|
||||||
|
department.setDeptStatus(request.deptStatus());
|
||||||
|
department.setUpdatedBy(loginUser.userId());
|
||||||
|
orgDepartmentMapper.updateById(department);
|
||||||
|
return new DepartmentView(department.getId(), department.getParentId(), department.getDeptCode(), department.getDeptName(), department.getDeptType(), department.getContactPhone(), department.getAddress(), department.getDisplayOrder(), department.getDeptStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrgDepartmentDO getDepartment(Long deptId, Long tenantId) {
|
||||||
|
OrgDepartmentDO department = orgDepartmentMapper.selectById(deptId);
|
||||||
|
if (department == null || Integer.valueOf(1).equals(department.getDeleted()) || !tenantId.equals(department.getTenantId())) {
|
||||||
|
throw new BusinessException("DEPARTMENT_NOT_FOUND", "组织不存在");
|
||||||
|
}
|
||||||
|
return department;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrgFirmPracticeAreaDO getPracticeArea(Long areaId, Long tenantId) {
|
||||||
|
OrgFirmPracticeAreaDO area = orgFirmPracticeAreaMapper.selectById(areaId);
|
||||||
|
if (area == null || Integer.valueOf(1).equals(area.getDeleted()) || !tenantId.equals(area.getTenantId())) {
|
||||||
|
throw new BusinessException("PRACTICE_AREA_NOT_FOUND", "专业领域不存在");
|
||||||
|
}
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FirmProfileView toView(OrgFirmProfileDO profile) {
|
||||||
|
return new FirmProfileView(
|
||||||
|
profile.getId(),
|
||||||
|
profile.getFirmName(),
|
||||||
|
profile.getFirmShortName(),
|
||||||
|
profile.getEnglishName(),
|
||||||
|
profile.getLogoAssetId(),
|
||||||
|
resolveAssetUrl(profile.getLogoAssetId()),
|
||||||
|
profile.getHeroAssetId(),
|
||||||
|
resolveAssetUrl(profile.getHeroAssetId()),
|
||||||
|
profile.getIntro(),
|
||||||
|
profile.getHotlinePhone(),
|
||||||
|
profile.getWebsiteUrl(),
|
||||||
|
profile.getWechatOfficialAccount(),
|
||||||
|
profile.getHqAddress(),
|
||||||
|
profile.getHqLatitude(),
|
||||||
|
profile.getHqLongitude()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal toBigDecimal(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new BigDecimal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveAssetUrl(Long assetId) {
|
||||||
|
if (assetId == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
FileAssetLiteDO asset = fileAssetLiteMapper.selectById(assetId);
|
||||||
|
if (asset == null || Integer.valueOf(1).equals(asset.getDeleted())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return StorageUrlUtils.buildPublicUrl(publicEndpoint, asset.getBucketName(), asset.getObjectKey(), asset.getAccessUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FirmProfileView(
|
||||||
|
Long id,
|
||||||
|
String firmName,
|
||||||
|
String firmShortName,
|
||||||
|
String englishName,
|
||||||
|
Long logoAssetId,
|
||||||
|
String logoUrl,
|
||||||
|
Long heroAssetId,
|
||||||
|
String heroUrl,
|
||||||
|
String intro,
|
||||||
|
String hotlinePhone,
|
||||||
|
String websiteUrl,
|
||||||
|
String wechatOfficialAccount,
|
||||||
|
String hqAddress,
|
||||||
|
BigDecimal hqLatitude,
|
||||||
|
BigDecimal hqLongitude
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpsertFirmProfileRequest(
|
||||||
|
String firmName,
|
||||||
|
String firmShortName,
|
||||||
|
String englishName,
|
||||||
|
Long logoAssetId,
|
||||||
|
Long heroAssetId,
|
||||||
|
String intro,
|
||||||
|
String hotlinePhone,
|
||||||
|
String websiteUrl,
|
||||||
|
String wechatOfficialAccount,
|
||||||
|
String hqAddress,
|
||||||
|
String hqLatitude,
|
||||||
|
String hqLongitude
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PracticeAreaView(Long id, String areaCode, String areaName, Integer displayOrder, String areaStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpsertPracticeAreaRequest(String areaCode, String areaName, Integer displayOrder, String areaStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DepartmentView(
|
||||||
|
Long id,
|
||||||
|
Long parentId,
|
||||||
|
String deptCode,
|
||||||
|
String deptName,
|
||||||
|
String deptType,
|
||||||
|
String contactPhone,
|
||||||
|
String address,
|
||||||
|
Integer displayOrder,
|
||||||
|
String deptStatus
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpsertDepartmentRequest(
|
||||||
|
Long parentId,
|
||||||
|
String deptCode,
|
||||||
|
String deptName,
|
||||||
|
String deptType,
|
||||||
|
String contactPhone,
|
||||||
|
String address,
|
||||||
|
Integer displayOrder,
|
||||||
|
String deptStatus
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/easycard-module-stat/pom.xml
Normal file
30
backend/easycard-module-stat/pom.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-stat</artifactId>
|
||||||
|
<name>easycard-module-stat</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.easycard.module.stat.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("card_share_log")
|
||||||
|
public class CardShareLogDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String miniappAppId;
|
||||||
|
private Long cardId;
|
||||||
|
private String shareChannel;
|
||||||
|
private String sharePath;
|
||||||
|
private String shareByOpenId;
|
||||||
|
private LocalDateTime sharedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.easycard.module.stat.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("card_stat_daily")
|
||||||
|
public class CardStatDailyDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long cardId;
|
||||||
|
private LocalDate statDate;
|
||||||
|
private Long viewCount;
|
||||||
|
private Long shareCount;
|
||||||
|
private Long uniqueVisitorCount;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.easycard.module.stat.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("card_view_log")
|
||||||
|
public class CardViewLogDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String miniappAppId;
|
||||||
|
private Long cardId;
|
||||||
|
private String viewerOpenId;
|
||||||
|
private String viewerIp;
|
||||||
|
private String sourceType;
|
||||||
|
private Long shareFromCardId;
|
||||||
|
private String pagePath;
|
||||||
|
private LocalDateTime viewedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.stat.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardShareLogDO;
|
||||||
|
|
||||||
|
public interface CardShareLogMapper extends BaseMapper<CardShareLogDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.stat.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardStatDailyDO;
|
||||||
|
|
||||||
|
public interface CardStatDailyMapper extends BaseMapper<CardStatDailyDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.stat.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardViewLogDO;
|
||||||
|
|
||||||
|
public interface CardViewLogMapper extends BaseMapper<CardViewLogDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.easycard.module.stat.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardShareLogDO;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardStatDailyDO;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardViewLogDO;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardShareLogMapper;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardStatDailyMapper;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardViewLogMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CardEventService {
|
||||||
|
|
||||||
|
private final CardViewLogMapper cardViewLogMapper;
|
||||||
|
private final CardShareLogMapper cardShareLogMapper;
|
||||||
|
private final CardStatDailyMapper cardStatDailyMapper;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void recordView(Long tenantId, String appId, Long cardId, String sourceType, Long shareFromCardId, String viewerIp, String pagePath) {
|
||||||
|
CardViewLogDO viewLog = new CardViewLogDO();
|
||||||
|
viewLog.setTenantId(tenantId);
|
||||||
|
viewLog.setMiniappAppId(appId);
|
||||||
|
viewLog.setCardId(cardId);
|
||||||
|
viewLog.setSourceType(sourceType);
|
||||||
|
viewLog.setShareFromCardId(shareFromCardId);
|
||||||
|
viewLog.setViewerIp(viewerIp);
|
||||||
|
viewLog.setPagePath(pagePath);
|
||||||
|
viewLog.setViewedAt(LocalDateTime.now());
|
||||||
|
cardViewLogMapper.insert(viewLog);
|
||||||
|
updateDailyStats(tenantId, cardId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void recordShare(Long tenantId, String appId, Long cardId, String shareChannel, String sharePath) {
|
||||||
|
CardShareLogDO shareLog = new CardShareLogDO();
|
||||||
|
shareLog.setTenantId(tenantId);
|
||||||
|
shareLog.setMiniappAppId(appId);
|
||||||
|
shareLog.setCardId(cardId);
|
||||||
|
shareLog.setShareChannel(shareChannel);
|
||||||
|
shareLog.setSharePath(sharePath);
|
||||||
|
shareLog.setSharedAt(LocalDateTime.now());
|
||||||
|
cardShareLogMapper.insert(shareLog);
|
||||||
|
updateDailyStats(tenantId, cardId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDailyStats(Long tenantId, Long cardId, boolean viewIncrement) {
|
||||||
|
LocalDate statDate = LocalDate.now();
|
||||||
|
CardStatDailyDO statDaily = cardStatDailyMapper.selectOne(Wrappers.<CardStatDailyDO>lambdaQuery()
|
||||||
|
.eq(CardStatDailyDO::getTenantId, tenantId)
|
||||||
|
.eq(CardStatDailyDO::getCardId, cardId)
|
||||||
|
.eq(CardStatDailyDO::getStatDate, statDate)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (statDaily == null) {
|
||||||
|
statDaily = new CardStatDailyDO();
|
||||||
|
statDaily.setTenantId(tenantId);
|
||||||
|
statDaily.setCardId(cardId);
|
||||||
|
statDaily.setStatDate(statDate);
|
||||||
|
statDaily.setViewCount(0L);
|
||||||
|
statDaily.setShareCount(0L);
|
||||||
|
statDaily.setUniqueVisitorCount(0L);
|
||||||
|
cardStatDailyMapper.insert(statDaily);
|
||||||
|
}
|
||||||
|
if (viewIncrement) {
|
||||||
|
statDaily.setViewCount(statDaily.getViewCount() + 1);
|
||||||
|
} else {
|
||||||
|
statDaily.setShareCount(statDaily.getShareCount() + 1);
|
||||||
|
}
|
||||||
|
cardStatDailyMapper.updateById(statDaily);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/easycard-module-system/pom.xml
Normal file
22
backend/easycard-module-system/pom.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-system</artifactId>
|
||||||
|
<name>easycard-module-system</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.easycard.module.system.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.tenant.TenantContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class SystemPingController {
|
||||||
|
|
||||||
|
@GetMapping("/api/v1/system/ping")
|
||||||
|
public ApiResponse<Map<String, Object>> ping() {
|
||||||
|
return ApiResponse.success(Map.of(
|
||||||
|
"service", "easycard-backend",
|
||||||
|
"status", "UP"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/open/ping")
|
||||||
|
public ApiResponse<Map<String, Object>> openPing() {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("service", "easycard-open-api");
|
||||||
|
payload.put("tenant", TenantContextHolder.getOptional().orElse(null));
|
||||||
|
return ApiResponse.success(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
backend/easycard-module-tenant/pom.xml
Normal file
50
backend/easycard-module-tenant/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-tenant</artifactId>
|
||||||
|
<name>easycard-module-tenant</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-user</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-card</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-file</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-stat</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.easycard.module.tenant.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.tenant.service.PlatformTenantService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/platform/miniapps")
|
||||||
|
public class PlatformMiniappController {
|
||||||
|
|
||||||
|
private final PlatformTenantService platformTenantService;
|
||||||
|
|
||||||
|
public PlatformMiniappController(PlatformTenantService platformTenantService) {
|
||||||
|
this.platformTenantService = platformTenantService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<PlatformTenantService.MiniappConfigView>> listMiniapps() {
|
||||||
|
return ApiResponse.success(platformTenantService.listMiniappConfigs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{tenantId}")
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<PlatformTenantService.MiniappConfigView> updateMiniapp(@PathVariable Long tenantId, @Valid @RequestBody MiniappConfigRequest request) {
|
||||||
|
return ApiResponse.success(platformTenantService.updateMiniappConfig(tenantId, request.toCommand()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.easycard.module.tenant.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.module.tenant.service.PlatformTenantService;
|
||||||
|
import com.easycard.module.tenant.vo.TenantDetailResp;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/platform/tenants")
|
||||||
|
public class PlatformTenantController {
|
||||||
|
|
||||||
|
private final PlatformTenantService platformTenantService;
|
||||||
|
|
||||||
|
public PlatformTenantController(PlatformTenantService platformTenantService) {
|
||||||
|
this.platformTenantService = platformTenantService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<TenantDetailResp>> listTenants() {
|
||||||
|
return ApiResponse.success(platformTenantService.listTenants());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{tenantId}")
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantDetailResp> getTenant(@PathVariable Long tenantId) {
|
||||||
|
TenantDetailResp tenant = platformTenantService.getTenantById(tenantId)
|
||||||
|
.orElseThrow(() -> new BusinessException("TENANT_NOT_FOUND", "租户不存在"));
|
||||||
|
return ApiResponse.success(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantDetailResp> createTenant(@Valid @RequestBody CreateTenantRequest request) {
|
||||||
|
return ApiResponse.success(platformTenantService.createTenant(request.toCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{tenantId}")
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantDetailResp> updateTenant(@PathVariable Long tenantId, @Valid @RequestBody UpdateTenantRequest request) {
|
||||||
|
return ApiResponse.success(platformTenantService.updateTenant(tenantId, request.toCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{tenantId}")
|
||||||
|
@PreAuthorize("hasAuthority('PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<Void> deleteTenant(@PathVariable Long tenantId) {
|
||||||
|
platformTenantService.deleteTenant(tenantId);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CreateTenantRequest(
|
||||||
|
@NotBlank(message = "租户名称不能为空") String tenantName,
|
||||||
|
@NotBlank(message = "管理员账号不能为空") String adminUsername,
|
||||||
|
@NotBlank(message = "管理员密码不能为空") String adminPassword,
|
||||||
|
String miniappAppId
|
||||||
|
) {
|
||||||
|
PlatformTenantService.CreateTenantCommand toCommand() {
|
||||||
|
return new PlatformTenantService.CreateTenantCommand(
|
||||||
|
null,
|
||||||
|
tenantName,
|
||||||
|
adminUsername,
|
||||||
|
adminPassword,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
20,
|
||||||
|
1024,
|
||||||
|
null,
|
||||||
|
new PlatformTenantService.MiniappConfigCommand(
|
||||||
|
"PROD",
|
||||||
|
miniappAppId,
|
||||||
|
"",
|
||||||
|
tenantName + "电子名片",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"v1.0.0",
|
||||||
|
"DRAFT",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpdateTenantRequest(
|
||||||
|
@NotBlank(message = "租户名称不能为空") String tenantName,
|
||||||
|
String miniappAppId
|
||||||
|
) {
|
||||||
|
PlatformTenantService.UpdateTenantCommand toCommand() {
|
||||||
|
return new PlatformTenantService.UpdateTenantCommand(
|
||||||
|
tenantName, miniappAppId, null, null, null, null, null, null, null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record MiniappConfigRequest(
|
||||||
|
String envCode,
|
||||||
|
String miniappAppId,
|
||||||
|
String miniappAppSecret,
|
||||||
|
String miniappName,
|
||||||
|
String miniappOriginalId,
|
||||||
|
String requestDomain,
|
||||||
|
String uploadDomain,
|
||||||
|
String downloadDomain,
|
||||||
|
String versionTag,
|
||||||
|
String publishStatus,
|
||||||
|
String remark
|
||||||
|
) {
|
||||||
|
PlatformTenantService.MiniappConfigCommand toCommand() {
|
||||||
|
return new PlatformTenantService.MiniappConfigCommand(
|
||||||
|
envCode == null ? "PROD" : envCode,
|
||||||
|
miniappAppId,
|
||||||
|
miniappAppSecret,
|
||||||
|
miniappName,
|
||||||
|
miniappOriginalId,
|
||||||
|
requestDomain,
|
||||||
|
uploadDomain,
|
||||||
|
downloadDomain,
|
||||||
|
versionTag,
|
||||||
|
publishStatus == null ? "DRAFT" : publishStatus,
|
||||||
|
remark
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.easycard.module.tenant.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.tenant.service.PlatformTenantService;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/tenant/miniapp-config")
|
||||||
|
public class TenantMiniappController {
|
||||||
|
|
||||||
|
private final PlatformTenantService platformTenantService;
|
||||||
|
|
||||||
|
public TenantMiniappController(PlatformTenantService platformTenantService) {
|
||||||
|
this.platformTenantService = platformTenantService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||||
|
public ApiResponse<PlatformTenantService.MiniappConfigView> getCurrentMiniappConfig() {
|
||||||
|
return ApiResponse.success(platformTenantService.getCurrentTenantMiniappConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
|
||||||
|
public ApiResponse<PlatformTenantService.MiniappConfigView> updateCurrentMiniappConfig(@RequestBody UpdateTenantMiniappConfigRequest request) {
|
||||||
|
return ApiResponse.success(platformTenantService.updateCurrentTenantMiniappConfig(request == null ? null : request.miniappAppId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpdateTenantMiniappConfigRequest(String miniappAppId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.easycard.module.tenant.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@TableName("sys_tenant")
|
||||||
|
public class TenantDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private String tenantCode;
|
||||||
|
private String tenantName;
|
||||||
|
private String tenantShortName;
|
||||||
|
private String contactName;
|
||||||
|
private String contactPhone;
|
||||||
|
private String contactEmail;
|
||||||
|
private String tenantStatus;
|
||||||
|
private LocalDateTime expireAt;
|
||||||
|
private Integer userLimit;
|
||||||
|
private Integer storageLimitMb;
|
||||||
|
private String remark;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantCode() {
|
||||||
|
return tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantCode(String tenantCode) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantName() {
|
||||||
|
return tenantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantName(String tenantName) {
|
||||||
|
this.tenantName = tenantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantShortName() {
|
||||||
|
return tenantShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantShortName(String tenantShortName) {
|
||||||
|
this.tenantShortName = tenantShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactName() {
|
||||||
|
return contactName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactName(String contactName) {
|
||||||
|
this.contactName = contactName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactPhone() {
|
||||||
|
return contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactPhone(String contactPhone) {
|
||||||
|
this.contactPhone = contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactEmail() {
|
||||||
|
return contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactEmail(String contactEmail) {
|
||||||
|
this.contactEmail = contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantStatus() {
|
||||||
|
return tenantStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantStatus(String tenantStatus) {
|
||||||
|
this.tenantStatus = tenantStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getExpireAt() {
|
||||||
|
return expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpireAt(LocalDateTime expireAt) {
|
||||||
|
this.expireAt = expireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUserLimit() {
|
||||||
|
return userLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserLimit(Integer userLimit) {
|
||||||
|
this.userLimit = userLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStorageLimitMb() {
|
||||||
|
return storageLimitMb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStorageLimitMb(Integer storageLimitMb) {
|
||||||
|
this.storageLimitMb = storageLimitMb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCreatedBy() {
|
||||||
|
return createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedBy(Long createdBy) {
|
||||||
|
this.createdBy = createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedTime() {
|
||||||
|
return createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedTime(LocalDateTime createdTime) {
|
||||||
|
this.createdTime = createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUpdatedBy() {
|
||||||
|
return updatedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedBy(Long updatedBy) {
|
||||||
|
this.updatedBy = updatedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedTime() {
|
||||||
|
return updatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedTime(LocalDateTime updatedTime) {
|
||||||
|
this.updatedTime = updatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDeleted() {
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleted(Integer deleted) {
|
||||||
|
this.deleted = deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.easycard.module.tenant.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@TableName("tenant_miniapp_config")
|
||||||
|
public class TenantMiniappConfigDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String envCode;
|
||||||
|
private String miniappAppId;
|
||||||
|
private String miniappAppSecret;
|
||||||
|
private String miniappName;
|
||||||
|
private String miniappOriginalId;
|
||||||
|
private String requestDomain;
|
||||||
|
private String uploadDomain;
|
||||||
|
private String downloadDomain;
|
||||||
|
private String versionTag;
|
||||||
|
private String publishStatus;
|
||||||
|
private LocalDateTime lastPublishedAt;
|
||||||
|
private String remark;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEnvCode() {
|
||||||
|
return envCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnvCode(String envCode) {
|
||||||
|
this.envCode = envCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappAppId() {
|
||||||
|
return miniappAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappAppId(String miniappAppId) {
|
||||||
|
this.miniappAppId = miniappAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappAppSecret() {
|
||||||
|
return miniappAppSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappAppSecret(String miniappAppSecret) {
|
||||||
|
this.miniappAppSecret = miniappAppSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappName() {
|
||||||
|
return miniappName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappName(String miniappName) {
|
||||||
|
this.miniappName = miniappName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappOriginalId() {
|
||||||
|
return miniappOriginalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappOriginalId(String miniappOriginalId) {
|
||||||
|
this.miniappOriginalId = miniappOriginalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestDomain() {
|
||||||
|
return requestDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestDomain(String requestDomain) {
|
||||||
|
this.requestDomain = requestDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUploadDomain() {
|
||||||
|
return uploadDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploadDomain(String uploadDomain) {
|
||||||
|
this.uploadDomain = uploadDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDownloadDomain() {
|
||||||
|
return downloadDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloadDomain(String downloadDomain) {
|
||||||
|
this.downloadDomain = downloadDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersionTag() {
|
||||||
|
return versionTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersionTag(String versionTag) {
|
||||||
|
this.versionTag = versionTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublishStatus() {
|
||||||
|
return publishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublishStatus(String publishStatus) {
|
||||||
|
this.publishStatus = publishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getLastPublishedAt() {
|
||||||
|
return lastPublishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastPublishedAt(LocalDateTime lastPublishedAt) {
|
||||||
|
this.lastPublishedAt = lastPublishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark() {
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(String remark) {
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCreatedBy() {
|
||||||
|
return createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedBy(Long createdBy) {
|
||||||
|
this.createdBy = createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedTime() {
|
||||||
|
return createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedTime(LocalDateTime createdTime) {
|
||||||
|
this.createdTime = createdTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUpdatedBy() {
|
||||||
|
return updatedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedBy(Long updatedBy) {
|
||||||
|
this.updatedBy = updatedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedTime() {
|
||||||
|
return updatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedTime(LocalDateTime updatedTime) {
|
||||||
|
this.updatedTime = updatedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDeleted() {
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleted(Integer deleted) {
|
||||||
|
this.deleted = deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.tenant.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.tenant.dal.entity.TenantDO;
|
||||||
|
|
||||||
|
public interface TenantMapper extends BaseMapper<TenantDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.tenant.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.tenant.dal.entity.TenantMiniappConfigDO;
|
||||||
|
|
||||||
|
public interface TenantMiniappConfigMapper extends BaseMapper<TenantMiniappConfigDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.easycard.module.tenant.service;
|
||||||
|
|
||||||
|
import com.easycard.common.tenant.TenantContext;
|
||||||
|
import com.easycard.module.tenant.vo.TenantDetailResp;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PlatformTenantService {
|
||||||
|
|
||||||
|
List<TenantDetailResp> listTenants();
|
||||||
|
|
||||||
|
Optional<TenantDetailResp> getTenantById(Long tenantId);
|
||||||
|
|
||||||
|
TenantDetailResp createTenant(CreateTenantCommand command);
|
||||||
|
|
||||||
|
TenantDetailResp updateTenant(Long tenantId, UpdateTenantCommand command);
|
||||||
|
|
||||||
|
void deleteTenant(Long tenantId);
|
||||||
|
|
||||||
|
List<MiniappConfigView> listMiniappConfigs();
|
||||||
|
|
||||||
|
MiniappConfigView updateMiniappConfig(Long tenantId, MiniappConfigCommand command);
|
||||||
|
|
||||||
|
MiniappConfigView getCurrentTenantMiniappConfig();
|
||||||
|
|
||||||
|
MiniappConfigView updateCurrentTenantMiniappConfig(String miniappAppId);
|
||||||
|
|
||||||
|
Optional<TenantContext> resolveTenantContextByMiniappAppId(String miniappAppId);
|
||||||
|
|
||||||
|
record CreateTenantCommand(
|
||||||
|
String tenantCode,
|
||||||
|
String tenantName,
|
||||||
|
String adminUsername,
|
||||||
|
String adminPassword,
|
||||||
|
String tenantShortName,
|
||||||
|
String contactName,
|
||||||
|
String contactPhone,
|
||||||
|
String contactEmail,
|
||||||
|
Integer userLimit,
|
||||||
|
Integer storageLimitMb,
|
||||||
|
String remark,
|
||||||
|
MiniappConfigCommand miniappConfig
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpdateTenantCommand(
|
||||||
|
String tenantName,
|
||||||
|
String miniappAppId,
|
||||||
|
String tenantShortName,
|
||||||
|
String contactName,
|
||||||
|
String contactPhone,
|
||||||
|
String contactEmail,
|
||||||
|
Integer userLimit,
|
||||||
|
Integer storageLimitMb,
|
||||||
|
String remark
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MiniappConfigCommand(
|
||||||
|
String envCode,
|
||||||
|
String miniappAppId,
|
||||||
|
String miniappAppSecret,
|
||||||
|
String miniappName,
|
||||||
|
String miniappOriginalId,
|
||||||
|
String requestDomain,
|
||||||
|
String uploadDomain,
|
||||||
|
String downloadDomain,
|
||||||
|
String versionTag,
|
||||||
|
String publishStatus,
|
||||||
|
String remark
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MiniappConfigView(
|
||||||
|
Long tenantId,
|
||||||
|
String tenantCode,
|
||||||
|
String tenantName,
|
||||||
|
String envCode,
|
||||||
|
String miniappAppId,
|
||||||
|
String miniappName,
|
||||||
|
String miniappOriginalId,
|
||||||
|
String requestDomain,
|
||||||
|
String uploadDomain,
|
||||||
|
String downloadDomain,
|
||||||
|
String versionTag,
|
||||||
|
String publishStatus
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
package com.easycard.module.tenant.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.common.tenant.TenantContext;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileDO;
|
||||||
|
import com.easycard.module.card.dal.entity.CardProfileSpecialtyDO;
|
||||||
|
import com.easycard.module.card.dal.mapper.CardProfileMapper;
|
||||||
|
import com.easycard.module.card.dal.mapper.CardProfileSpecialtyMapper;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetDO;
|
||||||
|
import com.easycard.module.file.dal.entity.FileAssetUsageDO;
|
||||||
|
import com.easycard.module.file.dal.mapper.FileAssetMapper;
|
||||||
|
import com.easycard.module.file.dal.mapper.FileAssetUsageMapper;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmPracticeAreaDO;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgFirmProfileDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgFirmPracticeAreaMapper;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgFirmProfileMapper;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardShareLogDO;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardStatDailyDO;
|
||||||
|
import com.easycard.module.stat.dal.entity.CardViewLogDO;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardShareLogMapper;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardStatDailyMapper;
|
||||||
|
import com.easycard.module.stat.dal.mapper.CardViewLogMapper;
|
||||||
|
import com.easycard.module.tenant.dal.entity.TenantDO;
|
||||||
|
import com.easycard.module.tenant.dal.entity.TenantMiniappConfigDO;
|
||||||
|
import com.easycard.module.tenant.dal.mapper.TenantMapper;
|
||||||
|
import com.easycard.module.tenant.dal.mapper.TenantMiniappConfigMapper;
|
||||||
|
import com.easycard.module.tenant.service.PlatformTenantService;
|
||||||
|
import com.easycard.module.tenant.vo.TenantDetailResp;
|
||||||
|
import com.easycard.module.user.dal.entity.SysLoginLogDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysOperationLogDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysLoginLogMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysOperationLogMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Primary
|
||||||
|
public class PlatformTenantServiceImpl implements PlatformTenantService {
|
||||||
|
|
||||||
|
private final TenantMapper tenantMapper;
|
||||||
|
private final TenantMiniappConfigMapper tenantMiniappConfigMapper;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final SysRoleMapper sysRoleMapper;
|
||||||
|
private final SysUserRoleMapper sysUserRoleMapper;
|
||||||
|
private final SysLoginLogMapper sysLoginLogMapper;
|
||||||
|
private final SysOperationLogMapper sysOperationLogMapper;
|
||||||
|
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||||
|
private final OrgFirmProfileMapper orgFirmProfileMapper;
|
||||||
|
private final OrgFirmPracticeAreaMapper orgFirmPracticeAreaMapper;
|
||||||
|
private final CardProfileMapper cardProfileMapper;
|
||||||
|
private final CardProfileSpecialtyMapper cardProfileSpecialtyMapper;
|
||||||
|
private final FileAssetMapper fileAssetMapper;
|
||||||
|
private final FileAssetUsageMapper fileAssetUsageMapper;
|
||||||
|
private final CardViewLogMapper cardViewLogMapper;
|
||||||
|
private final CardShareLogMapper cardShareLogMapper;
|
||||||
|
private final CardStatDailyMapper cardStatDailyMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public PlatformTenantServiceImpl(
|
||||||
|
TenantMapper tenantMapper,
|
||||||
|
TenantMiniappConfigMapper tenantMiniappConfigMapper,
|
||||||
|
SysUserMapper sysUserMapper,
|
||||||
|
SysRoleMapper sysRoleMapper,
|
||||||
|
SysUserRoleMapper sysUserRoleMapper,
|
||||||
|
SysLoginLogMapper sysLoginLogMapper,
|
||||||
|
SysOperationLogMapper sysOperationLogMapper,
|
||||||
|
OrgDepartmentMapper orgDepartmentMapper,
|
||||||
|
OrgFirmProfileMapper orgFirmProfileMapper,
|
||||||
|
OrgFirmPracticeAreaMapper orgFirmPracticeAreaMapper,
|
||||||
|
CardProfileMapper cardProfileMapper,
|
||||||
|
CardProfileSpecialtyMapper cardProfileSpecialtyMapper,
|
||||||
|
FileAssetMapper fileAssetMapper,
|
||||||
|
FileAssetUsageMapper fileAssetUsageMapper,
|
||||||
|
CardViewLogMapper cardViewLogMapper,
|
||||||
|
CardShareLogMapper cardShareLogMapper,
|
||||||
|
CardStatDailyMapper cardStatDailyMapper,
|
||||||
|
PasswordEncoder passwordEncoder
|
||||||
|
) {
|
||||||
|
this.tenantMapper = tenantMapper;
|
||||||
|
this.tenantMiniappConfigMapper = tenantMiniappConfigMapper;
|
||||||
|
this.sysUserMapper = sysUserMapper;
|
||||||
|
this.sysRoleMapper = sysRoleMapper;
|
||||||
|
this.sysUserRoleMapper = sysUserRoleMapper;
|
||||||
|
this.sysLoginLogMapper = sysLoginLogMapper;
|
||||||
|
this.sysOperationLogMapper = sysOperationLogMapper;
|
||||||
|
this.orgDepartmentMapper = orgDepartmentMapper;
|
||||||
|
this.orgFirmProfileMapper = orgFirmProfileMapper;
|
||||||
|
this.orgFirmPracticeAreaMapper = orgFirmPracticeAreaMapper;
|
||||||
|
this.cardProfileMapper = cardProfileMapper;
|
||||||
|
this.cardProfileSpecialtyMapper = cardProfileSpecialtyMapper;
|
||||||
|
this.fileAssetMapper = fileAssetMapper;
|
||||||
|
this.fileAssetUsageMapper = fileAssetUsageMapper;
|
||||||
|
this.cardViewLogMapper = cardViewLogMapper;
|
||||||
|
this.cardShareLogMapper = cardShareLogMapper;
|
||||||
|
this.cardStatDailyMapper = cardStatDailyMapper;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TenantDetailResp> listTenants() {
|
||||||
|
List<TenantDO> tenants = tenantMapper.selectList(
|
||||||
|
Wrappers.<TenantDO>lambdaQuery().eq(TenantDO::getDeleted, 0).orderByDesc(TenantDO::getId)
|
||||||
|
);
|
||||||
|
if (tenants.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> tenantIds = tenants.stream().map(TenantDO::getId).toList();
|
||||||
|
List<TenantMiniappConfigDO> miniappConfigs = tenantMiniappConfigMapper.selectList(
|
||||||
|
Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.in(TenantMiniappConfigDO::getTenantId, tenantIds)
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.eq(TenantMiniappConfigDO::getEnvCode, "PROD")
|
||||||
|
);
|
||||||
|
Map<Long, TenantMiniappConfigDO> miniappConfigMap = new HashMap<>();
|
||||||
|
for (TenantMiniappConfigDO miniappConfig : miniappConfigs) {
|
||||||
|
miniappConfigMap.put(miniappConfig.getTenantId(), miniappConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenants.stream()
|
||||||
|
.map(tenant -> toResp(tenant, miniappConfigMap.get(tenant.getId())))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<TenantDetailResp> getTenantById(Long tenantId) {
|
||||||
|
TenantDO tenant = tenantMapper.selectById(tenantId);
|
||||||
|
if (tenant == null || Integer.valueOf(1).equals(tenant.getDeleted())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
TenantMiniappConfigDO miniappConfig = tenantMiniappConfigMapper.selectOne(
|
||||||
|
Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getTenantId, tenantId)
|
||||||
|
.eq(TenantMiniappConfigDO::getEnvCode, "PROD")
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
return Optional.of(toResp(tenant, miniappConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public TenantDetailResp createTenant(CreateTenantCommand command) {
|
||||||
|
String tenantCode = StringUtils.hasText(command.tenantCode())
|
||||||
|
? command.tenantCode()
|
||||||
|
: generateTenantCode(command.tenantName(), command.miniappConfig() == null ? null : command.miniappConfig().miniappAppId());
|
||||||
|
validateTenantUniqueness(tenantCode, command.miniappConfig() == null ? null : command.miniappConfig().miniappAppId(), null);
|
||||||
|
TenantDO tenant = new TenantDO();
|
||||||
|
tenant.setTenantCode(tenantCode);
|
||||||
|
tenant.setTenantName(command.tenantName());
|
||||||
|
tenant.setTenantShortName(command.tenantShortName());
|
||||||
|
tenant.setContactName(command.contactName());
|
||||||
|
tenant.setContactPhone(command.contactPhone());
|
||||||
|
tenant.setContactEmail(command.contactEmail());
|
||||||
|
tenant.setTenantStatus("ENABLED");
|
||||||
|
tenant.setExpireAt(null);
|
||||||
|
tenant.setUserLimit(command.userLimit());
|
||||||
|
tenant.setStorageLimitMb(command.storageLimitMb());
|
||||||
|
tenant.setRemark(command.remark());
|
||||||
|
tenant.setCreatedBy(1L);
|
||||||
|
tenant.setUpdatedBy(1L);
|
||||||
|
tenant.setDeleted(0);
|
||||||
|
tenantMapper.insert(tenant);
|
||||||
|
|
||||||
|
createBuiltinRoles(tenant.getId());
|
||||||
|
createTenantAdminUser(tenant.getId(), command.adminUsername(), command.adminPassword(), command.tenantName());
|
||||||
|
TenantMiniappConfigDO miniappConfig = saveMiniappConfigInternal(tenant.getId(), command.miniappConfig(), null, 1L);
|
||||||
|
return toResp(tenant, miniappConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public TenantDetailResp updateTenant(Long tenantId, UpdateTenantCommand command) {
|
||||||
|
TenantDO tenant = getRequiredTenant(tenantId);
|
||||||
|
String miniappAppId = StringUtils.hasText(command.miniappAppId()) ? command.miniappAppId().trim() : null;
|
||||||
|
validateTenantUniqueness(null, miniappAppId, tenantId);
|
||||||
|
tenant.setTenantName(command.tenantName());
|
||||||
|
tenant.setTenantShortName(command.tenantShortName());
|
||||||
|
tenant.setContactName(command.contactName());
|
||||||
|
tenant.setContactPhone(command.contactPhone());
|
||||||
|
tenant.setContactEmail(command.contactEmail());
|
||||||
|
tenant.setUserLimit(command.userLimit());
|
||||||
|
tenant.setStorageLimitMb(command.storageLimitMb());
|
||||||
|
tenant.setRemark(command.remark());
|
||||||
|
tenant.setUpdatedBy(1L);
|
||||||
|
tenantMapper.updateById(tenant);
|
||||||
|
TenantMiniappConfigDO miniappConfig = tenantMiniappConfigMapper.selectOne(Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getTenantId, tenantId)
|
||||||
|
.eq(TenantMiniappConfigDO::getEnvCode, "PROD")
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (miniappConfig != null || miniappAppId != null) {
|
||||||
|
miniappConfig = saveMiniappConfigInternal(
|
||||||
|
tenantId,
|
||||||
|
new MiniappConfigCommand(
|
||||||
|
miniappConfig == null ? "PROD" : miniappConfig.getEnvCode(),
|
||||||
|
miniappAppId,
|
||||||
|
miniappConfig == null ? null : miniappConfig.getMiniappAppSecret(),
|
||||||
|
miniappConfig == null ? tenant.getTenantName() + "电子名片" : miniappConfig.getMiniappName(),
|
||||||
|
miniappConfig == null ? null : miniappConfig.getMiniappOriginalId(),
|
||||||
|
miniappConfig == null ? null : miniappConfig.getRequestDomain(),
|
||||||
|
miniappConfig == null ? null : miniappConfig.getUploadDomain(),
|
||||||
|
miniappConfig == null ? null : miniappConfig.getDownloadDomain(),
|
||||||
|
miniappConfig == null ? "v1.0.0" : miniappConfig.getVersionTag(),
|
||||||
|
miniappConfig == null ? "DRAFT" : miniappConfig.getPublishStatus(),
|
||||||
|
miniappConfig == null ? null : miniappConfig.getRemark()
|
||||||
|
),
|
||||||
|
miniappConfig,
|
||||||
|
1L
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return toResp(tenant, miniappConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteTenant(Long tenantId) {
|
||||||
|
TenantDO tenant = getRequiredTenant(tenantId);
|
||||||
|
|
||||||
|
sysUserRoleMapper.delete(Wrappers.<SysUserRoleDO>lambdaQuery().eq(SysUserRoleDO::getTenantId, tenantId));
|
||||||
|
sysLoginLogMapper.delete(Wrappers.<SysLoginLogDO>lambdaQuery().eq(SysLoginLogDO::getTenantId, tenantId));
|
||||||
|
sysOperationLogMapper.delete(Wrappers.<SysOperationLogDO>lambdaQuery().eq(SysOperationLogDO::getTenantId, tenantId));
|
||||||
|
cardViewLogMapper.delete(Wrappers.<CardViewLogDO>lambdaQuery().eq(CardViewLogDO::getTenantId, tenantId));
|
||||||
|
cardShareLogMapper.delete(Wrappers.<CardShareLogDO>lambdaQuery().eq(CardShareLogDO::getTenantId, tenantId));
|
||||||
|
cardStatDailyMapper.delete(Wrappers.<CardStatDailyDO>lambdaQuery().eq(CardStatDailyDO::getTenantId, tenantId));
|
||||||
|
|
||||||
|
cardProfileSpecialtyMapper.deleteForceByTenantId(tenantId);
|
||||||
|
cardProfileMapper.delete(Wrappers.<CardProfileDO>lambdaQuery().eq(CardProfileDO::getTenantId, tenantId));
|
||||||
|
fileAssetUsageMapper.delete(Wrappers.<FileAssetUsageDO>lambdaQuery().eq(FileAssetUsageDO::getTenantId, tenantId));
|
||||||
|
fileAssetMapper.delete(Wrappers.<FileAssetDO>lambdaQuery().eq(FileAssetDO::getTenantId, tenantId));
|
||||||
|
orgDepartmentMapper.delete(Wrappers.<OrgDepartmentDO>lambdaQuery().eq(OrgDepartmentDO::getTenantId, tenantId));
|
||||||
|
orgFirmPracticeAreaMapper.delete(Wrappers.<OrgFirmPracticeAreaDO>lambdaQuery().eq(OrgFirmPracticeAreaDO::getTenantId, tenantId));
|
||||||
|
orgFirmProfileMapper.delete(Wrappers.<OrgFirmProfileDO>lambdaQuery().eq(OrgFirmProfileDO::getTenantId, tenantId));
|
||||||
|
sysUserMapper.delete(Wrappers.<SysUserDO>lambdaQuery().eq(SysUserDO::getTenantId, tenantId));
|
||||||
|
sysRoleMapper.delete(Wrappers.<SysRoleDO>lambdaQuery().eq(SysRoleDO::getTenantId, tenantId));
|
||||||
|
tenantMiniappConfigMapper.delete(Wrappers.<TenantMiniappConfigDO>lambdaQuery().eq(TenantMiniappConfigDO::getTenantId, tenantId));
|
||||||
|
tenantMapper.deleteById(tenant.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MiniappConfigView> listMiniappConfigs() {
|
||||||
|
List<TenantMiniappConfigDO> configs = tenantMiniappConfigMapper.selectList(Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.orderByDesc(TenantMiniappConfigDO::getId));
|
||||||
|
if (configs.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Long> tenantIds = configs.stream().map(TenantMiniappConfigDO::getTenantId).distinct().toList();
|
||||||
|
Map<Long, TenantDO> tenantMap = tenantMapper.selectBatchIds(tenantIds).stream()
|
||||||
|
.collect(HashMap::new, (map, item) -> map.put(item.getId(), item), HashMap::putAll);
|
||||||
|
return configs.stream().map(config -> {
|
||||||
|
TenantDO tenant = tenantMap.get(config.getTenantId());
|
||||||
|
return new MiniappConfigView(
|
||||||
|
config.getTenantId(),
|
||||||
|
tenant == null ? "" : tenant.getTenantCode(),
|
||||||
|
tenant == null ? "" : tenant.getTenantName(),
|
||||||
|
config.getEnvCode(),
|
||||||
|
config.getMiniappAppId(),
|
||||||
|
config.getMiniappName(),
|
||||||
|
config.getMiniappOriginalId(),
|
||||||
|
config.getRequestDomain(),
|
||||||
|
config.getUploadDomain(),
|
||||||
|
config.getDownloadDomain(),
|
||||||
|
config.getVersionTag(),
|
||||||
|
config.getPublishStatus()
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MiniappConfigView updateMiniappConfig(Long tenantId, MiniappConfigCommand command) {
|
||||||
|
TenantDO tenant = getRequiredTenant(tenantId);
|
||||||
|
validateTenantUniqueness(null, command.miniappAppId(), tenantId);
|
||||||
|
TenantMiniappConfigDO config = saveMiniappConfigInternal(tenantId, command, getTenantMiniappConfig(tenantId, command.envCode()), 1L);
|
||||||
|
return toMiniappConfigView(tenant, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MiniappConfigView getCurrentTenantMiniappConfig() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
Long tenantId = getRequiredCurrentTenantId(loginUser);
|
||||||
|
TenantDO tenant = getRequiredTenant(tenantId);
|
||||||
|
return toMiniappConfigView(tenant, getTenantMiniappConfig(tenantId, "PROD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public MiniappConfigView updateCurrentTenantMiniappConfig(String miniappAppId) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
Long tenantId = getRequiredCurrentTenantId(loginUser);
|
||||||
|
TenantDO tenant = getRequiredTenant(tenantId);
|
||||||
|
String normalizedAppId = StringUtils.hasText(miniappAppId) ? miniappAppId.trim() : null;
|
||||||
|
validateTenantUniqueness(null, normalizedAppId, tenantId);
|
||||||
|
|
||||||
|
TenantMiniappConfigDO existed = getTenantMiniappConfig(tenantId, "PROD");
|
||||||
|
TenantMiniappConfigDO saved = saveMiniappConfigInternal(
|
||||||
|
tenantId,
|
||||||
|
new MiniappConfigCommand(
|
||||||
|
"PROD",
|
||||||
|
normalizedAppId,
|
||||||
|
existed == null ? null : existed.getMiniappAppSecret(),
|
||||||
|
existed == null ? tenant.getTenantName() + "电子名片" : existed.getMiniappName(),
|
||||||
|
existed == null ? null : existed.getMiniappOriginalId(),
|
||||||
|
existed == null ? null : existed.getRequestDomain(),
|
||||||
|
existed == null ? null : existed.getUploadDomain(),
|
||||||
|
existed == null ? null : existed.getDownloadDomain(),
|
||||||
|
existed == null ? "v1.0.0" : existed.getVersionTag(),
|
||||||
|
existed == null ? "DRAFT" : existed.getPublishStatus(),
|
||||||
|
existed == null ? null : existed.getRemark()
|
||||||
|
),
|
||||||
|
existed,
|
||||||
|
loginUser.userId()
|
||||||
|
);
|
||||||
|
return toMiniappConfigView(tenant, saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<TenantContext> resolveTenantContextByMiniappAppId(String miniappAppId) {
|
||||||
|
if (!StringUtils.hasText(miniappAppId)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
TenantMiniappConfigDO miniappConfig = tenantMiniappConfigMapper.selectOne(
|
||||||
|
Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getMiniappAppId, miniappAppId)
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (miniappConfig == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
TenantDO tenant = tenantMapper.selectById(miniappConfig.getTenantId());
|
||||||
|
if (tenant == null || Integer.valueOf(1).equals(tenant.getDeleted())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(new TenantContext(tenant.getId(), tenant.getTenantCode(), miniappAppId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateTenantUniqueness(String tenantCode, String miniappAppId, Long currentTenantId) {
|
||||||
|
if (StringUtils.hasText(tenantCode)) {
|
||||||
|
TenantDO existedTenant = tenantMapper.selectOne(Wrappers.<TenantDO>lambdaQuery()
|
||||||
|
.eq(TenantDO::getTenantCode, tenantCode)
|
||||||
|
.eq(TenantDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existedTenant != null && !existedTenant.getId().equals(currentTenantId)) {
|
||||||
|
throw new BusinessException("TENANT_CODE_DUPLICATED", "租户编码已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(miniappAppId)) {
|
||||||
|
TenantMiniappConfigDO existedConfig = tenantMiniappConfigMapper.selectOne(Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getMiniappAppId, miniappAppId)
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existedConfig != null && !existedConfig.getTenantId().equals(currentTenantId)) {
|
||||||
|
throw new BusinessException("MINIAPP_APP_ID_DUPLICATED", "小程序 AppID 已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TenantDO getRequiredTenant(Long tenantId) {
|
||||||
|
TenantDO tenant = tenantMapper.selectById(tenantId);
|
||||||
|
if (tenant == null || Integer.valueOf(1).equals(tenant.getDeleted())) {
|
||||||
|
throw new BusinessException("TENANT_NOT_FOUND", "租户不存在");
|
||||||
|
}
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long getRequiredCurrentTenantId(LoginUser loginUser) {
|
||||||
|
if (loginUser.tenantId() == null) {
|
||||||
|
throw new BusinessException("TENANT_CONTEXT_MISSING", "当前账号未绑定租户");
|
||||||
|
}
|
||||||
|
return loginUser.tenantId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TenantMiniappConfigDO getTenantMiniappConfig(Long tenantId, String envCode) {
|
||||||
|
return tenantMiniappConfigMapper.selectOne(Wrappers.<TenantMiniappConfigDO>lambdaQuery()
|
||||||
|
.eq(TenantMiniappConfigDO::getTenantId, tenantId)
|
||||||
|
.eq(TenantMiniappConfigDO::getEnvCode, envCode)
|
||||||
|
.eq(TenantMiniappConfigDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TenantMiniappConfigDO saveMiniappConfigInternal(Long tenantId, MiniappConfigCommand command, TenantMiniappConfigDO config, Long operatorId) {
|
||||||
|
if (command == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (config == null) {
|
||||||
|
config = new TenantMiniappConfigDO();
|
||||||
|
config.setTenantId(tenantId);
|
||||||
|
config.setCreatedBy(operatorId);
|
||||||
|
config.setDeleted(0);
|
||||||
|
}
|
||||||
|
config.setEnvCode(StringUtils.hasText(command.envCode()) ? command.envCode() : "PROD");
|
||||||
|
config.setMiniappAppId(StringUtils.hasText(command.miniappAppId()) ? command.miniappAppId().trim() : null);
|
||||||
|
config.setMiniappAppSecret(command.miniappAppSecret());
|
||||||
|
config.setMiniappName(command.miniappName());
|
||||||
|
config.setMiniappOriginalId(command.miniappOriginalId());
|
||||||
|
config.setRequestDomain(command.requestDomain());
|
||||||
|
config.setUploadDomain(command.uploadDomain());
|
||||||
|
config.setDownloadDomain(command.downloadDomain());
|
||||||
|
config.setVersionTag(command.versionTag());
|
||||||
|
config.setPublishStatus(command.publishStatus());
|
||||||
|
config.setRemark(command.remark());
|
||||||
|
config.setUpdatedBy(operatorId);
|
||||||
|
if ("PUBLISHED".equals(command.publishStatus())) {
|
||||||
|
config.setLastPublishedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
if (config.getId() == null) {
|
||||||
|
tenantMiniappConfigMapper.insert(config);
|
||||||
|
} else {
|
||||||
|
tenantMiniappConfigMapper.updateById(config);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MiniappConfigView toMiniappConfigView(TenantDO tenant, TenantMiniappConfigDO config) {
|
||||||
|
if (config == null) {
|
||||||
|
return new MiniappConfigView(
|
||||||
|
tenant.getId(),
|
||||||
|
emptyIfNull(tenant.getTenantCode()),
|
||||||
|
emptyIfNull(tenant.getTenantName()),
|
||||||
|
"PROD",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"v1.0.0",
|
||||||
|
"UNCONFIGURED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new MiniappConfigView(
|
||||||
|
tenant.getId(),
|
||||||
|
emptyIfNull(tenant.getTenantCode()),
|
||||||
|
emptyIfNull(tenant.getTenantName()),
|
||||||
|
StringUtils.hasText(config.getEnvCode()) ? config.getEnvCode() : "PROD",
|
||||||
|
emptyIfNull(config.getMiniappAppId()),
|
||||||
|
emptyIfNull(config.getMiniappName()),
|
||||||
|
emptyIfNull(config.getMiniappOriginalId()),
|
||||||
|
emptyIfNull(config.getRequestDomain()),
|
||||||
|
emptyIfNull(config.getUploadDomain()),
|
||||||
|
emptyIfNull(config.getDownloadDomain()),
|
||||||
|
StringUtils.hasText(config.getVersionTag()) ? config.getVersionTag() : "v1.0.0",
|
||||||
|
StringUtils.hasText(config.getPublishStatus()) ? config.getPublishStatus() : "DRAFT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String emptyIfNull(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBuiltinRoles(Long tenantId) {
|
||||||
|
insertRoleIfAbsent(tenantId, "TENANT_ADMIN", "租户管理员", "TENANT");
|
||||||
|
insertRoleIfAbsent(tenantId, "TENANT_USER", "普通用户", "SELF");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertRoleIfAbsent(Long tenantId, String roleCode, String roleName, String dataScope) {
|
||||||
|
SysRoleDO existed = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.eq(SysRoleDO::getTenantId, tenantId)
|
||||||
|
.eq(SysRoleDO::getRoleCode, roleCode)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existed != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SysRoleDO role = new SysRoleDO();
|
||||||
|
role.setTenantId(tenantId);
|
||||||
|
role.setRoleScope("TENANT");
|
||||||
|
role.setRoleCode(roleCode);
|
||||||
|
role.setRoleName(roleName);
|
||||||
|
role.setDataScope(dataScope);
|
||||||
|
role.setIsBuiltin(1);
|
||||||
|
role.setRoleStatus("ENABLED");
|
||||||
|
role.setCreatedBy(1L);
|
||||||
|
role.setUpdatedBy(1L);
|
||||||
|
role.setDeleted(0);
|
||||||
|
sysRoleMapper.insert(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTenantAdminUser(Long tenantId, String adminUsername, String adminPassword, String tenantName) {
|
||||||
|
String username = StringUtils.hasText(adminUsername) ? adminUsername.trim() : "tenant.admin";
|
||||||
|
String rawPassword = StringUtils.hasText(adminPassword) ? adminPassword : "123456";
|
||||||
|
SysUserDO existed = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, tenantId)
|
||||||
|
.eq(SysUserDO::getUsername, username)
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existed != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SysUserDO user = new SysUserDO();
|
||||||
|
user.setTenantId(tenantId);
|
||||||
|
user.setUserType("TENANT");
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(rawPassword));
|
||||||
|
user.setRealName(resolveAdminDisplayName(tenantName));
|
||||||
|
user.setNickName(resolveAdminDisplayName(tenantName));
|
||||||
|
user.setGender("UNKNOWN");
|
||||||
|
user.setUserStatus("ENABLED");
|
||||||
|
user.setMustUpdatePassword(1);
|
||||||
|
user.setCreatedBy(1L);
|
||||||
|
user.setUpdatedBy(1L);
|
||||||
|
user.setDeleted(0);
|
||||||
|
sysUserMapper.insert(user);
|
||||||
|
|
||||||
|
SysRoleDO adminRole = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.eq(SysRoleDO::getTenantId, tenantId)
|
||||||
|
.eq(SysRoleDO::getRoleCode, "TENANT_ADMIN")
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (adminRole != null) {
|
||||||
|
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||||
|
userRole.setTenantId(tenantId);
|
||||||
|
userRole.setUserId(user.getId());
|
||||||
|
userRole.setRoleId(adminRole.getId());
|
||||||
|
userRole.setCreatedBy(1L);
|
||||||
|
sysUserRoleMapper.insert(userRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateTenantCode(String tenantName, String miniappAppId) {
|
||||||
|
if (StringUtils.hasText(miniappAppId)) {
|
||||||
|
return "tenant-" + miniappAppId.trim().toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||||
|
}
|
||||||
|
return "tenant-" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveAdminDisplayName(String tenantName) {
|
||||||
|
if (!StringUtils.hasText(tenantName)) {
|
||||||
|
return "租户管理员";
|
||||||
|
}
|
||||||
|
return tenantName.length() > 24 ? tenantName.substring(0, 24) : tenantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TenantDetailResp toResp(TenantDO tenant, TenantMiniappConfigDO miniappConfig) {
|
||||||
|
TenantDetailResp resp = new TenantDetailResp();
|
||||||
|
resp.setId(tenant.getId());
|
||||||
|
resp.setTenantCode(tenant.getTenantCode());
|
||||||
|
resp.setTenantName(tenant.getTenantName());
|
||||||
|
resp.setTenantShortName(tenant.getTenantShortName());
|
||||||
|
resp.setUserLimit(tenant.getUserLimit());
|
||||||
|
resp.setStorageLimitMb(tenant.getStorageLimitMb());
|
||||||
|
resp.setContactName(tenant.getContactName());
|
||||||
|
resp.setContactPhone(tenant.getContactPhone());
|
||||||
|
if (miniappConfig != null) {
|
||||||
|
resp.setMiniappAppId(miniappConfig.getMiniappAppId());
|
||||||
|
resp.setMiniappName(miniappConfig.getMiniappName());
|
||||||
|
resp.setMiniappOriginalId(miniappConfig.getMiniappOriginalId());
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.easycard.module.tenant.vo;
|
||||||
|
|
||||||
|
public class TenantDetailResp {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String tenantCode;
|
||||||
|
private String tenantName;
|
||||||
|
private String tenantShortName;
|
||||||
|
private Integer userLimit;
|
||||||
|
private Integer storageLimitMb;
|
||||||
|
private String contactName;
|
||||||
|
private String contactPhone;
|
||||||
|
private String miniappAppId;
|
||||||
|
private String miniappName;
|
||||||
|
private String miniappOriginalId;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantCode() {
|
||||||
|
return tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantCode(String tenantCode) {
|
||||||
|
this.tenantCode = tenantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantName() {
|
||||||
|
return tenantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantName(String tenantName) {
|
||||||
|
this.tenantName = tenantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantShortName() {
|
||||||
|
return tenantShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantShortName(String tenantShortName) {
|
||||||
|
this.tenantShortName = tenantShortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUserLimit() {
|
||||||
|
return userLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserLimit(Integer userLimit) {
|
||||||
|
this.userLimit = userLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStorageLimitMb() {
|
||||||
|
return storageLimitMb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStorageLimitMb(Integer storageLimitMb) {
|
||||||
|
this.storageLimitMb = storageLimitMb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactName() {
|
||||||
|
return contactName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactName(String contactName) {
|
||||||
|
this.contactName = contactName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContactPhone() {
|
||||||
|
return contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactPhone(String contactPhone) {
|
||||||
|
this.contactPhone = contactPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappAppId() {
|
||||||
|
return miniappAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappAppId(String miniappAppId) {
|
||||||
|
this.miniappAppId = miniappAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappName() {
|
||||||
|
return miniappName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappName(String miniappName) {
|
||||||
|
this.miniappName = miniappName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMiniappOriginalId() {
|
||||||
|
return miniappOriginalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMiniappOriginalId(String miniappOriginalId) {
|
||||||
|
this.miniappOriginalId = miniappOriginalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.easycard.module.tenant.web;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.tenant.TenantContext;
|
||||||
|
import com.easycard.common.tenant.TenantContextHolder;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.easycard.module.tenant.service.PlatformTenantService;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class MiniappTenantContextFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private static final String MINIAPP_APP_ID_HEADER = "X-Miniapp-Appid";
|
||||||
|
|
||||||
|
private final PlatformTenantService platformTenantService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public MiniappTenantContextFilter(PlatformTenantService platformTenantService, ObjectMapper objectMapper) {
|
||||||
|
this.platformTenantService = platformTenantService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
return !request.getRequestURI().startsWith("/api/open/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String miniappAppId = request.getHeader(MINIAPP_APP_ID_HEADER);
|
||||||
|
if (!StringUtils.hasText(miniappAppId)) {
|
||||||
|
writeError(response, HttpServletResponse.SC_BAD_REQUEST,
|
||||||
|
ApiResponse.fail("MINIAPP_APP_ID_MISSING", "缺少请求头 X-Miniapp-Appid"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<TenantContext> tenantContextOptional = platformTenantService.resolveTenantContextByMiniappAppId(miniappAppId);
|
||||||
|
if (tenantContextOptional.isEmpty()) {
|
||||||
|
writeError(response, HttpServletResponse.SC_NOT_FOUND,
|
||||||
|
ApiResponse.fail("TENANT_MINIAPP_NOT_FOUND", "未找到对应的小程序租户配置"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
TenantContextHolder.set(tenantContextOptional.get());
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
TenantContextHolder.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeError(HttpServletResponse response, int status, ApiResponse<Void> apiResponse) throws IOException {
|
||||||
|
response.setStatus(status);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(apiResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
backend/easycard-module-user/pom.xml
Normal file
35
backend/easycard-module-user/pom.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<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>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>easycard-module-user</artifactId>
|
||||||
|
<name>easycard-module-user</name>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-common</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-module-org</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.easycard.module.user.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.web.ClientRequestUtils;
|
||||||
|
import com.easycard.module.user.service.AuthService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/auth")
|
||||||
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
public AuthController(AuthService authService) {
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ApiResponse<AuthService.LoginResult> login(@Valid @RequestBody LoginRequest request, HttpServletRequest servletRequest) {
|
||||||
|
return ApiResponse.success(authService.login(
|
||||||
|
request.username(),
|
||||||
|
request.password(),
|
||||||
|
ClientRequestUtils.getClientIp(servletRequest),
|
||||||
|
servletRequest.getHeader("User-Agent")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
public ApiResponse<Void> logout() {
|
||||||
|
return ApiResponse.success("退出成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
public ApiResponse<AuthService.CurrentUserView> me() {
|
||||||
|
return ApiResponse.success(authService.getCurrentUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/change-password")
|
||||||
|
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
|
||||||
|
authService.changePassword(request.oldPassword(), request.newPassword());
|
||||||
|
return ApiResponse.success("修改成功", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record LoginRequest(
|
||||||
|
@NotBlank(message = "账号不能为空") String username,
|
||||||
|
@NotBlank(message = "密码不能为空") String password
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChangePasswordRequest(
|
||||||
|
@NotBlank(message = "原密码不能为空") String oldPassword,
|
||||||
|
@NotBlank(message = "新密码不能为空") String newPassword
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.easycard.module.user.controller;
|
||||||
|
|
||||||
|
import com.easycard.common.api.ApiResponse;
|
||||||
|
import com.easycard.module.user.service.TenantUserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/tenant/users")
|
||||||
|
public class TenantUserController {
|
||||||
|
|
||||||
|
private final TenantUserService tenantUserService;
|
||||||
|
|
||||||
|
public TenantUserController(TenantUserService tenantUserService) {
|
||||||
|
this.tenantUserService = tenantUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<List<TenantUserService.TenantUserView>> listUsers(@RequestParam(required = false) String keyword) {
|
||||||
|
return ApiResponse.success(tenantUserService.listUsers(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantUserService.TenantUserView> createUser(@Valid @RequestBody UpsertTenantUserRequest request) {
|
||||||
|
return ApiResponse.success(tenantUserService.createUser(request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{userId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantUserService.TenantUserView> updateUser(@PathVariable Long userId, @Valid @RequestBody UpsertTenantUserRequest request) {
|
||||||
|
return ApiResponse.success(tenantUserService.updateUser(userId, request.toServiceRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{userId}/status")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantUserService.TenantUserView> updateUserStatus(@PathVariable Long userId, @Valid @RequestBody UpdateTenantUserStatusRequest request) {
|
||||||
|
return ApiResponse.success(tenantUserService.updateUserStatus(userId, request.userStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/sort")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<Void> sortUsers(@Valid @RequestBody SortTenantUsersRequest request) {
|
||||||
|
tenantUserService.sortUsers(request.userIds());
|
||||||
|
return ApiResponse.success("排序已生效", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{userId}/reset-password")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<Void> resetPassword(@PathVariable Long userId) {
|
||||||
|
tenantUserService.resetPassword(userId);
|
||||||
|
return ApiResponse.success("重置成功", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{userId}")
|
||||||
|
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||||
|
public ApiResponse<TenantUserService.TenantUserView> getUser(@PathVariable Long userId) {
|
||||||
|
return ApiResponse.success(tenantUserService.getUser(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpsertTenantUserRequest(
|
||||||
|
@NotBlank(message = "账号不能为空") String username,
|
||||||
|
@NotBlank(message = "姓名不能为空") String realName,
|
||||||
|
String mobile,
|
||||||
|
@Email(message = "邮箱格式错误") String email,
|
||||||
|
Long deptId,
|
||||||
|
String jobTitle,
|
||||||
|
@NotBlank(message = "状态不能为空") String userStatus,
|
||||||
|
@NotBlank(message = "角色不能为空") String roleCode
|
||||||
|
) {
|
||||||
|
TenantUserService.CreateOrUpdateUserRequest toServiceRequest() {
|
||||||
|
return new TenantUserService.CreateOrUpdateUserRequest(
|
||||||
|
username,
|
||||||
|
realName,
|
||||||
|
mobile,
|
||||||
|
email,
|
||||||
|
deptId,
|
||||||
|
jobTitle,
|
||||||
|
userStatus,
|
||||||
|
roleCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record UpdateTenantUserStatusRequest(
|
||||||
|
@NotBlank(message = "状态不能为空") String userStatus
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record SortTenantUsersRequest(
|
||||||
|
@Size(min = 1, message = "排序成员不能为空") List<@NotNull(message = "成员ID不能为空") Long> userIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.easycard.module.user.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("sys_login_log")
|
||||||
|
public class SysLoginLogDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long userId;
|
||||||
|
private String userType;
|
||||||
|
private String loginType;
|
||||||
|
private String loginStatus;
|
||||||
|
private String clientIp;
|
||||||
|
private String userAgent;
|
||||||
|
private String failReason;
|
||||||
|
private LocalDateTime loginAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.easycard.module.user.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("sys_operation_log")
|
||||||
|
public class SysOperationLogDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long userId;
|
||||||
|
private String userName;
|
||||||
|
private String moduleName;
|
||||||
|
private String bizType;
|
||||||
|
private Long bizId;
|
||||||
|
private String operationType;
|
||||||
|
private String requestMethod;
|
||||||
|
private String requestUri;
|
||||||
|
private String requestBody;
|
||||||
|
private String responseCode;
|
||||||
|
private String responseMessage;
|
||||||
|
private String clientIp;
|
||||||
|
private LocalDateTime operatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.easycard.module.user.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("sys_role")
|
||||||
|
public class SysRoleDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String roleScope;
|
||||||
|
private String roleCode;
|
||||||
|
private String roleName;
|
||||||
|
private String dataScope;
|
||||||
|
private Integer isBuiltin;
|
||||||
|
private String roleStatus;
|
||||||
|
private String remark;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.easycard.module.user.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("sys_user")
|
||||||
|
public class SysUserDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String userType;
|
||||||
|
private String username;
|
||||||
|
private String passwordHash;
|
||||||
|
private String realName;
|
||||||
|
private String nickName;
|
||||||
|
private String gender;
|
||||||
|
private String mobile;
|
||||||
|
private String email;
|
||||||
|
private Long avatarAssetId;
|
||||||
|
private Long deptId;
|
||||||
|
private String jobTitle;
|
||||||
|
private String userStatus;
|
||||||
|
private Integer memberSort;
|
||||||
|
private Integer mustUpdatePassword;
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
|
private String lastLoginIp;
|
||||||
|
private String remark;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
private Long updatedBy;
|
||||||
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.easycard.module.user.dal.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("sys_user_role")
|
||||||
|
public class SysUserRoleDO {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long userId;
|
||||||
|
private Long roleId;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime createdTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.user.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysLoginLogDO;
|
||||||
|
|
||||||
|
public interface SysLoginLogMapper extends BaseMapper<SysLoginLogDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.user.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysOperationLogDO;
|
||||||
|
|
||||||
|
public interface SysOperationLogMapper extends BaseMapper<SysOperationLogDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.user.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||||
|
|
||||||
|
public interface SysRoleMapper extends BaseMapper<SysRoleDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.user.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||||
|
|
||||||
|
public interface SysUserMapper extends BaseMapper<SysUserDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.easycard.module.user.dal.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||||
|
|
||||||
|
public interface SysUserRoleMapper extends BaseMapper<SysUserRoleDO> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.easycard.module.user.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.JwtTokenService;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final SysRoleMapper sysRoleMapper;
|
||||||
|
private final SysUserRoleMapper sysUserRoleMapper;
|
||||||
|
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
private final UserAuditService userAuditService;
|
||||||
|
|
||||||
|
@Value("${easycard.security.jwt-expire-hours:24}")
|
||||||
|
private long jwtExpireHours;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public LoginResult login(String username, String password, String clientIp, String userAgent) {
|
||||||
|
SysUserDO user = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getUsername, username)
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (user == null) {
|
||||||
|
userAuditService.recordLogin(0L, null, "UNKNOWN", "FAIL", clientIp, userAgent, "账号不存在");
|
||||||
|
throw new BusinessException("AUTH_FAILED", "账号或密码错误");
|
||||||
|
}
|
||||||
|
if (!"ENABLED".equals(user.getUserStatus())) {
|
||||||
|
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "FAIL", clientIp, userAgent, "账号已停用");
|
||||||
|
throw new BusinessException("AUTH_FAILED", "账号已停用");
|
||||||
|
}
|
||||||
|
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
||||||
|
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "FAIL", clientIp, userAgent, "密码错误");
|
||||||
|
throw new BusinessException("AUTH_FAILED", "账号或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> roleCodes = getRoleCodes(user.getId(), user.getTenantId());
|
||||||
|
LoginUser loginUser = new LoginUser(
|
||||||
|
user.getId(),
|
||||||
|
user.getTenantId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealName(),
|
||||||
|
user.getUserType(),
|
||||||
|
roleCodes
|
||||||
|
);
|
||||||
|
user.setLastLoginAt(LocalDateTime.now());
|
||||||
|
user.setLastLoginIp(clientIp);
|
||||||
|
user.setUpdatedBy(user.getId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "SUCCESS", clientIp, userAgent, null);
|
||||||
|
return new LoginResult(jwtTokenService.generateToken(loginUser, Duration.ofHours(jwtExpireHours)), buildCurrentUser(user, roleCodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrentUserView getCurrentUser() {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
SysUserDO user = getRequiredUser(loginUser.userId());
|
||||||
|
return buildCurrentUser(user, loginUser.roleCodes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void changePassword(String oldPassword, String newPassword) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
SysUserDO user = getRequiredUser(loginUser.userId());
|
||||||
|
if (!passwordEncoder.matches(oldPassword, user.getPasswordHash())) {
|
||||||
|
throw new BusinessException("PASSWORD_INVALID", "原密码错误");
|
||||||
|
}
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||||
|
user.setMustUpdatePassword(0);
|
||||||
|
user.setUpdatedBy(user.getId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
userAuditService.recordOperation(user.getTenantId(), user.getId(), user.getRealName(), "AUTH", "PASSWORD", user.getId(), "CHANGE_PASSWORD");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SysUserDO getRequiredUser(Long userId) {
|
||||||
|
SysUserDO user = sysUserMapper.selectById(userId);
|
||||||
|
if (user == null || Integer.valueOf(1).equals(user.getDeleted())) {
|
||||||
|
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRoleCodes(Long userId, Long tenantId) {
|
||||||
|
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||||
|
.eq(SysUserRoleDO::getUserId, userId)
|
||||||
|
.eq(SysUserRoleDO::getTenantId, tenantId));
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).toList();
|
||||||
|
return sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.in(SysRoleDO::getId, roleIds)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.eq(SysRoleDO::getRoleStatus, "ENABLED"))
|
||||||
|
.stream()
|
||||||
|
.map(SysRoleDO::getRoleCode)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CurrentUserView buildCurrentUser(SysUserDO user, List<String> roleCodes) {
|
||||||
|
String tenantName = "平台";
|
||||||
|
if (user.getTenantId() != null && user.getTenantId() > 0) {
|
||||||
|
tenantName = resolveTenantName(user.getTenantId());
|
||||||
|
}
|
||||||
|
String roleName = roleCodes.isEmpty() ? "未分配角色" : roleCodes.get(0);
|
||||||
|
String deptName = "";
|
||||||
|
if (user.getDeptId() != null) {
|
||||||
|
OrgDepartmentDO department = orgDepartmentMapper.selectById(user.getDeptId());
|
||||||
|
deptName = department == null ? "" : department.getDeptName();
|
||||||
|
}
|
||||||
|
return new CurrentUserView(
|
||||||
|
user.getId(),
|
||||||
|
user.getTenantId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealName(),
|
||||||
|
user.getUserType(),
|
||||||
|
tenantName,
|
||||||
|
roleName,
|
||||||
|
roleCodes,
|
||||||
|
deptName,
|
||||||
|
user.getJobTitle(),
|
||||||
|
user.getMustUpdatePassword() != null && user.getMustUpdatePassword() == 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTenantName(Long tenantId) {
|
||||||
|
List<String> tenantNames = jdbcTemplate.query(
|
||||||
|
"""
|
||||||
|
SELECT tenant_name
|
||||||
|
FROM sys_tenant
|
||||||
|
WHERE id = ?
|
||||||
|
AND deleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(rs, rowNum) -> rs.getString("tenant_name"),
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
|
String tenantName = tenantNames.isEmpty() ? "" : tenantNames.getFirst();
|
||||||
|
return StringUtils.hasText(tenantName) ? tenantName : "当前租户";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record LoginResult(String accessToken, CurrentUserView currentUser) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CurrentUserView(
|
||||||
|
Long id,
|
||||||
|
Long tenantId,
|
||||||
|
String username,
|
||||||
|
String realName,
|
||||||
|
String userType,
|
||||||
|
String tenantName,
|
||||||
|
String roleName,
|
||||||
|
List<String> roleCodes,
|
||||||
|
String deptName,
|
||||||
|
String jobTitle,
|
||||||
|
boolean mustUpdatePassword
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
package com.easycard.module.user.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.easycard.common.auth.LoginUser;
|
||||||
|
import com.easycard.common.auth.SecurityUtils;
|
||||||
|
import com.easycard.common.exception.BusinessException;
|
||||||
|
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||||
|
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||||
|
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TenantUserService {
|
||||||
|
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final SysRoleMapper sysRoleMapper;
|
||||||
|
private final SysUserRoleMapper sysUserRoleMapper;
|
||||||
|
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserAuditService userAuditService;
|
||||||
|
|
||||||
|
public List<TenantUserView> listUsers(String keyword) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
Long tenantId = loginUser.tenantId();
|
||||||
|
List<SysUserDO> users = sysUserMapper.selectList(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, tenantId)
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.like(StringUtils.hasText(keyword), SysUserDO::getRealName, keyword)
|
||||||
|
.orderByAsc(SysUserDO::getMemberSort)
|
||||||
|
.orderByDesc(SysUserDO::getId));
|
||||||
|
if (users.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<Long, OrgDepartmentDO> deptMap = orgDepartmentMapper.selectList(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||||
|
.eq(OrgDepartmentDO::getTenantId, tenantId)
|
||||||
|
.eq(OrgDepartmentDO::getDeleted, 0))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(OrgDepartmentDO::getId, Function.identity()));
|
||||||
|
Map<Long, String> roleCodeMap = loadPrimaryRoleMap(users, tenantId);
|
||||||
|
return users.stream().map(user -> new TenantUserView(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealName(),
|
||||||
|
user.getMobile(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getJobTitle(),
|
||||||
|
user.getUserStatus(),
|
||||||
|
user.getMemberSort(),
|
||||||
|
user.getDeptId(),
|
||||||
|
deptMap.containsKey(user.getDeptId()) ? deptMap.get(user.getDeptId()).getDeptName() : "",
|
||||||
|
roleCodeMap.getOrDefault(user.getId(), "")
|
||||||
|
)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public TenantUserView createUser(CreateOrUpdateUserRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||||
|
SysUserDO existedUser = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(SysUserDO::getUsername, request.username())
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (existedUser != null) {
|
||||||
|
throw new BusinessException("USERNAME_DUPLICATED", "登录账号已存在");
|
||||||
|
}
|
||||||
|
validateUserStatus(request.userStatus());
|
||||||
|
SysRoleDO role = getRoleByCode(loginUser.tenantId(), request.roleCode());
|
||||||
|
SysUserDO user = new SysUserDO();
|
||||||
|
user.setTenantId(loginUser.tenantId());
|
||||||
|
user.setUserType("TENANT");
|
||||||
|
user.setUsername(request.username());
|
||||||
|
user.setPasswordHash(passwordEncoder.encode("123456"));
|
||||||
|
user.setRealName(request.realName());
|
||||||
|
user.setNickName(request.realName());
|
||||||
|
user.setGender("UNKNOWN");
|
||||||
|
user.setMobile(request.mobile());
|
||||||
|
user.setEmail(request.email());
|
||||||
|
user.setDeptId(request.deptId());
|
||||||
|
user.setJobTitle(request.jobTitle());
|
||||||
|
user.setUserStatus(request.userStatus());
|
||||||
|
user.setMemberSort(nextMemberSort(loginUser.tenantId()));
|
||||||
|
user.setMustUpdatePassword(1);
|
||||||
|
user.setCreatedBy(loginUser.userId());
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
user.setDeleted(0);
|
||||||
|
sysUserMapper.insert(user);
|
||||||
|
|
||||||
|
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||||
|
userRole.setTenantId(loginUser.tenantId());
|
||||||
|
userRole.setUserId(user.getId());
|
||||||
|
userRole.setRoleId(role.getId());
|
||||||
|
userRole.setCreatedBy(loginUser.userId());
|
||||||
|
sysUserRoleMapper.insert(userRole);
|
||||||
|
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "CREATE");
|
||||||
|
return getUser(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public TenantUserView updateUser(Long userId, CreateOrUpdateUserRequest request) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||||
|
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||||
|
validateUserStatus(request.userStatus());
|
||||||
|
SysRoleDO role = getRoleByCode(loginUser.tenantId(), request.roleCode());
|
||||||
|
user.setRealName(request.realName());
|
||||||
|
user.setNickName(request.realName());
|
||||||
|
user.setMobile(request.mobile());
|
||||||
|
user.setEmail(request.email());
|
||||||
|
user.setDeptId(request.deptId());
|
||||||
|
user.setJobTitle(request.jobTitle());
|
||||||
|
user.setUserStatus(request.userStatus());
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
|
||||||
|
sysUserRoleMapper.delete(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||||
|
.eq(SysUserRoleDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(SysUserRoleDO::getUserId, userId));
|
||||||
|
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||||
|
userRole.setTenantId(loginUser.tenantId());
|
||||||
|
userRole.setUserId(user.getId());
|
||||||
|
userRole.setRoleId(role.getId());
|
||||||
|
userRole.setCreatedBy(loginUser.userId());
|
||||||
|
sysUserRoleMapper.insert(userRole);
|
||||||
|
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "UPDATE");
|
||||||
|
return getUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public TenantUserView updateUserStatus(Long userId, String userStatus) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
validateUserStatus(userStatus);
|
||||||
|
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||||
|
if (userStatus.equals(user.getUserStatus())) {
|
||||||
|
return getUser(userId);
|
||||||
|
}
|
||||||
|
user.setUserStatus(userStatus);
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
userAuditService.recordOperation(
|
||||||
|
loginUser.tenantId(),
|
||||||
|
loginUser.userId(),
|
||||||
|
loginUser.realName(),
|
||||||
|
"USER",
|
||||||
|
"SYS_USER",
|
||||||
|
user.getId(),
|
||||||
|
"UPDATE_STATUS"
|
||||||
|
);
|
||||||
|
return getUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void sortUsers(List<Long> userIds) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
if (userIds == null || userIds.isEmpty()) {
|
||||||
|
throw new BusinessException("USER_SORT_INVALID", "排序数据不能为空");
|
||||||
|
}
|
||||||
|
Set<Long> uniqueUserIds = new HashSet<>(userIds);
|
||||||
|
if (uniqueUserIds.size() != userIds.size()) {
|
||||||
|
throw new BusinessException("USER_SORT_INVALID", "排序数据存在重复成员");
|
||||||
|
}
|
||||||
|
List<SysUserDO> tenantUsers = sysUserMapper.selectList(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, loginUser.tenantId())
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.select(SysUserDO::getId));
|
||||||
|
Set<Long> expectedUserIds = tenantUsers.stream().map(SysUserDO::getId).collect(Collectors.toSet());
|
||||||
|
if (expectedUserIds.size() != uniqueUserIds.size() || !expectedUserIds.equals(uniqueUserIds)) {
|
||||||
|
throw new BusinessException("USER_SORT_INVALID", "排序数据必须包含当前租户全部成员");
|
||||||
|
}
|
||||||
|
Map<Long, SysUserDO> userMap = sysUserMapper.selectBatchIds(userIds).stream()
|
||||||
|
.filter(item -> loginUser.tenantId().equals(item.getTenantId()) && Integer.valueOf(0).equals(item.getDeleted()))
|
||||||
|
.collect(Collectors.toMap(SysUserDO::getId, Function.identity()));
|
||||||
|
for (int i = 0; i < userIds.size(); i++) {
|
||||||
|
Long sortedUserId = userIds.get(i);
|
||||||
|
SysUserDO user = userMap.get(sortedUserId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new BusinessException("USER_SORT_INVALID", "成员不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
user.setMemberSort((i + 1) * 10);
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
}
|
||||||
|
userAuditService.recordOperation(
|
||||||
|
loginUser.tenantId(),
|
||||||
|
loginUser.userId(),
|
||||||
|
loginUser.realName(),
|
||||||
|
"USER",
|
||||||
|
"SYS_USER",
|
||||||
|
0L,
|
||||||
|
"SORT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void resetPassword(Long userId) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||||
|
user.setPasswordHash(passwordEncoder.encode("123456"));
|
||||||
|
user.setMustUpdatePassword(1);
|
||||||
|
user.setUpdatedBy(loginUser.userId());
|
||||||
|
sysUserMapper.updateById(user);
|
||||||
|
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "RESET_PASSWORD");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TenantUserView getUser(Long userId) {
|
||||||
|
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||||
|
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||||
|
Map<Long, String> roleMap = loadPrimaryRoleMap(List.of(user), loginUser.tenantId());
|
||||||
|
String deptName = "";
|
||||||
|
if (user.getDeptId() != null) {
|
||||||
|
OrgDepartmentDO department = orgDepartmentMapper.selectById(user.getDeptId());
|
||||||
|
deptName = department == null ? "" : department.getDeptName();
|
||||||
|
}
|
||||||
|
return new TenantUserView(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealName(),
|
||||||
|
user.getMobile(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getJobTitle(),
|
||||||
|
user.getUserStatus(),
|
||||||
|
user.getMemberSort(),
|
||||||
|
user.getDeptId(),
|
||||||
|
deptName,
|
||||||
|
roleMap.getOrDefault(user.getId(), "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, String> loadPrimaryRoleMap(List<SysUserDO> users, Long tenantId) {
|
||||||
|
List<Long> userIds = users.stream().map(SysUserDO::getId).toList();
|
||||||
|
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||||
|
.eq(SysUserRoleDO::getTenantId, tenantId)
|
||||||
|
.in(SysUserRoleDO::getUserId, userIds));
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).distinct().toList();
|
||||||
|
Map<Long, SysRoleDO> roleMap = sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.in(SysRoleDO::getId, roleIds)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(SysRoleDO::getId, Function.identity()));
|
||||||
|
return userRoles.stream()
|
||||||
|
.sorted(Comparator.comparing(SysUserRoleDO::getRoleId))
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
SysUserRoleDO::getUserId,
|
||||||
|
item -> roleMap.containsKey(item.getRoleId()) ? roleMap.get(item.getRoleId()).getRoleCode() : "",
|
||||||
|
(left, right) -> left
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDepartment(Long tenantId, Long deptId) {
|
||||||
|
if (deptId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OrgDepartmentDO department = orgDepartmentMapper.selectById(deptId);
|
||||||
|
if (department == null || Integer.valueOf(1).equals(department.getDeleted()) || !tenantId.equals(department.getTenantId())) {
|
||||||
|
throw new BusinessException("DEPARTMENT_NOT_FOUND", "所属组织不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateUserStatus(String userStatus) {
|
||||||
|
if (!"ENABLED".equals(userStatus) && !"DISABLED".equals(userStatus)) {
|
||||||
|
throw new BusinessException("USER_STATUS_INVALID", "成员状态仅支持启用或停用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer nextMemberSort(Long tenantId) {
|
||||||
|
SysUserDO lastUser = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||||
|
.eq(SysUserDO::getTenantId, tenantId)
|
||||||
|
.eq(SysUserDO::getDeleted, 0)
|
||||||
|
.orderByDesc(SysUserDO::getMemberSort)
|
||||||
|
.orderByDesc(SysUserDO::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
int baseSort = lastUser == null || lastUser.getMemberSort() == null ? 0 : lastUser.getMemberSort();
|
||||||
|
return baseSort + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysRoleDO getRoleByCode(Long tenantId, String roleCode) {
|
||||||
|
SysRoleDO role = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||||
|
.eq(SysRoleDO::getTenantId, tenantId)
|
||||||
|
.eq(SysRoleDO::getRoleCode, roleCode)
|
||||||
|
.eq(SysRoleDO::getDeleted, 0)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (role == null) {
|
||||||
|
throw new BusinessException("ROLE_NOT_FOUND", "角色不存在");
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserDO getTenantUser(Long userId, Long tenantId) {
|
||||||
|
SysUserDO user = sysUserMapper.selectById(userId);
|
||||||
|
if (user == null || Integer.valueOf(1).equals(user.getDeleted()) || !tenantId.equals(user.getTenantId())) {
|
||||||
|
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateOrUpdateUserRequest(
|
||||||
|
String username,
|
||||||
|
String realName,
|
||||||
|
String mobile,
|
||||||
|
String email,
|
||||||
|
Long deptId,
|
||||||
|
String jobTitle,
|
||||||
|
String userStatus,
|
||||||
|
String roleCode
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TenantUserView(
|
||||||
|
Long id,
|
||||||
|
String username,
|
||||||
|
String realName,
|
||||||
|
String mobile,
|
||||||
|
String email,
|
||||||
|
String jobTitle,
|
||||||
|
String userStatus,
|
||||||
|
Integer memberSort,
|
||||||
|
Long deptId,
|
||||||
|
String deptName,
|
||||||
|
String roleCode
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.easycard.module.user.service;
|
||||||
|
|
||||||
|
import com.easycard.module.user.dal.entity.SysLoginLogDO;
|
||||||
|
import com.easycard.module.user.dal.entity.SysOperationLogDO;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysLoginLogMapper;
|
||||||
|
import com.easycard.module.user.dal.mapper.SysOperationLogMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserAuditService {
|
||||||
|
|
||||||
|
private final SysLoginLogMapper sysLoginLogMapper;
|
||||||
|
private final SysOperationLogMapper sysOperationLogMapper;
|
||||||
|
|
||||||
|
public UserAuditService(SysLoginLogMapper sysLoginLogMapper, SysOperationLogMapper sysOperationLogMapper) {
|
||||||
|
this.sysLoginLogMapper = sysLoginLogMapper;
|
||||||
|
this.sysOperationLogMapper = sysOperationLogMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordLogin(Long tenantId, Long userId, String userType, String loginStatus, String clientIp, String userAgent, String failReason) {
|
||||||
|
SysLoginLogDO loginLog = new SysLoginLogDO();
|
||||||
|
loginLog.setTenantId(tenantId == null ? 0L : tenantId);
|
||||||
|
loginLog.setUserId(userId);
|
||||||
|
loginLog.setUserType(userType == null ? "TENANT" : userType);
|
||||||
|
loginLog.setLoginType("PASSWORD");
|
||||||
|
loginLog.setLoginStatus(loginStatus);
|
||||||
|
loginLog.setClientIp(clientIp);
|
||||||
|
loginLog.setUserAgent(userAgent);
|
||||||
|
loginLog.setFailReason(failReason);
|
||||||
|
loginLog.setLoginAt(LocalDateTime.now());
|
||||||
|
sysLoginLogMapper.insert(loginLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordOperation(Long tenantId, Long userId, String userName, String moduleName, String bizType, Long bizId, String operationType) {
|
||||||
|
SysOperationLogDO operationLog = new SysOperationLogDO();
|
||||||
|
operationLog.setTenantId(tenantId == null ? 0L : tenantId);
|
||||||
|
operationLog.setUserId(userId);
|
||||||
|
operationLog.setUserName(userName);
|
||||||
|
operationLog.setModuleName(moduleName);
|
||||||
|
operationLog.setBizType(bizType);
|
||||||
|
operationLog.setBizId(bizId);
|
||||||
|
operationLog.setOperationType(operationType);
|
||||||
|
operationLog.setOperatedAt(LocalDateTime.now());
|
||||||
|
sysOperationLogMapper.insert(operationLog);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
backend/pom.xml
Normal file
94
backend/pom.xml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<groupId>com.easycard</groupId>
|
||||||
|
<artifactId>easycard-parent</artifactId>
|
||||||
|
<version>0.1.0-SNAPSHOT</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<name>easycard-parent</name>
|
||||||
|
<description>Easycard backend parent project</description>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>easycard-common</module>
|
||||||
|
<module>easycard-module-system</module>
|
||||||
|
<module>easycard-module-tenant</module>
|
||||||
|
<module>easycard-module-org</module>
|
||||||
|
<module>easycard-module-user</module>
|
||||||
|
<module>easycard-module-card</module>
|
||||||
|
<module>easycard-module-file</module>
|
||||||
|
<module>easycard-module-stat</module>
|
||||||
|
<module>easycard-boot</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<spring.boot.version>3.3.13</spring.boot.version>
|
||||||
|
<mybatis.plus.version>3.5.7</mybatis.plus.version>
|
||||||
|
<springdoc.version>2.6.0</springdoc.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
<version>${mybatis.plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>${springdoc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.34</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>${java.version}</release>
|
||||||
|
<parameters>true</parameters>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring.boot.version}</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Reference in New Issue
Block a user