feat: 搭建后端多租户名片服务

- 初始化 Spring Boot 多模块工程与通用基础能力

- 增加租户、组织、用户、名片、文件、统计等业务模块

- 补充数据库迁移脚本与基础测试
This commit is contained in:
2026-03-20 12:43:21 +08:00
parent 1a2a078c0f
commit 9ef50288e9
95 changed files with 6722 additions and 0 deletions

View File

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

View File

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

View File

@@ -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("电子名片系统后端接口文档"));
}
}

View File

@@ -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", "令牌无效或已过期")));
}
}

View File

@@ -0,0 +1,10 @@
spring:
flyway:
connect-retries: 30
logging:
file:
path: ${LOG_PATH:/app/logs}
level:
root: INFO
com.easycard: INFO

View File

@@ -0,0 +1,5 @@
logging:
level:
root: INFO
com.easycard: INFO
org.flywaydb: INFO

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

View File

@@ -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 版本脚本为准,不再额外维护一份完整的本地建表脚本。

View File

@@ -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='操作日志表';

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
ALTER TABLE `tenant_miniapp_config`
MODIFY COLUMN `miniapp_app_id` VARCHAR(64) NULL COMMENT '小程序AppID';

View File

@@ -0,0 +1,2 @@
ALTER TABLE `org_firm_profile`
DROP COLUMN `display_status`;

View File

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

View File

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

View File

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