feat: 搭建后端多租户名片服务
- 初始化 Spring Boot 多模块工程与通用基础能力 - 增加租户、组织、用户、名片、文件、统计等业务模块 - 补充数据库迁移脚本与基础测试
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user