diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql index d8b07de..833d00b 100644 --- a/backend/design/db_schema_pgsql.sql +++ b/backend/design/db_schema_pgsql.sql @@ -524,8 +524,8 @@ INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "st INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (1, 'security.token.access_ttl_minutes', '120', 'int', 1, 1, 'Access Token 有效期(分钟)', 0, '2026-02-09 09:54:21.888052', '2026-03-10 10:15:39.55035'); -INSERT INTO sys_user ("user_id", "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required") -VALUES (1, 'admin', '管理员', 'admin', NULL, '$2a$10$BOm1iCFj3ObfBeyQxOvjVO659vXvIRGOd4YR62r0TUHqSusWW5bFS', 1, 0, '2026-02-09 09:54:21.880637', '2026-02-28 17:57:32.63338', 't', NULL); +INSERT INTO sys_user ( "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required") +VALUES ( 'admin', '管理员', 'admin', NULL, '$2a$10$BOm1iCFj3ObfBeyQxOvjVO659vXvIRGOd4YR62r0TUHqSusWW5bFS', 1, 0, '2026-02-09 09:54:21.880637', '2026-02-28 17:57:32.63338', 't', NULL); INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (9, 'biz_hotword_category', '热词类别', 1, '语音识别纠错分类', '2026-02-28 17:08:52.362532', '2026-02-28 17:08:52.362532'); diff --git a/backend/pom.xml b/backend/pom.xml index 89e193b..4e8246b 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -124,6 +124,11 @@ jsoup 1.17.2 + + com.unisbase + unisbase-spring-boot-starter + 0.1.0 + diff --git a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java deleted file mode 100644 index 85c6772..0000000 --- a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.imeeting.auth; - -import com.imeeting.common.RedisKeys; -import com.imeeting.entity.SysTenant; -import com.imeeting.entity.SysUser; -import com.imeeting.security.LoginUser; -import com.imeeting.service.AuthScopeService; -import com.imeeting.service.AuthVersionService; -import com.imeeting.service.SysParamService; -import com.imeeting.service.SysPermissionService; -import com.imeeting.mapper.SysTenantMapper; -import com.imeeting.mapper.SysUserMapper; -import io.jsonwebtoken.Claims; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.context.annotation.Lazy; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Set; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtTokenProvider jwtTokenProvider; - private final SysPermissionService sysPermissionService; - private final SysTenantMapper sysTenantMapper; - private final SysUserMapper sysUserMapper; - private final SysParamService sysParamService; - private final StringRedisTemplate redisTemplate; - private final AuthScopeService authScopeService; - private final AuthVersionService authVersionService; - - public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, - @Lazy SysPermissionService sysPermissionService, - SysTenantMapper sysTenantMapper, - SysUserMapper sysUserMapper, - @Lazy SysParamService sysParamService, - StringRedisTemplate redisTemplate, - AuthScopeService authScopeService, - AuthVersionService authVersionService) { - this.jwtTokenProvider = jwtTokenProvider; - this.sysPermissionService = sysPermissionService; - this.sysTenantMapper = sysTenantMapper; - this.sysUserMapper = sysUserMapper; - this.sysParamService = sysParamService; - this.redisTemplate = redisTemplate; - this.authScopeService = authScopeService; - this.authVersionService = authVersionService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - String uri = request.getRequestURI(); - // Skip filter for public endpoints - if (uri.startsWith("/auth/") || uri.equals("/api/params/value")) { - filterChain.doFilter(request, response); - return; - } - - String authHeader = request.getHeader("Authorization"); - if (authHeader != null && authHeader.startsWith("Bearer ")) { - String token = authHeader.substring(7); - try { - Claims claims = jwtTokenProvider.parseToken(token); - String username = claims.get("username", String.class); - String displayName = claims.get("displayName", String.class); - Long userId = claims.get("userId", Long.class); - Long tenantId = claims.get("tenantId", Long.class); - Number tokenAuthVersionNum = claims.get("authVersion", Number.class); - - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // 1. Validate User Status (Ignore Tenant isolation here) - SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); - if (user == null || user.getStatus() != 1 || user.getIsDeleted() != 0) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"User account is disabled or deleted\"}"); - return; - } - - // 2. Validate Tenant Status & Grace Period - // Skip validation for system platform tenant (ID=0) - Long activeTenantId = tenantId; - if (activeTenantId != null && !Long.valueOf(0).equals(activeTenantId)) { - SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(activeTenantId); - if (tenant == null || tenant.getStatus() != 1) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant is disabled\"}"); - return; - } - - if (tenant.getExpireTime() != null) { - LocalDateTime now = LocalDateTime.now(); - if (now.isAfter(tenant.getExpireTime())) { - String graceDaysStr = sysParamService.getParamValue("sys.tenant.grace_period_days", "0"); - int graceDays = Integer.parseInt(graceDaysStr); - if (now.isAfter(tenant.getExpireTime().plusDays(graceDays))) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant subscription expired\"}"); - return; - } - } - } - } - - long currentAuthVersion = authVersionService.getVersion(userId, activeTenantId); - long requestAuthVersion = tokenAuthVersionNum == null ? 0L : tokenAuthVersionNum.longValue(); - if (currentAuthVersion != requestAuthVersion) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token revoked\"}"); - return; - } - - // 3. Get Permissions (With Redis Cache, Key must include tenantId) - String permKey = RedisKeys.authPermKey(userId, activeTenantId, currentAuthVersion); - Set permissions; - String cachedPerms = redisTemplate.opsForValue().get(permKey); - if (cachedPerms != null && !cachedPerms.trim().isEmpty()) { - permissions = Set.of(cachedPerms.split(",")); - } else { - permissions = sysPermissionService.listPermissionCodesByUserId(userId, activeTenantId); - if (permissions != null && !permissions.isEmpty()) { - redisTemplate.opsForValue().set(permKey, String.join(",", permissions), java.time.Duration.ofHours(2)); - } else { - permissions = Collections.emptySet(); - } - } - - boolean isTenantAdmin = authScopeService.isTenantAdmin(userId, activeTenantId); - LoginUser loginUser = new LoginUser(userId, activeTenantId, username, displayName,user.getIsPlatformAdmin(), isTenantAdmin, permissions); - - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - } catch (io.jsonwebtoken.ExpiredJwtException e) { - SecurityContextHolder.clearContext(); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token expired\"}"); - return; - } catch (io.jsonwebtoken.JwtException e) { - SecurityContextHolder.clearContext(); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"code\":\"401\",\"msg\":\"Invalid token\"}"); - return; - } catch (Exception ignored) { - SecurityContextHolder.clearContext(); - } - } - filterChain.doFilter(request, response); - } -} diff --git a/backend/src/main/java/com/imeeting/auth/JwtTokenProvider.java b/backend/src/main/java/com/imeeting/auth/JwtTokenProvider.java deleted file mode 100644 index f8cc728..0000000 --- a/backend/src/main/java/com/imeeting/auth/JwtTokenProvider.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.imeeting.auth; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.util.Date; -import java.util.Map; - -@Component -public class JwtTokenProvider { - private final Key key; - - public JwtTokenProvider(@Value("${security.jwt.secret}") String secret) { - this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - } - - public String createToken(Map claims, long ttlMillis) { - Date now = new Date(); - Date exp = new Date(now.getTime() + ttlMillis); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(exp) - .signWith(key, SignatureAlgorithm.HS256) - .compact(); - } - - public Claims parseToken(String token) { - return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); - } -} diff --git a/backend/src/main/java/com/imeeting/auth/dto/CaptchaResponse.java b/backend/src/main/java/com/imeeting/auth/dto/CaptchaResponse.java deleted file mode 100644 index 9a19ec8..0000000 --- a/backend/src/main/java/com/imeeting/auth/dto/CaptchaResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.imeeting.auth.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class CaptchaResponse { - private String captchaId; - private String imageBase64; -} diff --git a/backend/src/main/java/com/imeeting/auth/dto/DeviceCodeRequest.java b/backend/src/main/java/com/imeeting/auth/dto/DeviceCodeRequest.java deleted file mode 100644 index 0bc531d..0000000 --- a/backend/src/main/java/com/imeeting/auth/dto/DeviceCodeRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.imeeting.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class DeviceCodeRequest { - @NotBlank - private String username; - @NotBlank - private String password; - private String captchaId; - private String captchaCode; - private String deviceName; -} diff --git a/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java b/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java deleted file mode 100644 index 43e8a6f..0000000 --- a/backend/src/main/java/com/imeeting/auth/dto/LoginRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.imeeting.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class LoginRequest { - private String tenantCode; - @NotBlank - private String username; - @NotBlank - private String password; - private String captchaId; - private String captchaCode; - private String deviceCode; -} diff --git a/backend/src/main/java/com/imeeting/auth/dto/RefreshRequest.java b/backend/src/main/java/com/imeeting/auth/dto/RefreshRequest.java deleted file mode 100644 index 2bb995b..0000000 --- a/backend/src/main/java/com/imeeting/auth/dto/RefreshRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.imeeting.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class RefreshRequest { - @NotBlank - private String refreshToken; -} diff --git a/backend/src/main/java/com/imeeting/auth/dto/TokenResponse.java b/backend/src/main/java/com/imeeting/auth/dto/TokenResponse.java deleted file mode 100644 index 78f18b2..0000000 --- a/backend/src/main/java/com/imeeting/auth/dto/TokenResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.imeeting.auth.dto; - -import lombok.Builder; -import lombok.Data; -import java.util.List; - -@Data -@Builder -public class TokenResponse { - private String accessToken; - private String refreshToken; - private long accessExpiresInMinutes; - private long refreshExpiresInDays; - private Integer pwdResetRequired; - private List availableTenants; - - @Data - @Builder - public static class TenantInfo { - private Long tenantId; - private String tenantCode; - private String tenantName; - } -} diff --git a/backend/src/main/java/com/imeeting/common/ApiResponse.java b/backend/src/main/java/com/imeeting/common/ApiResponse.java deleted file mode 100644 index 8a9b39b..0000000 --- a/backend/src/main/java/com/imeeting/common/ApiResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.imeeting.common; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ApiResponse { - private String code; - private String msg; - private T data; - - public static ApiResponse ok(T data) { - return new ApiResponse<>("0", "OK", data); - } - - public static ApiResponse error(String msg) { - return new ApiResponse<>("-1", msg, null); - } -} diff --git a/backend/src/main/java/com/imeeting/common/GlobalExceptionHandler.java b/backend/src/main/java/com/imeeting/common/GlobalExceptionHandler.java deleted file mode 100644 index c23c8c6..0000000 --- a/backend/src/main/java/com/imeeting/common/GlobalExceptionHandler.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imeeting.common; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class GlobalExceptionHandler { - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - @ExceptionHandler(IllegalArgumentException.class) - public ApiResponse handleIllegalArgument(IllegalArgumentException ex) { - log.warn("Business error: {}", ex.getMessage()); - return ApiResponse.error(ex.getMessage()); - } - - @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) - public ApiResponse handleAccessDenied(org.springframework.security.access.AccessDeniedException ex) { - log.warn("Access denied: {}", ex.getMessage()); - return ApiResponse.error("无权限操作"); - } - - @ExceptionHandler(Exception.class) - public ApiResponse handleGeneric(Exception ex) { - log.error("Unhandled exception", ex); - return ApiResponse.error("系统异常"); - } -} diff --git a/backend/src/main/java/com/imeeting/common/PageResult.java b/backend/src/main/java/com/imeeting/common/PageResult.java deleted file mode 100644 index c77de57..0000000 --- a/backend/src/main/java/com/imeeting/common/PageResult.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.common; - -import lombok.Data; - -@Data -public class PageResult { - private long total; - private T records; -} diff --git a/backend/src/main/java/com/imeeting/common/annotation/Log.java b/backend/src/main/java/com/imeeting/common/annotation/Log.java deleted file mode 100644 index b9fee6e..0000000 --- a/backend/src/main/java/com/imeeting/common/annotation/Log.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.imeeting.common.annotation; - -import java.lang.annotation.*; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface Log { - String value() default ""; // 操作描述 - String type() default ""; // 资源类型/模块名 -} diff --git a/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java b/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java deleted file mode 100644 index 80cf8ba..0000000 --- a/backend/src/main/java/com/imeeting/common/aspect/LogAspect.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.imeeting.common.aspect; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.imeeting.common.annotation.Log; -import com.imeeting.entity.SysLog; -import com.imeeting.security.LoginUser; -import com.imeeting.service.SysLogService; -import jakarta.servlet.http.HttpServletRequest; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.lang.reflect.Method; -import java.time.LocalDateTime; - -@Aspect -@Component -public class LogAspect { - - private final SysLogService sysLogService; - private final ObjectMapper objectMapper; - - public LogAspect(SysLogService sysLogService, ObjectMapper objectMapper) { - this.sysLogService = sysLogService; - this.objectMapper = objectMapper; - } - - @Around("@annotation(com.imeeting.common.annotation.Log)") - public Object around(ProceedingJoinPoint point) throws Throwable { - long start = System.currentTimeMillis(); - Object result = null; - Exception exception = null; - - try { - result = point.proceed(); - return result; - } catch (Exception e) { - exception = e; - throw e; - } finally { - saveLog(point, result, exception, System.currentTimeMillis() - start); - } - } - - private void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception e, long duration) { - try { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) return; - HttpServletRequest request = attributes.getRequest(); - - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - Method method = signature.getMethod(); - Log logAnnotation = method.getAnnotation(Log.class); - - SysLog sysLog = new SysLog(); - sysLog.setLogType("OPERATION"); - sysLog.setOperation(logAnnotation.value()); - sysLog.setMethod(request.getMethod() + " " + request.getRequestURI()); - sysLog.setDuration(duration); - sysLog.setIp(request.getRemoteAddr()); - sysLog.setCreatedAt(LocalDateTime.now()); - - // 仅保留请求参数,移除响应结果 - sysLog.setParams(getArgsJson(joinPoint)); - - // 获取当前租户和用户信息 - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - LoginUser user = (LoginUser) auth.getPrincipal(); - sysLog.setUserId(user.getUserId()); - sysLog.setTenantId(user.getTenantId()); - sysLog.setUsername(user.getUsername()); - } - - sysLog.setStatus(e != null ? 0 : 1); - sysLogService.recordLog(sysLog); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private String getArgsJson(ProceedingJoinPoint joinPoint) { - try { - Object[] args = joinPoint.getArgs(); - if (args == null || args.length == 0) return null; - - StringBuilder sb = new StringBuilder(); - for (Object arg : args) { - if (arg instanceof jakarta.servlet.ServletRequest - || arg instanceof jakarta.servlet.ServletResponse - || arg instanceof org.springframework.web.multipart.MultipartFile) { - continue; - } - try { - sb.append(objectMapper.writeValueAsString(arg)).append(" "); - } catch (Exception e) { - sb.append("[Unserializable Argument] "); - } - } - return sb.toString().trim(); - } catch (Exception e) { - return "[Error capturing params]"; - } - } -} diff --git a/backend/src/main/java/com/imeeting/config/CacheConfig.java b/backend/src/main/java/com/imeeting/config/CacheConfig.java deleted file mode 100644 index 08cf9f3..0000000 --- a/backend/src/main/java/com/imeeting/config/CacheConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imeeting.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -@Configuration -public class CacheConfig { - - @Bean - public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { - RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofHours(1)) // Default TTL - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) - .disableCachingNullValues(); - - return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(config) - .build(); - } -} diff --git a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java b/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java deleted file mode 100644 index d92c137..0000000 --- a/backend/src/main/java/com/imeeting/config/MybatisPlusConfig.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.imeeting.config; - -import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; -import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; -import com.imeeting.security.LoginUser; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.LongValue; -import org.apache.ibatis.reflection.MetaObject; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.time.LocalDateTime; -import java.util.List; - -@Configuration -public class MybatisPlusConfig { - - @Bean - public MybatisPlusInterceptor mybatisPlusInterceptor() { - MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); - interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { - @Override - public Expression getTenantId() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - LoginUser user = (LoginUser) auth.getPrincipal(); - if (user.getTenantId() != null) { - return new LongValue(user.getTenantId()); - } - } - // If no tenant context (e.g. system task or error), return 0 - return new LongValue(0); - } - - @Override - public String getTenantIdColumn() { - return "tenant_id"; - } - - @Override - public boolean ignoreTable(String tableName) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - LoginUser user = (LoginUser) auth.getPrincipal(); - // 只有当平台管理员处于系统租户(0)时,才忽略所有过滤。 - // 如果他切换到了具体租户(>0),则必须接受过滤,确保只能看到当前租户数据。 - if (Boolean.TRUE.equals(user.getIsPlatformAdmin()) && Long.valueOf(0).equals(user.getTenantId())) { - return true; - } - } - - // 公共表始终忽略过滤 - return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param", "biz_speakers", "biz_prompt_templates", "biz_asr_models", "biz_llm_models", "biz_meetings", "biz_meeting_transcripts", "biz_ai_tasks").contains(tableName.toLowerCase()); - } - })); - interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); - return interceptor; - } - - @Bean - public MetaObjectHandler metaObjectHandler() { - return new MetaObjectHandler() { - @Override - public void insertFill(MetaObject metaObject) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - LoginUser user = (LoginUser) auth.getPrincipal(); - strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId()); - } else { - strictInsertFill(metaObject, "tenantId", Long.class, 0L); - } - - strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class); - strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class); - strictInsertFill(metaObject, "status", () -> 1, Integer.class); - strictInsertFill(metaObject, "isDeleted", () -> 0, Integer.class); - } - - @Override - public void updateFill(MetaObject metaObject) { - strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class); - } - }; - } -} diff --git a/backend/src/main/java/com/imeeting/config/SecurityConfig.java b/backend/src/main/java/com/imeeting/config/SecurityConfig.java deleted file mode 100644 index c6f621d..0000000 --- a/backend/src/main/java/com/imeeting/config/SecurityConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.imeeting.config; - -import com.imeeting.auth.JwtAuthenticationFilter; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -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.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.List; - -@Configuration -@EnableMethodSecurity -public class SecurityConfig { - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { - http.csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/**").permitAll() - .requestMatchers("/api/open/**").permitAll() - .requestMatchers("/api/static/**").permitAll() - .requestMatchers("/api/params/value").permitAll() - .anyRequest().authenticated() - ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { - return configuration.getAuthenticationManager(); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(true); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } -} diff --git a/backend/src/main/java/com/imeeting/config/SysParamCacheInitializer.java b/backend/src/main/java/com/imeeting/config/SysParamCacheInitializer.java deleted file mode 100644 index d73a664..0000000 --- a/backend/src/main/java/com/imeeting/config/SysParamCacheInitializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.imeeting.config; - -import com.imeeting.service.SysParamService; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -@Component -public class SysParamCacheInitializer implements ApplicationRunner { - private final SysParamService sysParamService; - - public SysParamCacheInitializer(SysParamService sysParamService) { - this.sysParamService = sysParamService; - } - - @Override - public void run(ApplicationArguments args) { - sysParamService.syncAllToCache(); - } -} diff --git a/backend/src/main/java/com/imeeting/config/WebConfig.java b/backend/src/main/java/com/imeeting/config/WebConfig.java deleted file mode 100644 index 4e16405..0000000 --- a/backend/src/main/java/com/imeeting/config/WebConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.imeeting.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebConfig implements WebMvcConfigurer { - - @Value("${app.upload-path}") - private String uploadPath; - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - String base = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/"; - String audioPath = "file:" + base + "audio/"; - - registry.addResourceHandler("/api/static/audio/**") - .addResourceLocations(audioPath); - } -} diff --git a/backend/src/main/java/com/imeeting/config/WebMvcConfig.java b/backend/src/main/java/com/imeeting/config/WebMvcConfig.java index 30f616d..6254a61 100644 --- a/backend/src/main/java/com/imeeting/config/WebMvcConfig.java +++ b/backend/src/main/java/com/imeeting/config/WebMvcConfig.java @@ -10,10 +10,10 @@ import java.io.File; @Configuration public class WebMvcConfig implements WebMvcConfigurer { - @Value("${app.upload-path}") + @Value("${unisbase.app.upload-path}") private String uploadPath; - @Value("${app.resource-prefix}") + @Value("${unisbase.app.resource-prefix}") private String resourcePrefix; @Override diff --git a/backend/src/main/java/com/imeeting/controller/AuthController.java b/backend/src/main/java/com/imeeting/controller/AuthController.java deleted file mode 100644 index be956cc..0000000 --- a/backend/src/main/java/com/imeeting/controller/AuthController.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.auth.JwtTokenProvider; -import com.imeeting.auth.dto.CaptchaResponse; -import com.imeeting.auth.dto.DeviceCodeRequest; -import com.imeeting.auth.dto.LoginRequest; -import com.imeeting.auth.dto.RefreshRequest; -import com.imeeting.auth.dto.TokenResponse; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.RedisKeys; -import com.imeeting.common.SysParamKeys; -import com.imeeting.service.SysParamService; -import com.imeeting.service.AuthService; -import com.wf.captcha.SpecCaptcha; -import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.web.bind.annotation.*; - -import java.time.Duration; -import java.util.UUID; - -@RestController -@RequestMapping("/auth") -public class AuthController { - private final AuthService authService; - private final StringRedisTemplate stringRedisTemplate; - private final JwtTokenProvider jwtTokenProvider; - private final SysParamService sysParamService; - - @Value("${app.captcha.ttl-seconds:120}") - private long captchaTtlSeconds; - - public AuthController(AuthService authService, StringRedisTemplate stringRedisTemplate, - JwtTokenProvider jwtTokenProvider, SysParamService sysParamService) { - this.authService = authService; - this.stringRedisTemplate = stringRedisTemplate; - this.jwtTokenProvider = jwtTokenProvider; - this.sysParamService = sysParamService; - } - - @GetMapping("/captcha") - public ApiResponse captcha() { - if (!isCaptchaEnabled()) { - return ApiResponse.error("Captcha disabled"); - } - SpecCaptcha captcha = new SpecCaptcha(130, 48, 4); - String code = captcha.text(); - String imageBase64 = captcha.toBase64(); - String captchaId = UUID.randomUUID().toString().replace("-", ""); - - stringRedisTemplate.opsForValue().set(RedisKeys.captchaKey(captchaId), code, Duration.ofSeconds(captchaTtlSeconds)); - return ApiResponse.ok(new CaptchaResponse(captchaId, imageBase64)); - } - - @PostMapping("/device-code") - public ApiResponse deviceCode(@Valid @RequestBody DeviceCodeRequest request) { - LoginRequest loginRequest = new LoginRequest(); - loginRequest.setUsername(request.getUsername()); - loginRequest.setPassword(request.getPassword()); - loginRequest.setCaptchaId(request.getCaptchaId()); - loginRequest.setCaptchaCode(request.getCaptchaCode()); - String deviceCode = authService.createDeviceCode(loginRequest, request.getDeviceName()); - return ApiResponse.ok(deviceCode); - } - - @PostMapping("/login") - public ApiResponse login(@Valid @RequestBody LoginRequest request) { - return ApiResponse.ok(authService.login(request)); - } - - @PostMapping("/refresh") - public ApiResponse refresh(@Valid @RequestBody RefreshRequest request) { - return ApiResponse.ok(authService.refresh(request.getRefreshToken())); - } - - @PostMapping("/switch-tenant") - public ApiResponse switchTenant(@RequestParam Long tenantId, @RequestHeader("Authorization") String authorization) { - String token = authorization.replace("Bearer ", ""); - var claims = jwtTokenProvider.parseToken(token); - Long userId = claims.get("userId", Long.class); - String deviceCode = claims.get("deviceCode", String.class); - return ApiResponse.ok(authService.switchTenant(userId, tenantId, deviceCode)); - } - - @PostMapping("/logout") - public ApiResponse logout(@RequestHeader("Authorization") String authorization) { - String token = authorization.replace("Bearer ", ""); - var claims = jwtTokenProvider.parseToken(token); - Long userId = claims.get("userId", Long.class); - String deviceCode = claims.get("deviceCode", String.class); - authService.logout(userId, deviceCode); - return ApiResponse.ok(null); - } - - private boolean isCaptchaEnabled() { - String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true"); - return Boolean.parseBoolean(value); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/DeviceController.java b/backend/src/main/java/com/imeeting/controller/DeviceController.java deleted file mode 100644 index 6c78616..0000000 --- a/backend/src/main/java/com/imeeting/controller/DeviceController.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.common.ApiResponse; -import com.imeeting.entity.Device; -import com.imeeting.service.DeviceService; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/devices") -public class DeviceController { - private final DeviceService deviceService; - - public DeviceController(DeviceService deviceService) { - this.deviceService = deviceService; - } - - @GetMapping - public ApiResponse> list() { - return ApiResponse.ok(deviceService.list()); - } - - @GetMapping("/{id}") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(deviceService.getById(id)); - } - - @PostMapping - public ApiResponse create(@RequestBody Device device) { - return ApiResponse.ok(deviceService.save(device)); - } - - @PutMapping("/{id}") - public ApiResponse update(@PathVariable Long id, @RequestBody Device device) { - device.setDeviceId(id); - return ApiResponse.ok(deviceService.updateById(device)); - } - - @DeleteMapping("/{id}") - public ApiResponse delete(@PathVariable Long id) { - return ApiResponse.ok(deviceService.removeById(id)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/DictItemController.java b/backend/src/main/java/com/imeeting/controller/DictItemController.java deleted file mode 100644 index a2eb120..0000000 --- a/backend/src/main/java/com/imeeting/controller/DictItemController.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.common.ApiResponse; -import com.imeeting.entity.SysDictItem; -import com.imeeting.service.SysDictItemService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/dict-items") -public class DictItemController { - private final SysDictItemService sysDictItemService; - - public DictItemController(SysDictItemService sysDictItemService) { - this.sysDictItemService = sysDictItemService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys_dict:list')") - public ApiResponse> list(@RequestParam(required = false) String typeCode) { - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - if (typeCode != null && !typeCode.isEmpty()) { - queryWrapper.eq(SysDictItem::getTypeCode, typeCode); - } - queryWrapper.orderByAsc(SysDictItem::getSortOrder); - return ApiResponse.ok(sysDictItemService.list(queryWrapper)); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysDictItemService.getById(id)); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys_dict:create')") - public ApiResponse create(@RequestBody SysDictItem dictItem) { - return ApiResponse.ok(sysDictItemService.save(dictItem)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:update')") - public ApiResponse update(@PathVariable Long id, @RequestBody SysDictItem dictItem) { - dictItem.setDictItemId(id); - return ApiResponse.ok(sysDictItemService.updateById(dictItem)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:delete')") - public ApiResponse delete(@PathVariable Long id) { - return ApiResponse.ok(sysDictItemService.removeById(id)); - } - - @GetMapping("/type/{typeCode}") -// @PreAuthorize("@ss.hasPermi('sys_dict:query')") - public ApiResponse> getByType(@PathVariable String typeCode) { - return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/DictTypeController.java b/backend/src/main/java/com/imeeting/controller/DictTypeController.java deleted file mode 100644 index cc26c5d..0000000 --- a/backend/src/main/java/com/imeeting/controller/DictTypeController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.imeeting.common.ApiResponse; -import com.imeeting.entity.SysDictType; -import com.imeeting.service.SysDictTypeService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/dict-types") -public class DictTypeController { - private final SysDictTypeService sysDictTypeService; - - public DictTypeController(SysDictTypeService sysDictTypeService) { - this.sysDictTypeService = sysDictTypeService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys_dict:list')") - public ApiResponse> list( - @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "10") Integer size, - @RequestParam(required = false) String typeCode, - @RequestParam(required = false) String typeName) { - Page page = new Page<>(current, size); - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - if (typeCode != null && !typeCode.isEmpty()) { - queryWrapper.like(SysDictType::getTypeCode, typeCode); - } - if (typeName != null && !typeName.isEmpty()) { - queryWrapper.like(SysDictType::getTypeName, typeName); - } - queryWrapper.orderByAsc(SysDictType::getTypeCode); - return ApiResponse.ok(sysDictTypeService.page(page, queryWrapper)); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysDictTypeService.getById(id)); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys_dict:create')") - public ApiResponse create(@RequestBody SysDictType dictType) { - return ApiResponse.ok(sysDictTypeService.save(dictType)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:update')") - public ApiResponse update(@PathVariable Long id, @RequestBody SysDictType dictType) { - dictType.setDictTypeId(id); - return ApiResponse.ok(sysDictTypeService.updateById(dictType)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_dict:delete')") - public ApiResponse delete(@PathVariable Long id) { - return ApiResponse.ok(sysDictTypeService.removeById(id)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/PermissionController.java b/backend/src/main/java/com/imeeting/controller/PermissionController.java deleted file mode 100644 index e00c3c5..0000000 --- a/backend/src/main/java/com/imeeting/controller/PermissionController.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.common.ApiResponse; -import com.imeeting.dto.PermissionNode; -import com.imeeting.entity.SysPermission; -import com.imeeting.entity.SysRole; -import com.imeeting.mapper.SysRolePermissionMapper; -import com.imeeting.mapper.SysUserRoleMapper; -import com.imeeting.service.AuthVersionService; -import com.imeeting.service.SysPermissionService; -import com.imeeting.service.SysRoleService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/permissions") -public class PermissionController { - private final SysPermissionService sysPermissionService; - private final SysRolePermissionMapper sysRolePermissionMapper; - private final SysUserRoleMapper sysUserRoleMapper; - private final SysRoleService sysRoleService; - private final AuthVersionService authVersionService; - - public PermissionController(SysPermissionService sysPermissionService, - SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper, - SysRoleService sysRoleService, AuthVersionService authVersionService) { - this.sysPermissionService = sysPermissionService; - this.sysRolePermissionMapper = sysRolePermissionMapper; - this.sysUserRoleMapper = sysUserRoleMapper; - this.sysRoleService = sysRoleService; - this.authVersionService = authVersionService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys:permission:list')") - public ApiResponse> list() { - Long tenantId = getCurrentTenantId(); - // 平台管理员查询所有 - if (Long.valueOf(0).equals(tenantId)) { - return ApiResponse.ok(sysPermissionService.list()); - } - // 非平台管理员只能查询自己拥有的权限 - return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), tenantId)); - } - - @GetMapping("/me") - public ApiResponse> myPermissions() { - return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId())); - } - - @GetMapping("/tree") - @PreAuthorize("@ss.hasPermi('sys:permission:list')") - public ApiResponse> tree() { - Long tenantId = getCurrentTenantId(); - List list; - if (Long.valueOf(0).equals(tenantId)) { - list = sysPermissionService.list(); - } else { - list = sysPermissionService.listByUserId(getCurrentUserId(), tenantId); - } - return ApiResponse.ok(buildTree(list)); - } - - @GetMapping("/tree/me") - public ApiResponse> myTree() { - return ApiResponse.ok(buildTree(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId()))); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:permission:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysPermissionService.getById(id)); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys:permission:create')") - public ApiResponse create(@RequestBody SysPermission perm) { - String error = validateParent(perm); - if (error != null) { - return ApiResponse.error(error); - } - return ApiResponse.ok(sysPermissionService.save(perm)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:permission:update')") - public ApiResponse update(@PathVariable Long id, @RequestBody SysPermission perm) { - List roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id); - perm.setPermId(id); - String error = validateParent(perm); - if (error != null) { - return ApiResponse.error(error); - } - boolean updated = sysPermissionService.updateById(perm); - if (perm.getLevel() != null && perm.getLevel() == 1) { - sysPermissionService.lambdaUpdate() - .set(SysPermission::getParentId, null) - .eq(SysPermission::getPermId, id) - .update(); - } - if (updated) { - invalidateRoleUsers(roleIds); - } - return ApiResponse.ok(updated); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:permission:delete')") - public ApiResponse delete(@PathVariable Long id) { - List roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id); - boolean removed = sysPermissionService.removeById(id); - if (removed) { - invalidateRoleUsers(roleIds); - } - return ApiResponse.ok(removed); - } - - private Long getCurrentUserId() { - org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) { - return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId(); - } - return null; - } - - private Long getCurrentTenantId() { - org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) { - return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId(); - } - return null; - } - - private String validateParent(SysPermission perm) { - if (perm.getLevel() == null) { - return null; - } - if (perm.getPermType() != null && "button".equalsIgnoreCase(perm.getPermType())) { - if (perm.getCode() == null || perm.getCode().trim().isEmpty()) { - return "Code required for button permission"; - } - } - if (perm.getLevel() == 1) { - perm.setParentId(null); - return null; - } - if (perm.getLevel() == 2) { - if (perm.getParentId() == null) { - return "ParentId required for level 2"; - } - SysPermission parent = sysPermissionService.getById(perm.getParentId()); - if (parent == null) { - return "Parent not found"; - } - if (parent.getLevel() == null || parent.getLevel() != 1) { - return "Parent must be level 1"; - } - } - return null; - } - - private List buildTree(List list) { - Map map = new HashMap<>(); - List roots = new ArrayList<>(); - for (SysPermission p : list) { - PermissionNode node = toNode(p); - map.put(node.getPermId(), node); - } - for (PermissionNode node : map.values()) { - Long parentId = node.getParentId(); - if (parentId != null && map.containsKey(parentId)) { - map.get(parentId).getChildren().add(node); - } else { - roots.add(node); - } - } - sortTree(roots); - return roots; - } - - private void sortTree(List nodes) { - nodes.sort(Comparator.comparingInt(n -> n.getSortOrder() == null ? 0 : n.getSortOrder())); - for (PermissionNode node : nodes) { - if (node.getChildren() != null && !node.getChildren().isEmpty()) { - sortTree(node.getChildren()); - } - } - } - - private PermissionNode toNode(SysPermission p) { - PermissionNode node = new PermissionNode(); - node.setPermId(p.getPermId()); - node.setParentId(p.getParentId()); - node.setName(p.getName()); - node.setCode(p.getCode()); - node.setPermType(p.getPermType()); - node.setLevel(p.getLevel()); - node.setPath(p.getPath()); - node.setComponent(p.getComponent()); - node.setIcon(p.getIcon()); - node.setSortOrder(p.getSortOrder()); - node.setIsVisible(p.getIsVisible()); - node.setStatus(p.getStatus()); - node.setDescription(p.getDescription()); - node.setMeta(p.getMeta()); - return node; - } - - private void invalidateRoleUsers(List roleIds) { - if (roleIds == null || roleIds.isEmpty()) { - return; - } - for (Long roleId : roleIds) { - if (roleId == null) { - continue; - } - SysRole role = sysRoleService.getById(roleId); - if (role == null || role.getTenantId() == null) { - continue; - } - List userIds = sysUserRoleMapper.selectUserIdsByRoleId(roleId); - authVersionService.invalidateUsersTenantAuth(userIds, role.getTenantId()); - } - } -} diff --git a/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java b/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java deleted file mode 100644 index 986feea..0000000 --- a/backend/src/main/java/com/imeeting/controller/PlatformConfigController.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.common.ApiResponse; -import com.imeeting.dto.PlatformConfigVO; -import com.imeeting.entity.SysPlatformConfig; -import com.imeeting.service.SysPlatformConfigService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -@RestController -@RequestMapping("/api") -public class PlatformConfigController { - - private final SysPlatformConfigService platformConfigService; - - public PlatformConfigController(SysPlatformConfigService platformConfigService) { - this.platformConfigService = platformConfigService; - } - - /** - * 公开配置接口 (用于登录页、favicon等) - */ - @GetMapping("/open/platform/config") - public ApiResponse getOpenConfig() { - return ApiResponse.ok(platformConfigService.getConfig()); - } - - /** - * 获取管理配置 (需要登录) - */ - @GetMapping("/admin/platform/config") - @PreAuthorize("isAuthenticated()") - public ApiResponse getAdminConfig() { - return ApiResponse.ok(platformConfigService.getConfig()); - } - - /** - * 更新配置 (仅限平台管理员) - */ - @PutMapping("/admin/platform/config") - @PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')") - public ApiResponse updateConfig(@RequestBody SysPlatformConfig config) { - return ApiResponse.ok(platformConfigService.updateConfig(config)); - } - - /** - * 上传资源 (仅限平台管理员) - */ - @PostMapping("/admin/platform/config/upload") - @PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')") - public ApiResponse upload(@RequestParam("file") MultipartFile file) { - return ApiResponse.ok(platformConfigService.uploadAsset(file)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/RoleController.java b/backend/src/main/java/com/imeeting/controller/RoleController.java deleted file mode 100644 index 5a80bbc..0000000 --- a/backend/src/main/java/com/imeeting/controller/RoleController.java +++ /dev/null @@ -1,344 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.annotation.Log; -import com.imeeting.entity.SysRole; -import com.imeeting.entity.SysRolePermission; -import com.imeeting.entity.SysUser; -import com.imeeting.entity.SysUserRole; -import com.imeeting.mapper.SysRolePermissionMapper; -import com.imeeting.mapper.SysUserRoleMapper; -import com.imeeting.service.AuthScopeService; -import com.imeeting.service.AuthVersionService; -import com.imeeting.service.SysRoleService; -import com.imeeting.service.SysUserService; -import com.imeeting.service.SysPermissionService; -import com.imeeting.service.SysTenantUserService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.Set; - -@RestController -@RequestMapping("/api/roles") -public class RoleController { - private final SysRoleService sysRoleService; - private final SysUserService sysUserService; - private final SysRolePermissionMapper sysRolePermissionMapper; - private final SysUserRoleMapper sysUserRoleMapper; - private final SysPermissionService sysPermissionService; - private final AuthScopeService authScopeService; - private final AuthVersionService authVersionService; - private final SysTenantUserService sysTenantUserService; - - public RoleController(SysRoleService sysRoleService, SysUserService sysUserService, - SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper, - SysPermissionService sysPermissionService, - AuthScopeService authScopeService, - AuthVersionService authVersionService, - SysTenantUserService sysTenantUserService) { - this.sysRoleService = sysRoleService; - this.sysUserService = sysUserService; - this.sysRolePermissionMapper = sysRolePermissionMapper; - this.sysUserRoleMapper = sysUserRoleMapper; - this.sysPermissionService = sysPermissionService; - this.authScopeService = authScopeService; - this.authVersionService = authVersionService; - this.sysTenantUserService = sysTenantUserService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys:role:list')") - public ApiResponse> list(@RequestParam(required = false) Long tenantId) { - QueryWrapper wrapper = new QueryWrapper<>(); - - if (authScopeService.isCurrentPlatformAdmin()) { - if (tenantId != null) { - wrapper.eq("tenant_id", tenantId); - } - } else { - Long currentTenantId = getCurrentTenantId(); - wrapper.eq("tenant_id", currentTenantId); - } - - return ApiResponse.ok(sysRoleService.list(wrapper)); - } - - @GetMapping("/{id}/users") - @PreAuthorize("@ss.hasPermi('sys:role:query')") - public ApiResponse> listUsers(@PathVariable Long id) { - SysRole role = sysRoleService.getById(id); - if (role == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(role.getTenantId())) { - return ApiResponse.error("禁止跨租户查看角色用户"); - } - return ApiResponse.ok(sysUserService.listUsersByRoleId(id)); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:role:query')") - public ApiResponse get(@PathVariable Long id) { - SysRole role = sysRoleService.getById(id); - if (role == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(role.getTenantId())) { - return ApiResponse.error("禁止跨租户查看角色"); - } - return ApiResponse.ok(role); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys:role:create')") - @Log(value = "新增角色", type = "角色管理") - public ApiResponse create(@RequestBody SysRole role) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - if (!authScopeService.isCurrentPlatformAdmin()) { - role.setTenantId(currentTenantId); - } else if (role.getTenantId() == null) { - return ApiResponse.error("tenantId required for platform role creation"); - } - return ApiResponse.ok(sysRoleService.save(role)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:role:update')") - @Log(value = "修改角色", type = "角色管理") - public ApiResponse update(@PathVariable Long id, @RequestBody SysRole role) { - SysRole existing = sysRoleService.getById(id); - if (existing == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(existing.getTenantId())) { - return ApiResponse.error("禁止跨租户修改角色"); - } - role.setRoleId(id); - role.setTenantId(existing.getTenantId()); - return ApiResponse.ok(sysRoleService.updateById(role)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:role:delete')") - @Log(value = "删除角色", type = "角色管理") - public ApiResponse delete(@PathVariable Long id) { - SysRole existing = sysRoleService.getById(id); - if (existing == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(existing.getTenantId())) { - return ApiResponse.error("禁止跨租户删除角色"); - } - if ("TENANT_ADMIN".equalsIgnoreCase(existing.getRoleCode()) && !authScopeService.isCurrentPlatformAdmin()) { - return ApiResponse.error("租户管理员角色只能由平台管理员删除"); - } - List userIds = sysUserRoleMapper.selectUserIdsByRoleId(id); - boolean removed = sysRoleService.removeById(id); - if (removed) { - authVersionService.invalidateUsersTenantAuth(userIds, existing.getTenantId()); - } - return ApiResponse.ok(removed); - } - - @GetMapping("/{id}/permissions") - @PreAuthorize("@ss.hasPermi('sys:role:permission:list')") - public ApiResponse> listRolePermissions(@PathVariable Long id) { - SysRole targetRole = sysRoleService.getById(id); - if (targetRole == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(targetRole.getTenantId())) { - return ApiResponse.error("禁止跨租户查看角色权限"); - } - List rows = sysRolePermissionMapper.selectList( - new QueryWrapper().eq("role_id", id) - ); - List permIds = new ArrayList<>(); - for (SysRolePermission row : rows) { - if (row.getPermId() != null) { - permIds.add(row.getPermId()); - } - } - return ApiResponse.ok(permIds); - } - - @PostMapping("/{id}/permissions") - @PreAuthorize("@ss.hasPermi('sys:role:permission:save')") - @Transactional(rollbackFor = Exception.class) - public ApiResponse saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) { - List permIds = payload == null ? null : payload.getPermIds(); - - // 权限越权校验 - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - SysRole targetRole = sysRoleService.getById(id); - if (targetRole == null) { - return ApiResponse.error("角色不存在"); - } - - // 关键校验:只有平台管理员可以修改 TENANT_ADMIN 角色的权限 - if ("TENANT_ADMIN".equalsIgnoreCase(targetRole.getRoleCode())) { - if (!authScopeService.isCurrentPlatformAdmin()) { - return ApiResponse.error("租户管理员角色的权限只能由平台管理员修改"); - } - } - if (!canAccessTenant(targetRole.getTenantId())) { - return ApiResponse.error("禁止跨租户修改角色权限"); - } - - if (!authScopeService.isCurrentPlatformAdmin()) { - List myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId); - - Set myPermIds = myPerms.stream() - .map(com.imeeting.entity.SysPermission::getPermId) - .collect(Collectors.toSet()); - - if (permIds != null) { - for (Long pId : permIds) { - if (!myPermIds.contains(pId)) { - return ApiResponse.error("越权分配权限:" + pId); - } - } - } - } - - sysRolePermissionMapper.delete(new QueryWrapper().eq("role_id", id)); - if (permIds == null || permIds.isEmpty()) { - authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId()); - return ApiResponse.ok(true); - } - for (Long permId : permIds) { - if (permId == null) { - continue; - } - SysRolePermission item = new SysRolePermission(); - item.setRoleId(id); - item.setPermId(permId); - sysRolePermissionMapper.insert(item); - } - authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId()); - return ApiResponse.ok(true); - } - - @PostMapping("/{id}/users") - @PreAuthorize("@ss.hasPermi('sys:role:update')") - @Log(value = "角色关联用户", type = "角色管理") - @Transactional(rollbackFor = Exception.class) - public ApiResponse bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) { - if (payload == null || payload.getUserIds() == null) { - return ApiResponse.ok(true); - } - SysRole role = sysRoleService.getById(id); - if (role == null || role.getRoleId() == null || role.getTenantId() == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(role.getTenantId())) { - return ApiResponse.error("禁止跨租户绑定用户"); - } - - List toInsertUserIds = new ArrayList<>(); - for (Long userId : payload.getUserIds()) { - if (userId == null) { - continue; - } - - // 修复:处理逻辑删除导致的唯一键冲突 - // 执行物理删除,彻底清除旧记录(包括已逻辑删除的) - sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId()); - - // 确保该用户属于该租户 - boolean hasMembership = sysTenantUserService.count( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(com.imeeting.entity.SysTenantUser::getUserId, userId) - .eq(com.imeeting.entity.SysTenantUser::getTenantId, role.getTenantId()) - ) > 0; - if (!hasMembership) { - return ApiResponse.error("用户不属于角色所在租户:" + role.getTenantId()); - } - toInsertUserIds.add(userId); - } - - for (Long userId : toInsertUserIds) { - SysUserRole ur = new SysUserRole(); - ur.setTenantId(role.getTenantId()); - ur.setRoleId(id); - ur.setUserId(userId); - sysUserRoleMapper.insert(ur); - authVersionService.invalidateUserTenantAuth(userId, role.getTenantId()); - } - return ApiResponse.ok(true); - } - - @DeleteMapping("/{id}/users/{userId}") - @PreAuthorize("@ss.hasPermi('sys:role:update')") - @Log(value = "角色取消关联用户", type = "角色管理") - @Transactional(rollbackFor = Exception.class) - public ApiResponse unbindUser(@PathVariable Long id, @PathVariable Long userId) { - SysRole role = sysRoleService.getById(id); - if (role == null || role.getRoleId() == null || role.getTenantId() == null) { - return ApiResponse.error("角色不存在"); - } - if (!canAccessTenant(role.getTenantId())) { - return ApiResponse.error("禁止跨租户解绑用户"); - } - sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId()); - authVersionService.invalidateUserTenantAuth(userId, role.getTenantId()); - return ApiResponse.ok(true); - } - - private Long getCurrentUserId() { - org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) { - return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId(); - } - return null; - } - - private Long getCurrentTenantId() { - org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) { - return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId(); - } - return null; - } - - private boolean canAccessTenant(Long targetTenantId) { - if (targetTenantId == null) { - return false; - } - if (authScopeService.isCurrentPlatformAdmin()) { - return true; - } - Long currentTenantId = getCurrentTenantId(); - return currentTenantId != null && currentTenantId.equals(targetTenantId); - } - - public static class UserBindingPayload { - private List userIds; - public List getUserIds() { return userIds; } - public void setUserIds(List userIds) { this.userIds = userIds; } - } - - public static class PermissionBindingPayload { - private List permIds; - - public List getPermIds() { - return permIds; - } - - public void setPermIds(List permIds) { - this.permIds = permIds; - } - } -} diff --git a/backend/src/main/java/com/imeeting/controller/SysLogController.java b/backend/src/main/java/com/imeeting/controller/SysLogController.java deleted file mode 100644 index 88bdf08..0000000 --- a/backend/src/main/java/com/imeeting/controller/SysLogController.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.imeeting.common.ApiResponse; -import com.imeeting.entity.SysLog; -import com.imeeting.security.LoginUser; -import com.imeeting.service.SysLogService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/logs") -public class SysLogController { - private final SysLogService sysLogService; - - public SysLogController(SysLogService sysLogService) { - this.sysLogService = sysLogService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys_log:list')") - public ApiResponse> list( - @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "10") Integer size, - @RequestParam(required = false) String username, - @RequestParam(required = false) String logType, - @RequestParam(required = false) String operation, - @RequestParam(required = false) Integer status, - @RequestParam(required = false) String startDate, - @RequestParam(required = false) String endDate, - @RequestParam(required = false) String sortField, - @RequestParam(required = false) String sortOrder - ) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - LoginUser loginUser = (LoginUser) auth.getPrincipal(); - - // 判定平台管理员: isPlatformAdmin=true 且 tenantId=0 - boolean isPlatformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId()); - - QueryWrapper query = new QueryWrapper<>(); - // 只有联表查询才需要前缀 'l.' - String prefix = isPlatformAdmin ? "l." : ""; - - if (logType != null && !logType.isEmpty()) { - query.eq(prefix + "log_type", logType); - } - if (username != null && !username.isEmpty()) { - query.like(prefix + "username", username); - } - if (operation != null && !operation.isEmpty()) { - query.like(prefix + "operation", operation); - } - if (status != null) { - query.eq(prefix + "status", status); - } - if (startDate != null && !startDate.isEmpty()) { - query.ge(prefix + "created_at", startDate + " 00:00:00"); - } - if (endDate != null && !endDate.isEmpty()) { - query.le(prefix + "created_at", endDate + " 23:59:59"); - } - - // 动态排序逻辑 - if (sortField != null && !sortField.isEmpty()) { - String column = "created_at"; - if ("duration".equals(sortField)) column = "duration"; - - if ("ascend".equals(sortOrder)) { - query.orderByAsc(prefix + column); - } else { - query.orderByDesc(prefix + column); - } - } else { - query.orderByDesc(prefix + "created_at"); - } - - if (isPlatformAdmin) { - return ApiResponse.ok(sysLogService.selectPageWithTenant(new Page<>(current, size), query)); - } else { - return ApiResponse.ok(sysLogService.page(new Page<>(current, size), query)); - } - } -} diff --git a/backend/src/main/java/com/imeeting/controller/SysOrgController.java b/backend/src/main/java/com/imeeting/controller/SysOrgController.java deleted file mode 100644 index a63efc1..0000000 --- a/backend/src/main/java/com/imeeting/controller/SysOrgController.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.annotation.Log; -import com.imeeting.entity.SysOrg; -import com.imeeting.service.SysOrgService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/orgs") -public class SysOrgController { - private final SysOrgService sysOrgService; - - public SysOrgController(SysOrgService sysOrgService) { - this.sysOrgService = sysOrgService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys:org:list')") - public ApiResponse> list(@RequestParam(required = false) Long tenantId) { - return ApiResponse.ok(sysOrgService.listTree(tenantId)); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:org:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysOrgService.getById(id)); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys:org:create')") - @Log(value = "新增组织", type = "组织管理") - public ApiResponse create(@RequestBody SysOrg org) { - return ApiResponse.ok(sysOrgService.save(org)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:org:update')") - @Log(value = "修改组织", type = "组织管理") - public ApiResponse update(@PathVariable Long id, @RequestBody SysOrg org) { - org.setId(id); - return ApiResponse.ok(sysOrgService.updateById(org)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:org:delete')") - @Log(value = "删除组织", type = "组织管理") - public ApiResponse delete(@PathVariable Long id) { - // Check if has children - long count = sysOrgService.count(new LambdaQueryWrapper().eq(SysOrg::getParentId, id)); - if (count > 0) { - return ApiResponse.error("存在下级组织,无法删除"); - } - return ApiResponse.ok(sysOrgService.removeById(id)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/SysParamController.java b/backend/src/main/java/com/imeeting/controller/SysParamController.java deleted file mode 100644 index 733137c..0000000 --- a/backend/src/main/java/com/imeeting/controller/SysParamController.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.common.ApiResponse; -import com.imeeting.common.PageResult; -import com.imeeting.dto.SysParamQueryDTO; -import com.imeeting.dto.SysParamVO; -import com.imeeting.entity.SysParam; -import com.imeeting.service.SysParamService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/params") -public class SysParamController { - private final SysParamService sysParamService; - - public SysParamController(SysParamService sysParamService) { - this.sysParamService = sysParamService; - } - - @GetMapping("/page") - @PreAuthorize("@ss.hasPermi('sys_param:list')") - public ApiResponse>> page(SysParamQueryDTO query) { - return ApiResponse.ok(sysParamService.page(query)); - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys_param:list')") - public ApiResponse> list() { - return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList())); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_param:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(toVO(sysParamService.getById(id))); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys_param:create')") - public ApiResponse create(@RequestBody SysParam param) { - boolean saved = sysParamService.save(param); - if (saved) { - sysParamService.syncParamToCache(param); - } - return ApiResponse.ok(saved); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_param:update')") - public ApiResponse update(@PathVariable Long id, @RequestBody SysParam param) { - param.setParamId(id); - boolean updated = sysParamService.updateById(param); - if (updated) { - sysParamService.syncParamToCache(param); - } - return ApiResponse.ok(updated); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_param:delete')") - public ApiResponse delete(@PathVariable Long id) { - SysParam param = sysParamService.getById(id); - boolean removed = sysParamService.removeById(id); - if (removed && param != null) { - sysParamService.deleteParamCache(param.getParamKey()); - } - return ApiResponse.ok(removed); - } - - @GetMapping("/value") - public ApiResponse getValue(@RequestParam("key") String key, - @RequestParam(value = "defaultValue", required = false) String defaultValue) { - return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue)); - } - - private SysParamVO toVO(SysParam entity) { - if (entity == null) return null; - SysParamVO vo = new SysParamVO(); - vo.setParamId(entity.getParamId()); - vo.setParamKey(entity.getParamKey()); - vo.setParamValue(entity.getParamValue()); - vo.setParamType(entity.getParamType()); - vo.setIsSystem(entity.getIsSystem()); - vo.setDescription(entity.getDescription()); - vo.setStatus(entity.getStatus()); - vo.setCreatedAt(entity.getCreatedAt()); - vo.setUpdatedAt(entity.getUpdatedAt()); - return vo; - } -} diff --git a/backend/src/main/java/com/imeeting/controller/SysTenantController.java b/backend/src/main/java/com/imeeting/controller/SysTenantController.java deleted file mode 100644 index 79e4562..0000000 --- a/backend/src/main/java/com/imeeting/controller/SysTenantController.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.imeeting.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.annotation.Log; -import com.imeeting.entity.SysTenant; -import com.imeeting.service.SysTenantService; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/tenants") -public class SysTenantController { - private final SysTenantService sysTenantService; - - public SysTenantController(SysTenantService sysTenantService) { - this.sysTenantService = sysTenantService; - } - - @GetMapping - @PreAuthorize("@ss.hasPermi('sys_tenant:list')") - public ApiResponse> list( - @RequestParam(defaultValue = "1") Integer current, - @RequestParam(defaultValue = "10") Integer size, - @RequestParam(required = false) String name, - @RequestParam(required = false) String code - ) { - LambdaQueryWrapper query = new LambdaQueryWrapper<>(); - if (name != null && !name.isEmpty()) { - query.like(SysTenant::getTenantName, name); - } - if (code != null && !code.isEmpty()) { - query.like(SysTenant::getTenantCode, code); - } - query.orderByDesc(SysTenant::getCreatedAt); - return ApiResponse.ok(sysTenantService.page(new Page<>(current, size), query)); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_tenant:query')") - public ApiResponse get(@PathVariable Long id) { - return ApiResponse.ok(sysTenantService.getById(id)); - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys_tenant:create')") - @Log(value = "新增租户", type = "租户管理") - public ApiResponse create(@RequestBody com.imeeting.dto.CreateTenantDTO tenantDto) { - return ApiResponse.ok(sysTenantService.createTenantWithAdmin(tenantDto)); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_tenant:update')") - @Log(value = "修改租户", type = "租户管理") - public ApiResponse update(@PathVariable Long id, @RequestBody SysTenant tenant) { - tenant.setId(id); - return ApiResponse.ok(sysTenantService.updateById(tenant)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys_tenant:delete')") - @Log(value = "删除租户", type = "租户管理") - public ApiResponse delete(@PathVariable Long id) { - return ApiResponse.ok(sysTenantService.removeById(id)); - } -} diff --git a/backend/src/main/java/com/imeeting/controller/UserController.java b/backend/src/main/java/com/imeeting/controller/UserController.java deleted file mode 100644 index f4594e9..0000000 --- a/backend/src/main/java/com/imeeting/controller/UserController.java +++ /dev/null @@ -1,371 +0,0 @@ -package com.imeeting.controller; - -import com.imeeting.common.ApiResponse; -import com.imeeting.dto.PasswordUpdateDTO; -import com.imeeting.dto.UserProfile; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.security.LoginUser; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.imeeting.entity.SysUser; -import com.imeeting.entity.SysUserRole; -import com.imeeting.mapper.SysUserRoleMapper; -import com.imeeting.service.AuthScopeService; -import com.imeeting.service.AuthVersionService; -import com.imeeting.service.SysUserService; -import com.imeeting.common.annotation.Log; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; - -@RestController -@RequestMapping("/api/users") -public class UserController { - private final SysUserService sysUserService; - private final PasswordEncoder passwordEncoder; - private final SysUserRoleMapper sysUserRoleMapper; - private final com.imeeting.service.SysTenantUserService sysTenantUserService; - private final com.imeeting.service.SysRoleService sysRoleService; - private final AuthScopeService authScopeService; - private final AuthVersionService authVersionService; - - public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder, - SysUserRoleMapper sysUserRoleMapper, - com.imeeting.service.SysTenantUserService sysTenantUserService, - com.imeeting.service.SysRoleService sysRoleService, - AuthScopeService authScopeService, - AuthVersionService authVersionService) { - this.sysUserService = sysUserService; - this.passwordEncoder = passwordEncoder; - this.sysUserRoleMapper = sysUserRoleMapper; - this.sysTenantUserService = sysTenantUserService; - this.sysRoleService = sysRoleService; - this.authScopeService = authScopeService; - this.authVersionService = authVersionService; - } - - @GetMapping -// @PreAuthorize("@ss.hasPermi('sys:user:list')") - public ApiResponse> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) { - Long currentTenantId = getCurrentTenantId(); - List users; - Long targetTenantId = null; - - if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) { - users = sysUserService.list(); - } else { - targetTenantId = tenantId != null ? tenantId : currentTenantId; - if (targetTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - users = sysUserService.listUsersByTenant(targetTenantId, orgId); - } - - if (users != null && !users.isEmpty()) { - for (SysUser user : users) { - // 加载租户关系 - user.setMemberships(sysTenantUserService.listByUserId(user.getUserId())); - - // 加载角色信息 - QueryWrapper roleQuery = new QueryWrapper().eq("user_id", user.getUserId()); - if (targetTenantId != null) { - roleQuery.eq("tenant_id", targetTenantId); - } - List userRoles = sysUserRoleMapper.selectList(roleQuery); - if (userRoles != null && !userRoles.isEmpty()) { - List roleIds = userRoles.stream() - .map(SysUserRole::getRoleId) - .collect(java.util.stream.Collectors.toList()); - user.setRoles(sysRoleService.listByIds(roleIds)); - } - } - } - return ApiResponse.ok(users); - } - - @GetMapping("/me") - public ApiResponse me() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser)) { - return ApiResponse.error("Unauthorized"); - } - LoginUser loginUser = (LoginUser) authentication.getPrincipal(); - Long userId = loginUser.getUserId(); - - SysUser user = sysUserService.getByIdIgnoreTenant(userId); - if (user == null) { - return ApiResponse.error("User not found"); - } - UserProfile profile = new UserProfile(); - profile.setUserId(user.getUserId()); - profile.setUsername(user.getUsername()); - profile.setDisplayName(user.getDisplayName()); - profile.setEmail(user.getEmail()); - profile.setPhone(user.getPhone()); - profile.setStatus(user.getStatus()); - profile.setAdmin(userId == 1L); - profile.setIsPlatformAdmin(user.getIsPlatformAdmin()); - profile.setIsTenantAdmin(loginUser.getIsTenantAdmin()); - profile.setPwdResetRequired(user.getPwdResetRequired()); - return ApiResponse.ok(profile); - } - - @GetMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:user:query')") - public ApiResponse get(@PathVariable Long id) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) { - return ApiResponse.error("禁止跨租户查看用户"); - } - SysUser user = sysUserService.getByIdIgnoreTenant(id); - if (user != null) { - user.setMemberships(sysTenantUserService.listByUserId(id)); - } - return ApiResponse.ok(user); - } - - private Long getCurrentTenantId() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - return ((LoginUser) auth.getPrincipal()).getTenantId(); - } - return null; - } - - @PostMapping - @PreAuthorize("@ss.hasPermi('sys:user:create')") - @Log(value = "新增用户", type = "用户管理") - public ApiResponse create(@RequestBody SysUser user) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - // 非平台管理员强制设置为当前租户 - if (!Long.valueOf(0).equals(currentTenantId)) { - if (user.getMemberships() != null && !user.getMemberships().isEmpty()) { - user.getMemberships().forEach(m -> m.setTenantId(currentTenantId)); - } else { - // 如果没传身份,补齐当前租户身份 - List memberships = new java.util.ArrayList<>(); - com.imeeting.entity.SysTenantUser m = new com.imeeting.entity.SysTenantUser(); - m.setTenantId(currentTenantId); - memberships.add(m); - user.setMemberships(memberships); - } - } - - if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { - user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash())); - } - boolean saved = sysUserService.save(user); - if (saved) { - sysTenantUserService.syncMemberships(user.getUserId(), user.getMemberships()); - } - return ApiResponse.ok(saved); - } - - @PutMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:user:update')") - @Log(value = "修改用户", type = "用户管理") - public ApiResponse update(@PathVariable Long id, @RequestBody SysUser user) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - user.setUserId(id); - if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) { - return ApiResponse.error("禁止跨租户修改用户"); - } - - // 非平台管理员强制约束租户身份 - if (!Long.valueOf(0).equals(currentTenantId)) { - if (user.getMemberships() != null) { - user.getMemberships().forEach(m -> m.setTenantId(currentTenantId)); - } - } - - if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) { - user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash())); - } - boolean updated = sysUserService.updateById(user); - if (updated) { - sysTenantUserService.syncMemberships(id, user.getMemberships()); - } - return ApiResponse.ok(updated); - } - - @DeleteMapping("/{id}") - @PreAuthorize("@ss.hasPermi('sys:user:delete')") - @Log(value = "删除用户", type = "用户管理") - public ApiResponse delete(@PathVariable Long id) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) { - return ApiResponse.error("禁止跨租户删除用户"); - } - return ApiResponse.ok(sysUserService.removeById(id)); - } - - @PutMapping("/profile") - public ApiResponse updateProfile(@RequestBody SysUser user) { - Long userId = getCurrentUserId(); - SysUser existing = sysUserService.getByIdIgnoreTenant(userId); - if (existing == null) return ApiResponse.error("用户不存在"); - - existing.setDisplayName(user.getDisplayName()); - existing.setEmail(user.getEmail()); - existing.setPhone(user.getPhone()); - return ApiResponse.ok(sysUserService.updateById(existing)); - } - - @PutMapping("/password") - public ApiResponse updatePassword(@RequestBody PasswordUpdateDTO dto) { - Long userId = getCurrentUserId(); - SysUser user = sysUserService.getByIdIgnoreTenant(userId); - if (user == null) return ApiResponse.error("用户不存在"); - - if (!passwordEncoder.matches(dto.getOldPassword(), user.getPasswordHash())) { - return ApiResponse.error("旧密码不正确"); - } - - user.setPasswordHash(passwordEncoder.encode(dto.getNewPassword())); - user.setPwdResetRequired(0); // 重置标志位 - return ApiResponse.ok(sysUserService.updateById(user)); - } - - private Long getCurrentUserId() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.getPrincipal() instanceof LoginUser) { - return ((LoginUser) auth.getPrincipal()).getUserId(); - } - return null; - } - - @GetMapping("/{id}/roles") - @PreAuthorize("@ss.hasPermi('sys:user:role:list')") - public ApiResponse> listUserRoles(@PathVariable Long id) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) { - return ApiResponse.error("禁止跨租户查看用户角色"); - } - QueryWrapper query = new QueryWrapper().eq("user_id", id); - if (!authScopeService.isCurrentPlatformAdmin()) { - query.eq("tenant_id", currentTenantId); - } - List rows = sysUserRoleMapper.selectList(query); - List roleIds = new ArrayList<>(); - for (SysUserRole row : rows) { - if (row.getRoleId() != null) { - roleIds.add(row.getRoleId()); - } - } - return ApiResponse.ok(roleIds); - } - - @PostMapping("/{id}/roles") - @PreAuthorize("@ss.hasPermi('sys:user:role:save')") - @Transactional(rollbackFor = Exception.class) - public ApiResponse saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) { - Long currentTenantId = getCurrentTenantId(); - if (currentTenantId == null) { - return ApiResponse.error("Tenant ID required"); - } - if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) { - return ApiResponse.error("禁止跨租户分配角色"); - } - - List roleIds = payload == null ? null : payload.getRoleIds(); - - List rolesToBind = new ArrayList<>(); - if (roleIds != null) { - for (Long roleId : roleIds) { - if (roleId == null) { - continue; - } - com.imeeting.entity.SysRole role = sysRoleService.getById(roleId); - if (role == null || role.getRoleId() == null || role.getTenantId() == null) { - return ApiResponse.error("角色不存在:" + roleId); - } - Long roleTenantId = role.getTenantId(); - if (!authScopeService.isCurrentPlatformAdmin() && !currentTenantId.equals(roleTenantId)) { - return ApiResponse.error("禁止跨租户分配角色:" + roleId); - } - boolean hasMembership = sysTenantUserService.count( - new LambdaQueryWrapper() - .eq(com.imeeting.entity.SysTenantUser::getUserId, id) - .eq(com.imeeting.entity.SysTenantUser::getTenantId, roleTenantId) - ) > 0; - if (!hasMembership) { - return ApiResponse.error("用户不属于角色所在租户:" + roleTenantId); - } - rolesToBind.add(role); - } - } - - QueryWrapper scopeQuery = new QueryWrapper().eq("user_id", id); - if (!authScopeService.isCurrentPlatformAdmin()) { - scopeQuery.eq("tenant_id", currentTenantId); - } - List existingRows = sysUserRoleMapper.selectList(scopeQuery); - java.util.Set affectedTenantIds = new java.util.HashSet<>(); - for (SysUserRole row : existingRows) { - if (row.getTenantId() != null) { - affectedTenantIds.add(row.getTenantId()); - } - } - for (com.imeeting.entity.SysRole role : rolesToBind) { - if (role.getTenantId() != null) { - affectedTenantIds.add(role.getTenantId()); - } - } - - sysUserRoleMapper.delete(scopeQuery); - for (com.imeeting.entity.SysRole role : rolesToBind) { - SysUserRole item = new SysUserRole(); - item.setTenantId(role.getTenantId()); - item.setUserId(id); - item.setRoleId(role.getRoleId()); - sysUserRoleMapper.insert(item); - } - for (Long tenantId : affectedTenantIds) { - authVersionService.invalidateUserTenantAuth(id, tenantId); - } - return ApiResponse.ok(true); - } - - private boolean isUserInTenant(Long userId, Long tenantId) { - if (userId == null || tenantId == null) { - return false; - } - return sysTenantUserService.count( - new LambdaQueryWrapper() - .eq(com.imeeting.entity.SysTenantUser::getUserId, userId) - .eq(com.imeeting.entity.SysTenantUser::getTenantId, tenantId) - ) > 0; - } - - public static class RoleBindingPayload { - private List roleIds; - - public List getRoleIds() { - return roleIds; - } - - public void setRoleIds(List roleIds) { - this.roleIds = roleIds; - } - } -} diff --git a/backend/src/main/java/com/imeeting/controller/biz/AiModelController.java b/backend/src/main/java/com/imeeting/controller/biz/AiModelController.java index ef47514..431efcb 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/AiModelController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/AiModelController.java @@ -1,11 +1,13 @@ package com.imeeting.controller.biz; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.AiModelDTO; import com.imeeting.dto.biz.AiModelVO; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.AiModelService; +import com.unisbase.common.ApiResponse; +import com.unisbase.dto.PageResult; +import com.unisbase.security.LoginUser; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/com/imeeting/controller/biz/DashboardController.java b/backend/src/main/java/com/imeeting/controller/biz/DashboardController.java index 42645f8..a4a022f 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/DashboardController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/DashboardController.java @@ -1,9 +1,11 @@ package com.imeeting.controller.biz; -import com.imeeting.common.ApiResponse; + import com.imeeting.dto.biz.MeetingVO; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.MeetingService; +import com.unisbase.common.ApiResponse; +import com.unisbase.security.LoginUser; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; diff --git a/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java b/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java index a7cd3dd..91f9982 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java @@ -2,13 +2,15 @@ package com.imeeting.controller.biz; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.HotWordDTO; import com.imeeting.dto.biz.HotWordVO; import com.imeeting.entity.biz.HotWord; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.HotWordService; +import com.unisbase.common.ApiResponse; +import com.unisbase.dto.PageResult; +import com.unisbase.security.LoginUser; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java index 3286dbd..d9d3e41 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingController.java @@ -1,7 +1,6 @@ package com.imeeting.controller.biz; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.PageResult; + import com.imeeting.common.RedisKeys; import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingTranscriptVO; @@ -10,11 +9,14 @@ import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.Meeting; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.PromptTemplateService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.unisbase.common.ApiResponse; +import com.unisbase.dto.PageResult; +import com.unisbase.security.LoginUser; import org.apache.fontbox.ttf.TrueTypeCollection; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -67,17 +69,20 @@ public class MeetingController { private final PromptTemplateService promptTemplateService; private final StringRedisTemplate redisTemplate; private final String uploadPath; + private final String resourcePrefix; public MeetingController(MeetingService meetingService, AiTaskService aiTaskService, PromptTemplateService promptTemplateService, StringRedisTemplate redisTemplate, - @Value("${app.upload-path}") String uploadPath) { + @Value("${unisbase.app.upload-path}") String uploadPath, + @Value("${unisbase.app.resource-prefix}") String resourcePrefix) { this.meetingService = meetingService; this.aiTaskService = aiTaskService; this.promptTemplateService = promptTemplateService; this.redisTemplate = redisTemplate; this.uploadPath = uploadPath; + this.resourcePrefix = resourcePrefix; } @GetMapping("/{id}/progress") @@ -120,8 +125,8 @@ public class MeetingController { String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); file.transferTo(new File(uploadDir + fileName)); - - return ApiResponse.ok("/api/static/audio/" + fileName); + String baseResourcePrefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/"; + return ApiResponse.ok(baseResourcePrefix+"audio/" + fileName); } @PostMapping diff --git a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java index 4ac0c41..7d1c7a5 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/PromptTemplateController.java @@ -1,12 +1,14 @@ package com.imeeting.controller.biz; -import com.imeeting.common.ApiResponse; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.PromptTemplateDTO; import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.PromptTemplate; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.PromptTemplateService; +import com.unisbase.common.ApiResponse; +import com.unisbase.dto.PageResult; +import com.unisbase.security.LoginUser; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/com/imeeting/controller/biz/SpeakerController.java b/backend/src/main/java/com/imeeting/controller/biz/SpeakerController.java index d7ca022..26b9389 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/SpeakerController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/SpeakerController.java @@ -1,12 +1,14 @@ package com.imeeting.controller.biz; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.common.ApiResponse; + import com.imeeting.dto.biz.SpeakerRegisterDTO; import com.imeeting.dto.biz.SpeakerVO; import com.imeeting.entity.biz.Speaker; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.SpeakerService; +import com.unisbase.common.ApiResponse; +import com.unisbase.security.LoginUser; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; diff --git a/backend/src/main/java/com/imeeting/dto/CreateTenantDTO.java b/backend/src/main/java/com/imeeting/dto/CreateTenantDTO.java deleted file mode 100644 index 0853c9a..0000000 --- a/backend/src/main/java/com/imeeting/dto/CreateTenantDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; -import java.time.LocalDateTime; - -@Data -public class CreateTenantDTO { - private String tenantCode; - private String tenantName; - private String contactName; - private String contactPhone; - private String remark; - private LocalDateTime expireTime; -} diff --git a/backend/src/main/java/com/imeeting/dto/PasswordUpdateDTO.java b/backend/src/main/java/com/imeeting/dto/PasswordUpdateDTO.java deleted file mode 100644 index cdba6fd..0000000 --- a/backend/src/main/java/com/imeeting/dto/PasswordUpdateDTO.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; - -@Data -public class PasswordUpdateDTO { - private String oldPassword; - private String newPassword; -} diff --git a/backend/src/main/java/com/imeeting/dto/PermissionNode.java b/backend/src/main/java/com/imeeting/dto/PermissionNode.java deleted file mode 100644 index 4ef177f..0000000 --- a/backend/src/main/java/com/imeeting/dto/PermissionNode.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class PermissionNode { - private Long permId; - private Long parentId; - private String name; - private String code; - private String permType; - private Integer level; - private String path; - private String component; - private String icon; - private Integer sortOrder; - private Integer isVisible; - private Integer status; - private String description; - private String meta; - private List children = new ArrayList<>(); -} diff --git a/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java b/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java deleted file mode 100644 index 68d08e5..0000000 --- a/backend/src/main/java/com/imeeting/dto/PlatformConfigVO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; -import java.time.LocalDateTime; - -@Data -public class PlatformConfigVO { - private String projectName; - private String logoUrl; - private String iconUrl; - private String loginBgUrl; - private String icpInfo; - private String copyrightInfo; - private String systemDescription; -} diff --git a/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java b/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java deleted file mode 100644 index 1a098c5..0000000 --- a/backend/src/main/java/com/imeeting/dto/SysParamQueryDTO.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; - -@Data -public class SysParamQueryDTO { - private String paramKey; - private String paramType; - private String description; - private Integer pageNum = 1; - private Integer pageSize = 10; -} diff --git a/backend/src/main/java/com/imeeting/dto/SysParamVO.java b/backend/src/main/java/com/imeeting/dto/SysParamVO.java deleted file mode 100644 index b00587a..0000000 --- a/backend/src/main/java/com/imeeting/dto/SysParamVO.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.imeeting.dto; - -import lombok.Data; -import java.time.LocalDateTime; - -@Data -public class SysParamVO { - private Long paramId; - private String paramKey; - private String paramValue; - private String paramType; - private Integer isSystem; - private String description; - private Integer status; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/imeeting/dto/UserProfile.java b/backend/src/main/java/com/imeeting/dto/UserProfile.java deleted file mode 100644 index 5211756..0000000 --- a/backend/src/main/java/com/imeeting/dto/UserProfile.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.imeeting.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -@Data -public class UserProfile { - private Long userId; - private String username; - private String displayName; - private String email; - private String phone; - private Integer status; - @JsonProperty("isAdmin") - private boolean isAdmin; - private Boolean isPlatformAdmin; - private Boolean isTenantAdmin; - private Integer pwdResetRequired; -} diff --git a/backend/src/main/java/com/imeeting/entity/BaseEntity.java b/backend/src/main/java/com/imeeting/entity/BaseEntity.java deleted file mode 100644 index 31ab02c..0000000 --- a/backend/src/main/java/com/imeeting/entity/BaseEntity.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.FieldFill; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableLogic; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -public class BaseEntity { - @TableField(fill = FieldFill.INSERT) - private Long tenantId; - - private Integer status; - - @TableLogic(value = "0", delval = "1") - private Integer isDeleted; - - @TableField(fill = FieldFill.INSERT) - private LocalDateTime createdAt; - - @TableField(fill = FieldFill.INSERT_UPDATE) - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/imeeting/entity/Device.java b/backend/src/main/java/com/imeeting/entity/Device.java deleted file mode 100644 index 86bc039..0000000 --- a/backend/src/main/java/com/imeeting/entity/Device.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("device_info") -public class Device extends BaseEntity { - @TableId(value = "device_id", type = IdType.AUTO) - private Long deviceId; - private Long userId; - private String deviceCode; - private String deviceName; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysDictItem.java b/backend/src/main/java/com/imeeting/entity/SysDictItem.java deleted file mode 100644 index 8c42b16..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysDictItem.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_dict_item") -public class SysDictItem extends BaseEntity { - @TableId(value = "dict_item_id", type = IdType.AUTO) - private Long dictItemId; - private String typeCode; - private String itemLabel; - private String itemValue; - private Integer sortOrder; - private String remark; - - @TableField(exist = false) - private Long tenantId; - @TableField(exist = false) - private Integer isDeleted; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysDictType.java b/backend/src/main/java/com/imeeting/entity/SysDictType.java deleted file mode 100644 index c4a285d..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysDictType.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_dict_type") -public class SysDictType extends BaseEntity { - @TableId(value = "dict_type_id", type = IdType.AUTO) - private Long dictTypeId; - private String typeCode; - private String typeName; - private String remark; - - @TableField(exist = false) - private Long tenantId; - @TableField(exist = false) - private Integer isDeleted; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysLog.java b/backend/src/main/java/com/imeeting/entity/SysLog.java deleted file mode 100644 index a0c31d7..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysLog.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import java.time.LocalDateTime; - -@Data -@TableName("sys_log") -public class SysLog { - @TableId(type = IdType.AUTO) - private Long id; - private Long tenantId; - private Long userId; - private String username; - private String logType; // LOGIN, OPERATION - private String operation; - private String method; - private String params; - private Integer status; - private String ip; - private Long duration; - private LocalDateTime createdAt; - - @TableField(exist = false) - private String tenantName; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysOrg.java b/backend/src/main/java/com/imeeting/entity/SysOrg.java deleted file mode 100644 index 603048d..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysOrg.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_org") -public class SysOrg extends BaseEntity { - @TableId(type = IdType.AUTO) - private Long id; - - private Long tenantId; - private Long parentId; - private String orgName; - private String orgCode; - private String orgPath; - private Integer sortOrder; - -} diff --git a/backend/src/main/java/com/imeeting/entity/SysParam.java b/backend/src/main/java/com/imeeting/entity/SysParam.java deleted file mode 100644 index b353014..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysParam.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_param") -public class SysParam extends BaseEntity { - @TableId(value = "param_id", type = IdType.AUTO) - private Long paramId; - private String paramKey; - private String paramValue; - private String paramType; - private Integer isSystem; - private String description; - - @TableField(exist = false) - private Long tenantId; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysPermission.java b/backend/src/main/java/com/imeeting/entity/SysPermission.java deleted file mode 100644 index a5b0d4c..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysPermission.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.*; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("sys_permission") -public class SysPermission { - @TableId(value = "perm_id", type = IdType.AUTO) - private Long permId; - private Long parentId; - private String name; - private String code; - private String permType; - private Integer level; - private String path; - private String component; - private String icon; - private Integer sortOrder; - private Integer isVisible; - private String description; - private String meta; - private Integer status; - - @TableLogic - private Boolean isDeleted; - - @TableField(fill = FieldFill.INSERT) - private LocalDateTime createdAt; - - @TableField(fill = FieldFill.INSERT_UPDATE) - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java b/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java deleted file mode 100644 index 4eaaad3..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysPlatformConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.annotation.TableField; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data - -@TableName("sys_platform_config") -public class SysPlatformConfig { - @TableId - private Long id; - private String projectName; - private String logoUrl; - private String iconUrl; - private String loginBgUrl; - private String icpInfo; - private String copyrightInfo; - private String systemDescription; - -} diff --git a/backend/src/main/java/com/imeeting/entity/SysRole.java b/backend/src/main/java/com/imeeting/entity/SysRole.java deleted file mode 100644 index fba0134..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysRole.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("sys_role") -public class SysRole extends BaseEntity { - @TableId(value = "role_id", type = IdType.AUTO) - private Long roleId; - private String roleCode; - private String roleName; - private String remark; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysRolePermission.java b/backend/src/main/java/com/imeeting/entity/SysRolePermission.java deleted file mode 100644 index 09fd594..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysRolePermission.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.FieldFill; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("sys_role_permission") -public class SysRolePermission { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - private Long roleId; - private Long permId; - - @TableField(fill = FieldFill.INSERT) - private LocalDateTime createdAt; - - @TableField(fill = FieldFill.INSERT_UPDATE) - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysTenant.java b/backend/src/main/java/com/imeeting/entity/SysTenant.java deleted file mode 100644 index c6ff789..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysTenant.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_tenant") -public class SysTenant extends BaseEntity { - @TableId(type = IdType.AUTO) - private Long id; - private String tenantCode; - private String tenantName; - private LocalDateTime expireTime; - private String contactName; - private String contactPhone; - private String remark; - - @TableField(exist = false) - private Long tenantId; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysTenantUser.java b/backend/src/main/java/com/imeeting/entity/SysTenantUser.java deleted file mode 100644 index 2804114..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysTenantUser.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_tenant_user") -public class SysTenantUser extends BaseEntity { - @TableId(type = IdType.AUTO) - private Long id; - private Long userId; - private Long tenantId; - private Long orgId; - - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private String orgName; - - @com.baomidou.mybatisplus.annotation.TableLogic(value = "0", delval = "0") - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private Integer isDeleted; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysUser.java b/backend/src/main/java/com/imeeting/entity/SysUser.java deleted file mode 100644 index 8ee5cd8..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysUser.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("sys_user") -public class SysUser extends BaseEntity { - @TableId(value = "user_id", type = IdType.AUTO) - private Long userId; - private String username; - private String displayName; - private String email; - private String phone; - private String passwordHash; - private Integer pwdResetRequired; - - private Boolean isPlatformAdmin; - - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private Long tenantId; - - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private Long orgId; - - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private java.util.List memberships; - - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private java.util.List roles; -} diff --git a/backend/src/main/java/com/imeeting/entity/SysUserRole.java b/backend/src/main/java/com/imeeting/entity/SysUserRole.java deleted file mode 100644 index d417af3..0000000 --- a/backend/src/main/java/com/imeeting/entity/SysUserRole.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imeeting.entity; - -import com.baomidou.mybatisplus.annotation.FieldFill; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableLogic; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("sys_user_role") -public class SysUserRole { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - private Long tenantId; - private Long userId; - private Long roleId; - @TableLogic(value = "0", delval = "1") - private Integer isDeleted; - - @TableField(fill = FieldFill.INSERT) - private LocalDateTime createdAt; - - @TableField(fill = FieldFill.INSERT_UPDATE) - private LocalDateTime updatedAt; -} diff --git a/backend/src/main/java/com/imeeting/entity/biz/AsrModel.java b/backend/src/main/java/com/imeeting/entity/biz/AsrModel.java index 3aafd38..a562027 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/AsrModel.java +++ b/backend/src/main/java/com/imeeting/entity/biz/AsrModel.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/entity/biz/HotWord.java b/backend/src/main/java/com/imeeting/entity/biz/HotWord.java index 88a3efb..6a819f3 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/HotWord.java +++ b/backend/src/main/java/com/imeeting/entity/biz/HotWord.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/entity/biz/LlmModel.java b/backend/src/main/java/com/imeeting/entity/biz/LlmModel.java index ea375ca..b11486f 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/LlmModel.java +++ b/backend/src/main/java/com/imeeting/entity/biz/LlmModel.java @@ -3,7 +3,7 @@ package com.imeeting.entity.biz; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java index 32799dc..bd14bb1 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/Meeting.java +++ b/backend/src/main/java/com/imeeting/entity/biz/Meeting.java @@ -4,13 +4,11 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; -import java.util.List; @Data @EqualsAndHashCode(callSuper = true) diff --git a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java index 37fd5df..8beaa33 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java +++ b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java @@ -3,7 +3,7 @@ package com.imeeting.entity.biz; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java index 3655e41..6ed7ab7 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java +++ b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplateUserConfig.java @@ -3,7 +3,7 @@ package com.imeeting.entity.biz; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/entity/biz/Speaker.java b/backend/src/main/java/com/imeeting/entity/biz/Speaker.java index 3f67416..cee54af 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/Speaker.java +++ b/backend/src/main/java/com/imeeting/entity/biz/Speaker.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.imeeting.entity.BaseEntity; +import com.unisbase.entity.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/backend/src/main/java/com/imeeting/mapper/DeviceMapper.java b/backend/src/main/java/com/imeeting/mapper/DeviceMapper.java deleted file mode 100644 index 0ef7005..0000000 --- a/backend/src/main/java/com/imeeting/mapper/DeviceMapper.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.Device; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface DeviceMapper extends BaseMapper {} diff --git a/backend/src/main/java/com/imeeting/mapper/SysDictItemMapper.java b/backend/src/main/java/com/imeeting/mapper/SysDictItemMapper.java deleted file mode 100644 index 7cb3786..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysDictItemMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysDictItem; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysDictItemMapper extends BaseMapper { -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysDictTypeMapper.java b/backend/src/main/java/com/imeeting/mapper/SysDictTypeMapper.java deleted file mode 100644 index 0dad502..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysDictTypeMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysDictType; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysDictTypeMapper extends BaseMapper { -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java b/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java deleted file mode 100644 index f175016..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysLogMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.conditions.Wrapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.core.toolkit.Constants; -import com.imeeting.entity.SysLog; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; - -@Mapper -public interface SysLogMapper extends BaseMapper { - - @Override - @InterceptorIgnore(tenantLine = "true") - int insert(SysLog entity); - - @Select("SELECT l.*, t.tenant_name FROM sys_log l " + - "LEFT JOIN sys_tenant t ON l.tenant_id = t.id " + - "${ew.customSqlSegment}") - IPage selectPageWithTenant(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysOrgMapper.java b/backend/src/main/java/com/imeeting/mapper/SysOrgMapper.java deleted file mode 100644 index ffb6951..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysOrgMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysOrg; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysOrgMapper extends BaseMapper { -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysParamMapper.java b/backend/src/main/java/com/imeeting/mapper/SysParamMapper.java deleted file mode 100644 index 46722c3..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysParamMapper.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysParam; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysParamMapper extends BaseMapper {} diff --git a/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java b/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java deleted file mode 100644 index d9c1490..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysPermissionMapper.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysPermission; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Select; - -import java.util.List; - -@Mapper -public interface SysPermissionMapper extends BaseMapper { - @com.baomidou.mybatisplus.annotation.InterceptorIgnore(tenantLine = "true") - @Select(""" - SELECT DISTINCT p.* - FROM sys_permission p - JOIN sys_role_permission rp ON rp.perm_id = p.perm_id - JOIN sys_role r ON r.role_id = rp.role_id - JOIN sys_user_role ur ON ur.role_id = r.role_id - WHERE p.is_deleted = 0 - AND r.is_deleted = 0 - AND ur.is_deleted = 0 - AND ur.user_id = #{userId} - AND r.tenant_id = #{tenantId} - AND (ur.tenant_id = #{tenantId} OR ur.tenant_id IS NULL) - """) - List selectByUserId(@Param("userId") Long userId, @Param("tenantId") Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java b/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java deleted file mode 100644 index 1c6874f..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysPlatformConfigMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysPlatformConfig; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysPlatformConfigMapper extends BaseMapper { -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysRoleMapper.java b/backend/src/main/java/com/imeeting/mapper/SysRoleMapper.java deleted file mode 100644 index 8394c1e..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysRoleMapper.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysRole; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysRoleMapper extends BaseMapper {} diff --git a/backend/src/main/java/com/imeeting/mapper/SysRolePermissionMapper.java b/backend/src/main/java/com/imeeting/mapper/SysRolePermissionMapper.java deleted file mode 100644 index 087169d..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysRolePermissionMapper.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysRolePermission; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -@Mapper -public interface SysRolePermissionMapper extends BaseMapper { - @Select(""" - SELECT DISTINCT role_id - FROM sys_role_permission - WHERE perm_id = #{permId} - """) - List selectRoleIdsByPermId(@Param("permId") Long permId); -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java b/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java deleted file mode 100644 index 24a4e5d..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysTenantMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysTenant; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Select; -import org.apache.ibatis.annotations.Param; -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; - -@Mapper -public interface SysTenantMapper extends BaseMapper { - @InterceptorIgnore(tenantLine = "true") - @Select("SELECT * FROM sys_tenant WHERE id = #{id}") - SysTenant selectByIdIgnoreTenant(@Param("id") Long id); -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java b/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java deleted file mode 100644 index bfd653b..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysTenantUserMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysTenantUser; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SysTenantUserMapper extends BaseMapper { -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java b/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java deleted file mode 100644 index bf1a932..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysUserMapper.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysUser; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Select; -import org.apache.ibatis.annotations.Param; -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; -import java.util.List; - -@Mapper -public interface SysUserMapper extends BaseMapper { - @Select(""" - SELECT u.* - FROM sys_user u - JOIN sys_user_role ur ON u.user_id = ur.user_id - WHERE ur.role_id = #{roleId} - AND ur.is_deleted = 0 - AND u.is_deleted = 0 - """) - List selectUsersByRoleId(@Param("roleId") Long roleId); - - @InterceptorIgnore(tenantLine = "true") - @Select("SELECT * FROM sys_user WHERE username = #{username} AND is_deleted = 0") - SysUser selectByUsernameIgnoreTenant(@Param("username") String username); - - @InterceptorIgnore(tenantLine = "true") - @Select("SELECT * FROM sys_user WHERE user_id = #{userId} AND is_deleted = 0") - SysUser selectByIdIgnoreTenant(@Param("userId") Long userId); - - @InterceptorIgnore(tenantLine = "true") - @Select("") - List selectUsersByTenant(@Param("tenantId") Long tenantId, @Param("orgId") Long orgId); - - @InterceptorIgnore(tenantLine = "true") - @Select(""" - SELECT t.id as tenantId, t.tenant_code as tenantCode, t.tenant_name as tenantName - FROM sys_tenant t - JOIN sys_tenant_user tu ON t.id = tu.tenant_id - JOIN sys_user u ON u.user_id = tu.user_id - WHERE u.username = #{username} AND u.is_deleted = 0 AND t.is_deleted = 0 - ORDER BY t.id ASC - """) - List selectTenantsByUsername(@Param("username") String username); -} diff --git a/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java b/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java deleted file mode 100644 index 611ee2e..0000000 --- a/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.imeeting.mapper; - -import com.baomidou.mybatisplus.annotation.InterceptorIgnore; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.imeeting.entity.SysUserRole; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; -import org.apache.ibatis.annotations.Delete; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -@Mapper -public interface SysUserRoleMapper extends BaseMapper { - @Delete(""" - DELETE FROM sys_user_role - WHERE role_id = #{roleId} AND user_id = #{userId} AND tenant_id = #{tenantId} - """) - int physicalDelete(@Param("roleId") Long roleId, @Param("userId") Long userId, @Param("tenantId") Long tenantId); - - @InterceptorIgnore(tenantLine = "true") - @Select(""" - SELECT COUNT(1) - FROM sys_user_role ur - JOIN sys_role r ON r.role_id = ur.role_id - WHERE ur.user_id = #{userId} - AND (ur.tenant_id = #{tenantId} OR ur.tenant_id IS NULL) - AND ur.is_deleted = 0 - AND r.is_deleted = 0 - AND r.tenant_id = #{tenantId} - AND r.role_code = 'TENANT_ADMIN' - """) - Long countTenantAdminRole(@Param("userId") Long userId, @Param("tenantId") Long tenantId); - - @Select(""" - SELECT DISTINCT ur.user_id - FROM sys_user_role ur - WHERE ur.role_id = #{roleId} - AND ur.is_deleted = 0 - """) - List selectUserIdsByRoleId(@Param("roleId") Long roleId); - - @Select(""" - SELECT ur.role_id - FROM sys_user_role ur - WHERE ur.user_id = #{userId} - AND ur.tenant_id = #{tenantId} - AND ur.is_deleted = 0 - """) - List selectRoleIdsByUserIdAndTenantId(@Param("userId") Long userId, @Param("tenantId") Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/security/LoginUser.java b/backend/src/main/java/com/imeeting/security/LoginUser.java deleted file mode 100644 index 34ba21b..0000000 --- a/backend/src/main/java/com/imeeting/security/LoginUser.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.imeeting.security; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class LoginUser implements UserDetails { - private Long userId; - private Long tenantId; - private String username; - private String displayName; - private Boolean isPlatformAdmin; - private Boolean isTenantAdmin; - private Set permissions; - - @Override - public Collection getAuthorities() { - if (permissions == null) return null; - return permissions.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } - - @Override - public String getPassword() { - return null; - } - - @Override - public String getUsername() { - return username; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/backend/src/main/java/com/imeeting/security/PermissionService.java b/backend/src/main/java/com/imeeting/security/PermissionService.java deleted file mode 100644 index 814558e..0000000 --- a/backend/src/main/java/com/imeeting/security/PermissionService.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.imeeting.security; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; - -import java.util.Set; - -@Service("ss") -public class PermissionService { - - /** - * 验证用户是否具备某权限 - * - * @param permission 权限字符串 - * @return 用户是否具备某权限 - */ - public boolean hasPermi(String permission) { - if (permission == null || permission.isEmpty()) { - return false; - } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser)) { - return false; - } - LoginUser loginUser = (LoginUser) authentication.getPrincipal(); - // 平台管理员在系统租户(0)下放行全部权限点 - if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) - && Long.valueOf(0L).equals(loginUser.getTenantId())) { - return true; - } - - Set permissions = loginUser.getPermissions(); - if (CollectionUtils.isEmpty(permissions)) { - return false; - } - - return permissions.contains(permission); - } -} diff --git a/backend/src/main/java/com/imeeting/service/AuthScopeService.java b/backend/src/main/java/com/imeeting/service/AuthScopeService.java deleted file mode 100644 index 7a2f7ed..0000000 --- a/backend/src/main/java/com/imeeting/service/AuthScopeService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.service; - -public interface AuthScopeService { - boolean isCurrentPlatformAdmin(); - - boolean isCurrentTenantAdmin(); - - boolean isTenantAdmin(Long userId, Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/service/AuthService.java b/backend/src/main/java/com/imeeting/service/AuthService.java deleted file mode 100644 index c409bf5..0000000 --- a/backend/src/main/java/com/imeeting/service/AuthService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.imeeting.service; - -import com.imeeting.auth.dto.LoginRequest; -import com.imeeting.auth.dto.TokenResponse; - -public interface AuthService { - TokenResponse login(LoginRequest request); - TokenResponse refresh(String refreshToken); - void logout(Long userId, String deviceCode); - String createDeviceCode(LoginRequest request, String deviceName); - TokenResponse switchTenant(Long userId, Long targetTenantId, String deviceCode); -} diff --git a/backend/src/main/java/com/imeeting/service/AuthVersionService.java b/backend/src/main/java/com/imeeting/service/AuthVersionService.java deleted file mode 100644 index b357393..0000000 --- a/backend/src/main/java/com/imeeting/service/AuthVersionService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.imeeting.service; - -import java.util.Collection; - -public interface AuthVersionService { - long getVersion(Long userId, Long tenantId); - - void invalidateUserTenantAuth(Long userId, Long tenantId); - - void invalidateUsersTenantAuth(Collection userIds, Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/service/DeviceService.java b/backend/src/main/java/com/imeeting/service/DeviceService.java deleted file mode 100644 index 7ba6150..0000000 --- a/backend/src/main/java/com/imeeting/service/DeviceService.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.Device; - -public interface DeviceService extends IService {} diff --git a/backend/src/main/java/com/imeeting/service/SysDictItemService.java b/backend/src/main/java/com/imeeting/service/SysDictItemService.java deleted file mode 100644 index fe012ac..0000000 --- a/backend/src/main/java/com/imeeting/service/SysDictItemService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysDictItem; - -import java.util.List; - -public interface SysDictItemService extends IService { - List getItemsByTypeCode(String typeCode); -} diff --git a/backend/src/main/java/com/imeeting/service/SysDictTypeService.java b/backend/src/main/java/com/imeeting/service/SysDictTypeService.java deleted file mode 100644 index 734ee0f..0000000 --- a/backend/src/main/java/com/imeeting/service/SysDictTypeService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysDictType; - -public interface SysDictTypeService extends IService { -} diff --git a/backend/src/main/java/com/imeeting/service/SysLogService.java b/backend/src/main/java/com/imeeting/service/SysLogService.java deleted file mode 100644 index dc7525a..0000000 --- a/backend/src/main/java/com/imeeting/service/SysLogService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.core.conditions.Wrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysLog; - -public interface SysLogService extends IService { - void recordLog(SysLog log); - - IPage selectPageWithTenant(IPage page, Wrapper queryWrapper); -} diff --git a/backend/src/main/java/com/imeeting/service/SysOrgService.java b/backend/src/main/java/com/imeeting/service/SysOrgService.java deleted file mode 100644 index f79b9e8..0000000 --- a/backend/src/main/java/com/imeeting/service/SysOrgService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysOrg; -import java.util.List; - -public interface SysOrgService extends IService { - List listTree(Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/service/SysParamService.java b/backend/src/main/java/com/imeeting/service/SysParamService.java deleted file mode 100644 index 8c04695..0000000 --- a/backend/src/main/java/com/imeeting/service/SysParamService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.common.PageResult; -import com.imeeting.dto.SysParamQueryDTO; -import com.imeeting.dto.SysParamVO; -import com.imeeting.entity.SysParam; - -import java.util.List; - -public interface SysParamService extends IService { - PageResult> page(SysParamQueryDTO query); - - String getParamValue(String key, String defaultValue); - - String getCachedParamValue(String key, String defaultValue); - - void syncParamToCache(SysParam param); - - void deleteParamCache(String key); - - void syncAllToCache(); -} diff --git a/backend/src/main/java/com/imeeting/service/SysPermissionService.java b/backend/src/main/java/com/imeeting/service/SysPermissionService.java deleted file mode 100644 index e25eb54..0000000 --- a/backend/src/main/java/com/imeeting/service/SysPermissionService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysPermission; - -import java.util.List; -import java.util.Set; - -public interface SysPermissionService extends IService { - List listByUserId(Long userId, Long tenantId); - - Set listPermissionCodesByUserId(Long userId, Long tenantId); -} diff --git a/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java b/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java deleted file mode 100644 index 20d9fe5..0000000 --- a/backend/src/main/java/com/imeeting/service/SysPlatformConfigService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysPlatformConfig; -import com.imeeting.dto.PlatformConfigVO; -import org.springframework.web.multipart.MultipartFile; - -public interface SysPlatformConfigService extends IService { - PlatformConfigVO getConfig(); - boolean updateConfig(SysPlatformConfig config); - String uploadAsset(MultipartFile file); -} diff --git a/backend/src/main/java/com/imeeting/service/SysRoleService.java b/backend/src/main/java/com/imeeting/service/SysRoleService.java deleted file mode 100644 index e517220..0000000 --- a/backend/src/main/java/com/imeeting/service/SysRoleService.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysRole; - -public interface SysRoleService extends IService {} diff --git a/backend/src/main/java/com/imeeting/service/SysTenantService.java b/backend/src/main/java/com/imeeting/service/SysTenantService.java deleted file mode 100644 index 01b851f..0000000 --- a/backend/src/main/java/com/imeeting/service/SysTenantService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.dto.CreateTenantDTO; -import com.imeeting.entity.SysTenant; - -public interface SysTenantService extends IService { - /** - * 创建租户并自动初始化管理员、角色、组织及权限 - * @param dto 租户创建信息 - * @return 租户ID - */ - Long createTenantWithAdmin(CreateTenantDTO dto); -} diff --git a/backend/src/main/java/com/imeeting/service/SysTenantUserService.java b/backend/src/main/java/com/imeeting/service/SysTenantUserService.java deleted file mode 100644 index 7e869fc..0000000 --- a/backend/src/main/java/com/imeeting/service/SysTenantUserService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.entity.SysTenantUser; -import java.util.List; - -public interface SysTenantUserService extends IService { - List listByUserId(Long userId); - void saveTenantUser(Long userId, Long tenantId, Long orgId); - void syncMemberships(Long userId, List memberships); -} diff --git a/backend/src/main/java/com/imeeting/service/SysUserService.java b/backend/src/main/java/com/imeeting/service/SysUserService.java deleted file mode 100644 index d01cae2..0000000 --- a/backend/src/main/java/com/imeeting/service/SysUserService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.extension.service.IService; - -import com.imeeting.entity.SysUser; - -import java.util.List; - - - -public interface SysUserService extends IService { - - List listUsersByRoleId(Long roleId); - - SysUser getByIdIgnoreTenant(Long userId); - - List listUsersByTenant(Long tenantId, Long orgId); - - } - - - - - - diff --git a/backend/src/main/java/com/imeeting/service/biz/AiModelService.java b/backend/src/main/java/com/imeeting/service/biz/AiModelService.java index 63ba0e8..af9c0b1 100644 --- a/backend/src/main/java/com/imeeting/service/biz/AiModelService.java +++ b/backend/src/main/java/com/imeeting/service/biz/AiModelService.java @@ -1,8 +1,9 @@ package com.imeeting.service.biz; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.AiModelDTO; import com.imeeting.dto.biz.AiModelVO; +import com.unisbase.dto.PageResult; import java.util.List; diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingService.java index 166a002..b9d286f 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingService.java @@ -1,12 +1,13 @@ package com.imeeting.service.biz; import com.baomidou.mybatisplus.extension.service.IService; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.dto.biz.MeetingVO; import com.imeeting.entity.biz.Meeting; +import com.unisbase.dto.PageResult; import java.util.List; diff --git a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java index 9d1ae45..aa59449 100644 --- a/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java +++ b/backend/src/main/java/com/imeeting/service/biz/PromptTemplateService.java @@ -4,15 +4,16 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.imeeting.dto.biz.PromptTemplateDTO; import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.PromptTemplate; -import com.imeeting.common.PageResult; +import com.unisbase.dto.PageResult; + import java.util.List; public interface PromptTemplateService extends IService { PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId); PromptTemplateVO updateTemplate(PromptTemplateDTO dto); - PageResult> pageTemplates(Integer current, Integer size, String name, String category, - Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); + PageResult> pageTemplates(Integer current, Integer size, String name, String category, + Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin); } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java index 6205b4b..9a9ce84 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiModelServiceImpl.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.imeeting.common.PageResult; + import com.imeeting.dto.biz.AiModelDTO; import com.imeeting.dto.biz.AiModelVO; import com.imeeting.entity.biz.AsrModel; @@ -13,6 +13,7 @@ import com.imeeting.entity.biz.LlmModel; import com.imeeting.mapper.biz.AsrModelMapper; import com.imeeting.mapper.biz.LlmModelMapper; import com.imeeting.service.biz.AiModelService; +import com.unisbase.dto.PageResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java index c209fe0..6605db0 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/AiTaskServiceImpl.java @@ -4,20 +4,22 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import com.imeeting.common.RedisKeys; import com.imeeting.dto.biz.AiModelVO; -import com.imeeting.entity.SysUser; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; -import com.imeeting.mapper.SysUserMapper; import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper; import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; + +import com.unisbase.entity.SysUser; +import com.unisbase.mapper.SysUserMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -54,10 +56,10 @@ public class AiTaskServiceImpl extends ServiceImpl impleme private final HotWordService hotWordService; private final StringRedisTemplate redisTemplate; - @Value("${app.server-base-url}") + @Value("${unisbase.app.server-base-url}") private String serverBaseUrl; - @Value("${app.upload-path}") + @Value("${unisbase.app.upload-path}") private String uploadPath; private final HttpClient httpClient = HttpClient.newBuilder() diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java index c202dd6..0467d4a 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingServiceImpl.java @@ -4,19 +4,17 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.common.PageResult; +import com.unisbase.dto.PageResult; import com.imeeting.dto.biz.MeetingDTO; import com.imeeting.dto.biz.MeetingTranscriptVO; import com.imeeting.dto.biz.MeetingVO; import com.imeeting.dto.biz.RealtimeTranscriptItemDTO; -import com.imeeting.entity.SysUser; import com.imeeting.entity.biz.AiTask; import com.imeeting.entity.biz.HotWord; import com.imeeting.entity.biz.Meeting; import com.imeeting.entity.biz.MeetingTranscript; import com.imeeting.entity.biz.PromptTemplate; import com.imeeting.event.MeetingCreatedEvent; -import com.imeeting.mapper.SysUserMapper; import com.imeeting.mapper.biz.AiTaskMapper; import com.imeeting.mapper.biz.MeetingMapper; import com.imeeting.mapper.biz.MeetingTranscriptMapper; @@ -25,6 +23,8 @@ import com.imeeting.service.biz.AiTaskService; import com.imeeting.service.biz.HotWordService; import com.imeeting.service.biz.MeetingService; import com.imeeting.service.biz.PromptTemplateService; +import com.unisbase.entity.SysUser; +import com.unisbase.mapper.SysUserMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -59,7 +59,7 @@ public class MeetingServiceImpl extends ServiceImpl impl private final SysUserMapper sysUserMapper; private final ApplicationEventPublisher eventPublisher; - @Value("${app.upload-path}") + @Value("${unisbase.app.upload-path}") private String uploadPath; @Override diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java index 7ff6efb..94da8df 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java @@ -3,7 +3,7 @@ package com.imeeting.service.biz.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.common.PageResult; +import com.unisbase.dto.PageResult; import com.imeeting.dto.biz.PromptTemplateDTO; import com.imeeting.dto.biz.PromptTemplateVO; import com.imeeting.entity.biz.PromptTemplate; diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java index 574e015..73f9316 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java @@ -7,9 +7,10 @@ import com.imeeting.dto.biz.SpeakerRegisterDTO; import com.imeeting.dto.biz.SpeakerVO; import com.imeeting.entity.biz.Speaker; import com.imeeting.mapper.biz.SpeakerMapper; -import com.imeeting.security.LoginUser; + import com.imeeting.service.biz.AiModelService; import com.imeeting.service.biz.SpeakerService; +import com.unisbase.security.LoginUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.context.SecurityContextHolder; @@ -35,13 +36,13 @@ import java.util.UUID; @Service public class SpeakerServiceImpl extends ServiceImpl implements SpeakerService { - @Value("${app.upload-path}") + @Value("${unisbase.app.upload-path}") private String uploadPath; - @Value("${app.server-base-url}") + @Value("${unisbase.app.server-base-url}") private String serverBaseUrl; - @Value("${app.resource-prefix}") + @Value("${unisbase.app.resource-prefix}") private String resourcePrefix; private final AiModelService aiModelService; diff --git a/backend/src/main/java/com/imeeting/service/impl/AuthScopeServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/AuthScopeServiceImpl.java deleted file mode 100644 index 1d7b1f1..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/AuthScopeServiceImpl.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.imeeting.service.impl; - -import com.imeeting.mapper.SysUserRoleMapper; -import com.imeeting.security.LoginUser; -import com.imeeting.service.AuthScopeService; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - -@Service -public class AuthScopeServiceImpl implements AuthScopeService { - private final SysUserRoleMapper sysUserRoleMapper; - - public AuthScopeServiceImpl(SysUserRoleMapper sysUserRoleMapper) { - this.sysUserRoleMapper = sysUserRoleMapper; - } - - @Override - public boolean isCurrentPlatformAdmin() { - LoginUser loginUser = getCurrentLoginUser(); - if (loginUser == null) { - return false; - } - return Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) - && Long.valueOf(0L).equals(loginUser.getTenantId()); - } - - @Override - public boolean isCurrentTenantAdmin() { - LoginUser loginUser = getCurrentLoginUser(); - if (loginUser == null) { - return false; - } - return isTenantAdmin(loginUser.getUserId(), loginUser.getTenantId()); - } - - @Override - public boolean isTenantAdmin(Long userId, Long tenantId) { - if (userId == null || tenantId == null || tenantId <= 0) { - return false; - } - Long count = sysUserRoleMapper.countTenantAdminRole(userId, tenantId); - return count != null && count > 0; - } - - private LoginUser getCurrentLoginUser() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser)) { - return null; - } - return (LoginUser) authentication.getPrincipal(); - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java deleted file mode 100644 index 639113c..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/AuthServiceImpl.java +++ /dev/null @@ -1,349 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.auth.JwtTokenProvider; -import com.imeeting.auth.dto.LoginRequest; -import com.imeeting.auth.dto.TokenResponse; -import com.imeeting.common.RedisKeys; -import com.imeeting.common.SysParamKeys; -import com.imeeting.entity.Device; -import com.imeeting.entity.SysLog; -import com.imeeting.entity.SysUser; -import com.imeeting.mapper.SysUserMapper; -import com.imeeting.service.*; -import io.jsonwebtoken.Claims; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -@Service -public class AuthServiceImpl implements AuthService { - private final SysUserService sysUserService; - private final SysUserMapper sysUserMapper; - private final DeviceService deviceService; - private final SysParamService sysParamService; - private final StringRedisTemplate stringRedisTemplate; - private final PasswordEncoder passwordEncoder; - private final JwtTokenProvider jwtTokenProvider; - private final AuthVersionService authVersionService; - private final SysLogService sysLogService; - private final HttpServletRequest httpServletRequest; - - @Value("${app.token.access-default-minutes:30}") - private long accessDefaultMinutes; - @Value("${app.token.refresh-default-days:7}") - private long refreshDefaultDays; - @Value("${app.captcha.max-attempts:5}") - private int captchaMaxAttempts; - - public AuthServiceImpl(SysUserService sysUserService, - SysUserMapper sysUserMapper, - DeviceService deviceService, - SysParamService sysParamService, - StringRedisTemplate stringRedisTemplate, - PasswordEncoder passwordEncoder, - JwtTokenProvider jwtTokenProvider, - AuthVersionService authVersionService, - SysLogService sysLogService, - HttpServletRequest httpServletRequest) { - this.sysUserService = sysUserService; - this.sysUserMapper = sysUserMapper; - this.deviceService = deviceService; - this.sysParamService = sysParamService; - this.stringRedisTemplate = stringRedisTemplate; - this.passwordEncoder = passwordEncoder; - this.jwtTokenProvider = jwtTokenProvider; - this.authVersionService = authVersionService; - this.sysLogService = sysLogService; - this.httpServletRequest = httpServletRequest; - } - - @Override - public TokenResponse login(LoginRequest request) { - long start = System.currentTimeMillis(); - try { - if (isCaptchaEnabled()) { - validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); - } - - SysUser user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername()); - - if (user == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { - throw new IllegalArgumentException("用户名或密码错误"); - } - - // 获取该用户关联的所有租户 - java.util.List availableTenants = sysUserMapper.selectTenantsByUsername(user.getUsername()); - - // 如果是平台管理员,且没有在租户列表中,手动添加系统租户(ID=0) - if (Boolean.TRUE.equals(user.getIsPlatformAdmin())) { - boolean hasSystemTenant = availableTenants.stream().anyMatch(t -> t.getTenantId() == 0L); - if (!hasSystemTenant) { - availableTenants.add(0, TokenResponse.TenantInfo.builder() - .tenantId(0L).tenantCode("SYSTEM").tenantName("系统平台").build()); - } - } - - if (availableTenants.isEmpty()) { - throw new IllegalArgumentException("该账号未关联任何租户"); - } - - // 确定当前租户: - // 1. 如果请求指定了租户,且用户属于该租户,则使用该租户 - // 2. 否则,如果用户是平台管理员,默认进入系统租户(0) - // 3. 否则,使用第一个非0的业务租户 - Long activeTenantId = null; - if (request.getTenantCode() != null && !request.getTenantCode().trim().isEmpty()) { - String tc = request.getTenantCode().trim(); - activeTenantId = availableTenants.stream() - .filter(t -> t.getTenantCode().equals(tc)) - .map(TokenResponse.TenantInfo::getTenantId) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("您不属于指定的租户: " + tc)); - } else { - if (Boolean.TRUE.equals(user.getIsPlatformAdmin())) { - activeTenantId = 0L; - } else { - // 优先选择非0的租户 - activeTenantId = availableTenants.stream() - .map(TokenResponse.TenantInfo::getTenantId) - .filter(id -> id != 0L) - .findFirst() - .orElse(availableTenants.get(0).getTenantId()); - } - } - - String deviceCode = request.getDeviceCode(); - if (deviceCode != null && !deviceCode.isEmpty()) { - Device device = deviceService.getOne(new LambdaQueryWrapper() - .eq(Device::getUserId, user.getUserId()) - .eq(Device::getDeviceCode, deviceCode) - .eq(Device::getIsDeleted, 0) - .eq(Device::getStatus, 1)); - if (device == null) { - throw new IllegalArgumentException("设备码无效"); - } - } - - long accessMinutes = parseLong(sysParamService.getParamValue("security.token.access_ttl_minutes", - String.valueOf(accessDefaultMinutes)), accessDefaultMinutes); - long refreshDays = parseLong(sysParamService.getParamValue("security.token.refresh_ttl_days", - String.valueOf(refreshDefaultDays)), refreshDefaultDays); - - if (deviceCode == null || deviceCode.isEmpty()) { - deviceCode = "default"; - } - TokenResponse tokens = issueTokens(user, activeTenantId, deviceCode, accessMinutes, refreshDays); - tokens.setAvailableTenants(availableTenants); - cacheRefreshToken(user.getUserId(), deviceCode, tokens.getRefreshToken(), refreshDays); - - recordLoginLog(user.getUserId(), activeTenantId, user.getUsername(), 1, "登录成功", System.currentTimeMillis() - start); - return tokens; - } catch (Exception e) { - recordLoginLog(null, null, request.getUsername(), 0, e.getMessage(), System.currentTimeMillis() - start); - throw e; - } - } - - private void recordLoginLog(Long userId, Long tenantId, String username, Integer status, String msg, long duration) { - SysLog sysLog = new SysLog(); - sysLog.setUserId(userId); - sysLog.setTenantId(tenantId); - sysLog.setUsername(username); - sysLog.setLogType("LOGIN"); - sysLog.setOperation("用户登录: " + username); - sysLog.setMethod("POST /api/auth/login"); - sysLog.setDuration(duration); - sysLog.setStatus(status); - sysLog.setIp(httpServletRequest.getRemoteAddr()); - sysLog.setCreatedAt(LocalDateTime.now()); - sysLogService.recordLog(sysLog); - } - - @Override - public TokenResponse refresh(String refreshToken) { - Claims claims = jwtTokenProvider.parseToken(refreshToken); - String tokenType = claims.get("tokenType", String.class); - if (!"refresh".equals(tokenType)) { - throw new IllegalArgumentException("无效的刷新令牌"); - } - Long userId = claims.get("userId", Long.class); - Long tenantId = claims.get("tenantId", Long.class); - String deviceCode = claims.get("deviceCode", String.class); - Number tokenAuthVersionNum = claims.get("authVersion", Number.class); - long currentAuthVersion = authVersionService.getVersion(userId, tenantId); - if (currentAuthVersion != (tokenAuthVersionNum == null ? 0L : tokenAuthVersionNum.longValue())) { - throw new IllegalArgumentException("刷新令牌已失效"); - } - String cached = stringRedisTemplate.opsForValue().get(RedisKeys.refreshTokenKey(userId, deviceCode)); - if (cached == null || !cached.equals(refreshToken)) { - throw new IllegalArgumentException("刷新令牌已失效"); - } - - long accessMinutes = parseLong(sysParamService.getParamValue("security.token.access_ttl_minutes", - String.valueOf(accessDefaultMinutes)), accessDefaultMinutes); - long refreshDays = parseLong(sysParamService.getParamValue("security.token.refresh_ttl_days", - String.valueOf(refreshDefaultDays)), refreshDefaultDays); - - SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); - TokenResponse tokens = issueTokens(user, tenantId, deviceCode, accessMinutes, refreshDays); - cacheRefreshToken(userId, deviceCode, tokens.getRefreshToken(), refreshDays); - return tokens; - } - - @Override - public TokenResponse switchTenant(Long userId, Long targetTenantId, String deviceCode) { - SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId); - if (user == null) { - throw new IllegalArgumentException("用户不存在"); - } - - // 校验权限:平台管理员可以直接进入租户0,或者用户确实关联了目标租户 - boolean hasAccess = false; - if (targetTenantId == 0L && Boolean.TRUE.equals(user.getIsPlatformAdmin())) { - hasAccess = true; - } else { - java.util.List tenants = sysUserMapper.selectTenantsByUsername(user.getUsername()); - hasAccess = tenants.stream().anyMatch(t -> t.getTenantId().equals(targetTenantId)); - } - - if (!hasAccess) { - throw new IllegalArgumentException("您不属于目标租户"); - } - - long accessMinutes = parseLong(sysParamService.getParamValue("security.token.access_ttl_minutes", - String.valueOf(accessDefaultMinutes)), accessDefaultMinutes); - long refreshDays = parseLong(sysParamService.getParamValue("security.token.refresh_ttl_days", - String.valueOf(refreshDefaultDays)), refreshDefaultDays); - - TokenResponse tokens = issueTokens(user, targetTenantId, deviceCode, accessMinutes, refreshDays); - cacheRefreshToken(userId, deviceCode, tokens.getRefreshToken(), refreshDays); - - // 重新获取该用户关联的所有租户信息返回 - java.util.List availableTenants = sysUserMapper.selectTenantsByUsername(user.getUsername()); - if (Boolean.TRUE.equals(user.getIsPlatformAdmin())) { - boolean hasSystemTenant = availableTenants.stream().anyMatch(t -> t.getTenantId() == 0L); - if (!hasSystemTenant) { - availableTenants.add(0, TokenResponse.TenantInfo.builder() - .tenantId(0L).tenantCode("SYSTEM").tenantName("系统平台").build()); - } - } - tokens.setAvailableTenants(availableTenants); - - return tokens; - } - - @Override - public void logout(Long userId, String deviceCode) { - stringRedisTemplate.delete(RedisKeys.refreshTokenKey(userId, deviceCode)); - } - - @Override - public String createDeviceCode(LoginRequest request, String deviceName) { - if (isCaptchaEnabled()) { - validateCaptcha(request.getCaptchaId(), request.getCaptchaCode()); - } - - SysUser user = sysUserMapper.selectByUsernameIgnoreTenant(request.getUsername()); - - if (user == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { - throw new IllegalArgumentException("用户名或密码错误"); - } - - String deviceCode = UUID.randomUUID().toString().replace("-", ""); - Device device = new Device(); - device.setUserId(user.getUserId()); - device.setDeviceCode(deviceCode); - device.setDeviceName(deviceName == null ? "default" : deviceName); - deviceService.save(device); - return deviceCode; - } - - private void validateCaptcha(String captchaId, String captchaCode) { - if (captchaId == null || captchaId.isEmpty()) { - throw new IllegalArgumentException("验证码不能为空"); - } - if (captchaCode == null || captchaCode.isEmpty()) { - throw new IllegalArgumentException("验证码不能为空"); - } - String key = RedisKeys.captchaKey(captchaId); - String stored = stringRedisTemplate.opsForValue().get(key); - if (stored == null) { - throw new IllegalArgumentException("验证码已过期"); - } - - String attemptsKey = RedisKeys.captchaAttemptsKey(captchaId); - long attempts = 0; - String attemptsStr = stringRedisTemplate.opsForValue().get(attemptsKey); - if (attemptsStr != null) { - attempts = Long.parseLong(attemptsStr); - } - if (attempts >= captchaMaxAttempts) { - throw new IllegalArgumentException("验证码已失效"); - } - - if (!stored.equalsIgnoreCase(captchaCode)) { - stringRedisTemplate.opsForValue().increment(attemptsKey); - stringRedisTemplate.expire(attemptsKey, Duration.ofMinutes(2)); - throw new IllegalArgumentException("验证码错误"); - } - - stringRedisTemplate.delete(key); - stringRedisTemplate.delete(attemptsKey); - } - - private boolean isCaptchaEnabled() { - String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true"); - return Boolean.parseBoolean(value); - } - - private TokenResponse issueTokens(SysUser user, Long tenantId, String deviceCode, long accessMinutes, long refreshDays) { - long authVersion = authVersionService.getVersion(user.getUserId(), tenantId); - Map accessClaims = new HashMap<>(); - accessClaims.put("tokenType", "access"); - accessClaims.put("userId", user.getUserId()); - accessClaims.put("tenantId", tenantId); - accessClaims.put("username", user.getUsername()); - accessClaims.put("displayName", user.getDisplayName()); - accessClaims.put("deviceCode", deviceCode); - accessClaims.put("authVersion", authVersion); - - Map refreshClaims = new HashMap<>(); - refreshClaims.put("tokenType", "refresh"); - refreshClaims.put("userId", user.getUserId()); - refreshClaims.put("tenantId", tenantId); - refreshClaims.put("deviceCode", deviceCode); - refreshClaims.put("authVersion", authVersion); - - String access = jwtTokenProvider.createToken(accessClaims, Duration.ofMinutes(accessMinutes).toMillis()); - String refresh = jwtTokenProvider.createToken(refreshClaims, Duration.ofDays(refreshDays).toMillis()); - return TokenResponse.builder() - .accessToken(access) - .refreshToken(refresh) - .accessExpiresInMinutes(accessMinutes) - .refreshExpiresInDays(refreshDays) - .pwdResetRequired(user.getPwdResetRequired()) - .build(); - } - - private void cacheRefreshToken(Long userId, String deviceCode, String refreshToken, long refreshDays) { - stringRedisTemplate.opsForValue().set(RedisKeys.refreshTokenKey(userId, deviceCode), - refreshToken, Duration.ofDays(refreshDays)); - } - - private long parseLong(String value, long defaultValue) { - try { - return Long.parseLong(value); - } catch (Exception e) { - return defaultValue; - } - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/AuthVersionServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/AuthVersionServiceImpl.java deleted file mode 100644 index 6de6307..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/AuthVersionServiceImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.imeeting.service.impl; - -import com.imeeting.common.RedisKeys; -import com.imeeting.service.AuthVersionService; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.time.Duration; -import java.util.Collection; - -@Service -public class AuthVersionServiceImpl implements AuthVersionService { - private static final Duration VERSION_TTL = Duration.ofDays(30); - private final StringRedisTemplate stringRedisTemplate; - - public AuthVersionServiceImpl(StringRedisTemplate stringRedisTemplate) { - this.stringRedisTemplate = stringRedisTemplate; - } - - @Override - public long getVersion(Long userId, Long tenantId) { - if (userId == null || tenantId == null) { - return 0L; - } - String value = stringRedisTemplate.opsForValue().get(RedisKeys.authVersionKey(userId, tenantId)); - if (value == null || value.trim().isEmpty()) { - return 0L; - } - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return 0L; - } - } - - @Override - public void invalidateUserTenantAuth(Long userId, Long tenantId) { - if (userId == null || tenantId == null) { - return; - } - String versionKey = RedisKeys.authVersionKey(userId, tenantId); - Long newVersion = stringRedisTemplate.opsForValue().increment(versionKey); - if (newVersion == null) { - return; - } - stringRedisTemplate.expire(versionKey, VERSION_TTL); - long previousVersion = Math.max(newVersion - 1, 0); - stringRedisTemplate.delete(RedisKeys.authPermKey(userId, tenantId, previousVersion)); - stringRedisTemplate.delete(RedisKeys.authPermKey(userId, tenantId, newVersion)); - } - - @Override - public void invalidateUsersTenantAuth(Collection userIds, Long tenantId) { - if (userIds == null || userIds.isEmpty() || tenantId == null) { - return; - } - for (Long userId : userIds) { - invalidateUserTenantAuth(userId, tenantId); - } - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/DeviceServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/DeviceServiceImpl.java deleted file mode 100644 index 31ca4f1..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/DeviceServiceImpl.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.Device; -import com.imeeting.mapper.DeviceMapper; -import com.imeeting.service.DeviceService; -import org.springframework.stereotype.Service; - -@Service -public class DeviceServiceImpl extends ServiceImpl implements DeviceService {} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysDictItemServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysDictItemServiceImpl.java deleted file mode 100644 index 944162a..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysDictItemServiceImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.imeeting.common.RedisKeys; -import com.imeeting.entity.SysDictItem; -import com.imeeting.mapper.SysDictItemMapper; -import com.imeeting.service.SysDictItemService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.Serializable; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Random; - -@Slf4j -@Service -public class SysDictItemServiceImpl extends ServiceImpl implements SysDictItemService { - - private final StringRedisTemplate redisTemplate; - private final ObjectMapper objectMapper; - private final Random random = new Random(); - - @Autowired - public SysDictItemServiceImpl(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { - this.redisTemplate = redisTemplate; - this.objectMapper = objectMapper; - } - - @Override - public List getItemsByTypeCode(String typeCode) { - String key = RedisKeys.sysDictKey(typeCode); - try { - String cached = redisTemplate.opsForValue().get(key); - if (RedisKeys.CACHE_EMPTY_MARKER.equals(cached)) { - return new ArrayList<>(); - } - if (cached != null) { - return objectMapper.readValue(cached, new TypeReference>() {}); - } - } catch (Exception e) { - log.error("Redis error for key {}: {}", key, e.getMessage()); - } - - List items = list(new LambdaQueryWrapper() - .eq(SysDictItem::getTypeCode, typeCode) - .eq(SysDictItem::getStatus, 1) - .orderByAsc(SysDictItem::getSortOrder)); - - try { - if (items == null || items.isEmpty()) { - redisTemplate.opsForValue().set(key, RedisKeys.CACHE_EMPTY_MARKER, Duration.ofMinutes(5)); - } else { - int jitter = random.nextInt(120); - redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(items), Duration.ofMinutes(1440 + jitter)); - } - } catch (Exception e) { - log.error("Failed to cache dictionary items for {}: {}", typeCode, e.getMessage()); - } - - return items; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean save(SysDictItem entity) { - boolean success = super.save(entity); - if (success && entity != null) { - deleteCache(entity.getTypeCode()); - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean updateById(SysDictItem entity) { - if (entity == null || entity.getDictItemId() == null) { - return super.updateById(entity); - } - SysDictItem old = getById(entity.getDictItemId()); - boolean success = super.updateById(entity); - if (success && old != null) { - deleteCache(old.getTypeCode()); - if (entity.getTypeCode() != null && !old.getTypeCode().equals(entity.getTypeCode())) { - deleteCache(entity.getTypeCode()); - } - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeById(Serializable id) { - SysDictItem old = getById(id); - boolean success = super.removeById(id); - if (success && old != null) { - deleteCache(old.getTypeCode()); - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeByIds(Collection list) { - if (list == null || list.isEmpty()) { - return false; - } - boolean allSuccess = true; - for (Object id : list) { - if (!removeById((Serializable) id)) { - allSuccess = false; - } - } - return allSuccess; - } - - private void deleteCache(String typeCode) { - if (typeCode != null) { - redisTemplate.delete(RedisKeys.sysDictKey(typeCode)); - } - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/imeeting/service/impl/SysDictTypeServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysDictTypeServiceImpl.java deleted file mode 100644 index 9d90cc4..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysDictTypeServiceImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.common.RedisKeys; -import com.imeeting.entity.SysDictType; -import com.imeeting.mapper.SysDictTypeMapper; -import com.imeeting.service.SysDictTypeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.Serializable; -import java.util.Collection; - -@Service -public class SysDictTypeServiceImpl extends ServiceImpl implements SysDictTypeService { - - private final StringRedisTemplate redisTemplate; - - @Autowired - public SysDictTypeServiceImpl(StringRedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean updateById(SysDictType entity) { - if (entity == null || entity.getDictTypeId() == null) { - return super.updateById(entity); - } - SysDictType old = getById(entity.getDictTypeId()); - boolean success = super.updateById(entity); - if (success && old != null) { - redisTemplate.delete(RedisKeys.sysDictKey(old.getTypeCode())); - if (entity.getTypeCode() != null && !old.getTypeCode().equals(entity.getTypeCode())) { - redisTemplate.delete(RedisKeys.sysDictKey(entity.getTypeCode())); - } - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeById(Serializable id) { - SysDictType old = getById(id); - boolean success = super.removeById(id); - if (success && old != null) { - redisTemplate.delete(RedisKeys.sysDictKey(old.getTypeCode())); - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeByIds(Collection list) { - if (list == null || list.isEmpty()) { - return false; - } - boolean allSuccess = true; - for (Object id : list) { - if (!removeById((Serializable) id)) { - allSuccess = false; - } - } - return allSuccess; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java deleted file mode 100644 index 6f3929d..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysLogServiceImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.Wrapper; -import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysLog; -import com.imeeting.mapper.SysLogMapper; -import com.imeeting.service.SysLogService; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -public class SysLogServiceImpl extends ServiceImpl implements SysLogService { - - @Async - @Override - public void recordLog(SysLog log) { - save(log); - } - - @Override - public IPage selectPageWithTenant(IPage page, Wrapper queryWrapper) { - return baseMapper.selectPageWithTenant(page, queryWrapper); - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java deleted file mode 100644 index 28d66d0..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysOrgServiceImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysOrg; -import com.imeeting.mapper.SysOrgMapper; -import com.imeeting.service.SysOrgService; -import org.springframework.stereotype.Service; -import java.util.List; - -@Service -public class SysOrgServiceImpl extends ServiceImpl implements SysOrgService { - @Override - public List listTree(Long tenantId) { - LambdaQueryWrapper query = new LambdaQueryWrapper<>(); - if (tenantId != null) { - query.eq(SysOrg::getTenantId, tenantId); - } - query.orderByAsc(SysOrg::getSortOrder); - return list(query); - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java deleted file mode 100644 index f7ff1f2..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysParamServiceImpl.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.common.PageResult; -import com.imeeting.common.RedisKeys; -import com.imeeting.dto.SysParamQueryDTO; -import com.imeeting.dto.SysParamVO; -import com.imeeting.entity.SysParam; -import com.imeeting.mapper.SysParamMapper; -import com.imeeting.service.SysParamService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.Serializable; -import java.time.Duration; -import java.util.List; -import java.util.stream.Collectors; - -@Slf4j -@Service -public class SysParamServiceImpl extends ServiceImpl implements SysParamService { - private final StringRedisTemplate redisTemplate; - - public SysParamServiceImpl(StringRedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - @Override - public PageResult> page(SysParamQueryDTO query) { - Page page = new Page<>(query.getPageNum(), query.getPageSize()); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - if (query.getParamKey() != null && !query.getParamKey().isEmpty()) { - wrapper.like(SysParam::getParamKey, query.getParamKey()); - } - if (query.getParamType() != null && !query.getParamType().isEmpty()) { - wrapper.eq(SysParam::getParamType, query.getParamType()); - } - if (query.getDescription() != null && !query.getDescription().isEmpty()) { - wrapper.like(SysParam::getDescription, query.getDescription()); - } - wrapper.orderByDesc(SysParam::getCreatedAt); - - Page result = this.baseMapper.selectPage(page, wrapper); - - PageResult> pageResult = new PageResult<>(); - pageResult.setTotal(result.getTotal()); - pageResult.setRecords(result.getRecords().stream().map(this::toVO).collect(Collectors.toList())); - return pageResult; - } - - private SysParamVO toVO(SysParam entity) { - if (entity == null) return null; - SysParamVO vo = new SysParamVO(); - vo.setParamId(entity.getParamId()); - vo.setParamKey(entity.getParamKey()); - vo.setParamValue(entity.getParamValue()); - vo.setParamType(entity.getParamType()); - vo.setIsSystem(entity.getIsSystem()); - vo.setDescription(entity.getDescription()); - vo.setStatus(entity.getStatus()); - vo.setCreatedAt(entity.getCreatedAt()); - vo.setUpdatedAt(entity.getUpdatedAt()); - return vo; - } - - @Override - public String getParamValue(String key, String defaultValue) { - if (key == null || key.isEmpty()) { - return defaultValue; - } - - String redisKey = RedisKeys.sysParamKey(key); - try { - // 1. 尝试从 Redis 获取 - String cachedValue = redisTemplate.opsForValue().get(redisKey); - if (cachedValue != null) { - // 如果是空标记,返回默认值 - if (RedisKeys.CACHE_EMPTY_MARKER.equals(cachedValue)) { - return defaultValue; - } - return cachedValue; - } - } catch (Exception e) { - log.error("Redis read error for key {}: {}", redisKey, e.getMessage()); - } - - // 2. Redis 未命中,查数据库 - log.info("Cache miss for param key: {}, fetching from DB", key); - SysParam param = getOne(new LambdaQueryWrapper().eq(SysParam::getParamKey, key)); - - if (param != null) { - String val = param.getParamValue(); - // 3. 回写 Redis - try { - redisTemplate.opsForValue().set(redisKey, val == null ? "" : val, Duration.ofHours(24)); - } catch (Exception e) { - log.error("Redis write error for key {}: {}", redisKey, e.getMessage()); - } - return val; - } else { - // 4. 数据库也无数据,设置空标记防止穿透 - try { - // Use default value or empty marker if needed - redisTemplate.opsForValue().set(redisKey, RedisKeys.CACHE_EMPTY_MARKER, Duration.ofMinutes(5)); - } catch (Exception e) { - log.error("Redis write empty marker error for key {}: {}", redisKey, e.getMessage()); - } - return defaultValue; - } - } - - @Override - public String getCachedParamValue(String key, String defaultValue) { - return getParamValue(key, defaultValue); - } - - @Override - public void syncParamToCache(SysParam param) { - if (param != null && param.getParamKey() != null) { - redisTemplate.opsForValue().set(RedisKeys.sysParamKey(param.getParamKey()), - param.getParamValue() == null ? "" : param.getParamValue(), Duration.ofHours(24)); - } - } - - @Override - public void deleteParamCache(String key) { - if (key != null) { - redisTemplate.delete(RedisKeys.sysParamKey(key)); - } - } - - @Override - public void syncAllToCache() { - log.info("Syncing all system parameters to Redis"); - List params = list(); - for (SysParam param : params) { - syncParamToCache(param); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean save(SysParam entity) { - boolean success = super.save(entity); - if (success && entity.getParamKey() != null) { - deleteParamCache(entity.getParamKey()); - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean updateById(SysParam entity) { - // 先查出旧的 Key 确保缓存被清理 - SysParam old = getById(entity.getParamId()); - boolean success = super.updateById(entity); - if (success && old != null) { - deleteParamCache(old.getParamKey()); - if (entity.getParamKey() != null && !entity.getParamKey().equals(old.getParamKey())) { - deleteParamCache(entity.getParamKey()); - } - } - return success; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeById(Serializable id) { - SysParam old = getById(id); - boolean success = super.removeById(id); - if (success && old != null) { - deleteParamCache(old.getParamKey()); - } - return success; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java deleted file mode 100644 index 84503ee..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysPermissionServiceImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysPermission; -import com.imeeting.entity.SysUser; -import com.imeeting.mapper.SysPermissionMapper; -import com.imeeting.service.SysPermissionService; -import com.imeeting.service.SysUserService; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Service -public class SysPermissionServiceImpl extends ServiceImpl implements SysPermissionService { - - private final SysUserService sysUserService; - - public SysPermissionServiceImpl(@Lazy SysUserService sysUserService) { - this.sysUserService = sysUserService; - } - - @Override - public List listByUserId(Long userId, Long tenantId) { - if (userId == null) { - return List.of(); - } - - SysUser user = sysUserService.getByIdIgnoreTenant(userId); - if (user != null && Boolean.TRUE.equals(user.getIsPlatformAdmin()) && Long.valueOf(0).equals(tenantId)) { - return list(); - } - - // 如果没有指定租户,或者租户为0但用户不是平台管理员,则返回空或按默认逻辑(通常需要指定租户) - if (tenantId == null) return List.of(); - - return baseMapper.selectByUserId(userId, tenantId); - } - - @Override - public Set listPermissionCodesByUserId(Long userId, Long tenantId) { - List perms = listByUserId(userId, tenantId); - return perms.stream() - .map(SysPermission::getCode) - .filter(code -> code != null && !code.isEmpty()) - .collect(Collectors.toSet()); - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java deleted file mode 100644 index 8a6fca7..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysPlatformConfigServiceImpl.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.imeeting.common.RedisKeys; -import com.imeeting.dto.PlatformConfigVO; -import com.imeeting.entity.SysPlatformConfig; -import com.imeeting.mapper.SysPlatformConfigMapper; -import com.imeeting.service.SysPlatformConfigService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.UUID; - -@Slf4j -@Service -public class SysPlatformConfigServiceImpl extends ServiceImpl implements SysPlatformConfigService { - - private final StringRedisTemplate redisTemplate; - private final ObjectMapper objectMapper; - - @Value("${app.upload-path}") - private String uploadPath; - - @Value("${app.resource-prefix}") - private String resourcePrefix; - - public SysPlatformConfigServiceImpl(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { - this.redisTemplate = redisTemplate; - this.objectMapper = objectMapper; - } - - @Override - public PlatformConfigVO getConfig() { - String key = RedisKeys.platformConfigKey(); - try { - String cached = redisTemplate.opsForValue().get(key); - if (cached != null) { - return objectMapper.readValue(cached, PlatformConfigVO.class); - } - } catch (Exception e) { - log.error("Read platform config from redis error", e); - } - - SysPlatformConfig config = getById(1L); - if (config == null) { - return new PlatformConfigVO(); - } - - PlatformConfigVO vo = toVO(config); - try { - redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(vo), Duration.ofDays(1)); - } catch (Exception e) { - log.error("Write platform config to redis error", e); - } - return vo; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean updateConfig(SysPlatformConfig config) { - config.setId(1L); - SysPlatformConfig old = getById(1L); - - boolean success = updateById(config); - if (success) { - redisTemplate.delete(RedisKeys.platformConfigKey()); - // 物理文件清理逻辑可以在这里根据需要扩展(例如对比 old 和 config 的 URL) - } - return success; - } - - @Override - public String uploadAsset(MultipartFile file) { - if (file.isEmpty()) { - throw new RuntimeException("File is empty"); - } - - String originalFilename = file.getOriginalFilename(); - String extension = ""; - if (originalFilename != null && originalFilename.contains(".")) { - extension = originalFilename.substring(originalFilename.lastIndexOf(".")); - } - - String fileName = UUID.randomUUID().toString() + extension; - Path path = Paths.get(uploadPath, fileName); - - try { - Files.copy(file.getInputStream(), path); - String prefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/"; - return prefix + fileName; - } catch (IOException e) { - log.error("Upload asset error", e); - throw new RuntimeException("Failed to store file"); - } - } - - private PlatformConfigVO toVO(SysPlatformConfig entity) { - PlatformConfigVO vo = new PlatformConfigVO(); - vo.setProjectName(entity.getProjectName()); - vo.setLogoUrl(entity.getLogoUrl()); - vo.setIconUrl(entity.getIconUrl()); - vo.setLoginBgUrl(entity.getLoginBgUrl()); - vo.setIcpInfo(entity.getIcpInfo()); - vo.setCopyrightInfo(entity.getCopyrightInfo()); - vo.setSystemDescription(entity.getSystemDescription()); - return vo; - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysRoleServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysRoleServiceImpl.java deleted file mode 100644 index cdfbbc9..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysRoleServiceImpl.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysRole; -import com.imeeting.mapper.SysRoleMapper; -import com.imeeting.service.SysRoleService; -import org.springframework.stereotype.Service; - -@Service -public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService {} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysTenantServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysTenantServiceImpl.java deleted file mode 100644 index 3fd3462..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysTenantServiceImpl.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.dto.CreateTenantDTO; -import com.imeeting.entity.*; -import com.imeeting.mapper.SysRolePermissionMapper; -import com.imeeting.mapper.SysTenantMapper; -import com.imeeting.mapper.SysUserRoleMapper; -import com.imeeting.service.*; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class SysTenantServiceImpl extends ServiceImpl implements SysTenantService { - - private final SysUserService sysUserService; - private final SysRoleService sysRoleService; - private final SysOrgService sysOrgService; - private final SysPermissionService sysPermissionService; - private final SysParamService sysParamService; - private final SysUserRoleMapper sysUserRoleMapper; - private final SysRolePermissionMapper sysRolePermissionMapper; - private final SysTenantUserService sysTenantUserService; - private final PasswordEncoder passwordEncoder; - private final com.imeeting.mapper.DeviceMapper deviceMapper; - - public SysTenantServiceImpl(SysUserService sysUserService, SysRoleService sysRoleService, - SysOrgService sysOrgService, SysPermissionService sysPermissionService, - SysParamService sysParamService, SysUserRoleMapper sysUserRoleMapper, - SysRolePermissionMapper sysRolePermissionMapper, - SysTenantUserService sysTenantUserService, PasswordEncoder passwordEncoder, - com.imeeting.mapper.DeviceMapper deviceMapper) { - this.sysUserService = sysUserService; - this.sysRoleService = sysRoleService; - this.sysOrgService = sysOrgService; - this.sysPermissionService = sysPermissionService; - this.sysParamService = sysParamService; - this.sysUserRoleMapper = sysUserRoleMapper; - this.sysRolePermissionMapper = sysRolePermissionMapper; - this.sysTenantUserService = sysTenantUserService; - this.passwordEncoder = passwordEncoder; - this.deviceMapper = deviceMapper; - } - - @Override - @Transactional(rollbackFor = Exception.class) - public boolean removeById(java.io.Serializable id) { - Long tenantId = (Long) id; - - // 1. 获取该租户下的所有用户 ID 和角色 ID - List tenantUsers = sysTenantUserService.list( - new LambdaQueryWrapper().eq(SysTenantUser::getTenantId, tenantId) - ); - List userIds = tenantUsers.stream().map(SysTenantUser::getUserId).collect(Collectors.toList()); - - List roles = sysRoleService.list( - new LambdaQueryWrapper().eq(SysRole::getTenantId, tenantId) - ); - List roleIds = roles.stream().map(SysRole::getRoleId).collect(Collectors.toList()); - - // 2. 逻辑删除角色权限关联 - if (roleIds != null && !roleIds.isEmpty()) { - sysRolePermissionMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper().in("role_id", roleIds)); - } - - // 3. 逻辑删除租户下的角色 - sysRoleService.lambdaUpdate() - .set(SysRole::getIsDeleted, 1) - .eq(SysRole::getTenantId, tenantId) - .update(); - - // 4. 逻辑删除租户下的组织 - sysOrgService.lambdaUpdate() - .set(SysOrg::getIsDeleted, 1) - .eq(SysOrg::getTenantId, tenantId) - .update(); - - // 4. 逻辑删除用户与租户的关联 - sysTenantUserService.remove(new LambdaQueryWrapper().eq(SysTenantUser::getTenantId, tenantId)); - - // 5. 逻辑删除用户与角色的关联 (带租户隔离的) - sysUserRoleMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper().eq("tenant_id", tenantId)); - - // 6. 清理孤立用户:如果用户不再属于任何租户,则逻辑删除该用户 - if (userIds != null && !userIds.isEmpty()) { - for (Long userId : userIds) { - long count = sysTenantUserService.count( - new LambdaQueryWrapper().eq(SysTenantUser::getUserId, userId) - ); - if (count == 0) { - sysUserService.removeById(userId); - } - } - } - - // 7. 逻辑删除租户下的设备 - if (deviceMapper != null) { - deviceMapper.delete(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper().eq("tenant_id", tenantId)); - } - - // 8. 最后逻辑删除租户记录本身 - return super.removeById(id); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createTenantWithAdmin(CreateTenantDTO dto) { - // 1. 校验租户编码唯一性 - if (count(new LambdaQueryWrapper().eq(SysTenant::getTenantCode, dto.getTenantCode())) > 0) { - throw new RuntimeException("租户编码已存在:" + dto.getTenantCode()); - } - - // 2. 创建租户 - SysTenant tenant = new SysTenant(); - tenant.setTenantCode(dto.getTenantCode()); - tenant.setTenantName(dto.getTenantName()); - tenant.setContactName(dto.getContactName()); - tenant.setContactPhone(dto.getContactPhone()); - tenant.setRemark(dto.getRemark()); - tenant.setExpireTime(dto.getExpireTime()); - tenant.setStatus(1); - save(tenant); - Long tenantId = tenant.getId(); - - // 3. 初始化根组织 - SysOrg rootOrg = new SysOrg(); - rootOrg.setTenantId(tenantId); - rootOrg.setOrgName(dto.getTenantName()); - rootOrg.setOrgCode(dto.getTenantCode() + "_ROOT"); - rootOrg.setParentId(null); - rootOrg.setStatus(1); - sysOrgService.save(rootOrg); - Long orgId = rootOrg.getId(); - - // 4. 创建租户管理员角色 - SysRole adminRole = new SysRole(); - adminRole.setTenantId(tenantId); - adminRole.setRoleCode("TENANT_ADMIN"); - adminRole.setRoleName("租户管理员"); - adminRole.setStatus(1); - adminRole.setRemark("系统自动初始化的租户管理员角色"); - sysRoleService.save(adminRole); - Long roleId = adminRole.getRoleId(); - - // 5. 分配默认菜单权限 - String menuCodes = sysParamService.getParamValue("tenant.init.default.menu.codes", ""); - if (menuCodes != null && !menuCodes.trim().isEmpty()) { - List codes = Arrays.asList(menuCodes.split(",")); - List perms = sysPermissionService.list( - new LambdaQueryWrapper().in(SysPermission::getCode, codes) - ); - if (!perms.isEmpty()) { - for (SysPermission p : perms) { - SysRolePermission rp = new SysRolePermission(); - rp.setRoleId(roleId); - rp.setPermId(p.getPermId()); - sysRolePermissionMapper.insert(rp); - } - } - } - - // 6. 创建管理员用户 - String username = "admin_" + dto.getTenantCode(); - if (sysUserService.count(new LambdaQueryWrapper().eq(SysUser::getUsername, username)) > 0) { - throw new RuntimeException("管理员用户名已存在:" + username); - } - - String defaultPwd = sysParamService.getParamValue("tenant.init.default.password", "123456"); - SysUser user = new SysUser(); - user.setUsername(username); - user.setDisplayName(dto.getTenantName() + "管理员"); - user.setPasswordHash(passwordEncoder.encode(defaultPwd)); - user.setPwdResetRequired(1); - user.setStatus(1); - user.setIsPlatformAdmin(false); - sysUserService.save(user); - Long userId = user.getUserId(); - - // 7. 绑定用户与角色 (sys_user_role) - SysUserRole ur = new SysUserRole(); - ur.setTenantId(tenantId); - ur.setUserId(userId); - ur.setRoleId(roleId); - sysUserRoleMapper.insert(ur); - - // 8. 绑定用户与租户/组织 (sys_tenant_user) - SysTenantUser tu = new SysTenantUser(); - tu.setUserId(userId); - tu.setTenantId(tenantId); - tu.setOrgId(orgId); - tu.setStatus(1); - sysTenantUserService.save(tu); - - return tenantId; - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java deleted file mode 100644 index ba54cf1..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysTenantUserServiceImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysTenantUser; -import com.imeeting.mapper.SysTenantUserMapper; -import com.imeeting.service.SysTenantUserService; -import org.springframework.stereotype.Service; -import java.util.List; - -@Service -public class SysTenantUserServiceImpl extends ServiceImpl implements SysTenantUserService { - - private final com.imeeting.service.SysOrgService sysOrgService; - - public SysTenantUserServiceImpl(com.imeeting.service.SysOrgService sysOrgService) { - this.sysOrgService = sysOrgService; - } - - @Override - public List listByUserId(Long userId) { - List list = list(new LambdaQueryWrapper().eq(SysTenantUser::getUserId, userId)); - if (list != null && !list.isEmpty()) { - for (SysTenantUser tu : list) { - if (tu.getOrgId() != null) { - com.imeeting.entity.SysOrg org = sysOrgService.getById(tu.getOrgId()); - if (org != null) { - tu.setOrgName(org.getOrgName()); - } - } - } - } - return list; - } - - @Override - public void saveTenantUser(Long userId, Long tenantId, Long orgId) { - LambdaQueryWrapper query = new LambdaQueryWrapper() - .eq(SysTenantUser::getUserId, userId) - .eq(SysTenantUser::getTenantId, tenantId); - SysTenantUser existing = getOne(query); - if (existing != null) { - existing.setOrgId(orgId); - updateById(existing); - } else { - SysTenantUser tu = new SysTenantUser(); - tu.setUserId(userId); - tu.setTenantId(tenantId); - tu.setOrgId(orgId); - save(tu); - } - } - - @Override - public void syncMemberships(Long userId, List memberships) { - if (userId == null) return; - - // 1. Physical removal of all existing memberships for this user - getBaseMapper().delete(new LambdaQueryWrapper().eq(SysTenantUser::getUserId, userId)); - - // 2. Add new ones - if (memberships != null && !memberships.isEmpty()) { - java.util.Set processedTenants = new java.util.HashSet<>(); - for (SysTenantUser m : memberships) { - if (m.getTenantId() != null && processedTenants.add(m.getTenantId())) { - SysTenantUser tu = new SysTenantUser(); - tu.setUserId(userId); - tu.setTenantId(m.getTenantId()); - tu.setOrgId(m.getOrgId()); - save(tu); - } - } - } - } -} diff --git a/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java b/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java deleted file mode 100644 index b328b42..0000000 --- a/backend/src/main/java/com/imeeting/service/impl/SysUserServiceImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.imeeting.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.imeeting.entity.SysUser; -import com.imeeting.mapper.SysUserMapper; -import com.imeeting.service.SysUserService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class SysUserServiceImpl extends ServiceImpl implements SysUserService { - - @Override - public List listUsersByRoleId(Long roleId) { - return baseMapper.selectUsersByRoleId(roleId); - } - - @Override - public SysUser getByIdIgnoreTenant(Long userId) { - return baseMapper.selectByIdIgnoreTenant(userId); - } - - @Override - public List listUsersByTenant(Long tenantId, Long orgId) { - return baseMapper.selectUsersByTenant(tenantId, orgId); - } - - @Override - public boolean save(SysUser entity) { - validateUniqueUsername(entity); - return super.save(entity); - } - - @Override - public boolean updateById(SysUser entity) { - validateUniqueUsername(entity); - return super.updateById(entity); - } - - private void validateUniqueUsername(SysUser user) { - if (user.getUsername() == null) return; - - LambdaQueryWrapper query = new LambdaQueryWrapper() - .eq(SysUser::getUsername, user.getUsername()); - - if (user.getUserId() != null) { - query.ne(SysUser::getUserId, user.getUserId()); - } - - if (count(query) > 0) { - throw new IllegalArgumentException("用户名 [" + user.getUsername() + "] 已被占用"); - } - } -} diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index 4a4cd1f..ecc4fc3 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -34,17 +34,35 @@ mybatis-plus: logic-delete-value: 1 logic-not-delete-value: 0 -security: - jwt: - secret: ${SECURITY_JWT_SECRET:change-me-please-change-me-32bytes} - -app: - server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:8080} - upload-path: ${APP_UPLOAD_PATH:/data/imeeting/uploads/} - resource-prefix: /api/static/ - captcha: - ttl-seconds: 120 - max-attempts: 5 - token: - access-default-minutes: 30 - refresh-default-days: 7 +unisbase: + web: + auth-endpoints-enabled: true + management-endpoints-enabled: true + tenant: + ignoreTables: + - biz_ai_tasks + - biz_meeting_transcripts + - biz_speakers + security: + enabled: true + mode: embedded + jwt-secret: ${SECURITY_JWT_SECRET:change-me-please-change-me-32bytes} + auth-header: Authorization + token-prefix: "Bearer " + permit-all-urls: + - /actuator/health + - /api/static/** + internal-auth: + enabled: true + secret: change-me-internal-secret + header-name: X-Internal-Secret + app: + server-base-url: ${APP_SERVER_BASE_URL:http://127.0.0.1:8080} # 本地应用对外暴露的 IP 和端口 + upload-path: ${APP_UPLOAD_PATH:/data/imeeting/uploads/} + resource-prefix: /api/static/ + captcha: + ttl-seconds: 120 + max-attempts: 5 + token: + access-default-minutes: 30 + refresh-default-days: 7 \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e477a08..d2a27a6 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -35,17 +35,35 @@ mybatis-plus: logic-delete-value: 1 logic-not-delete-value: 0 -security: - jwt: - secret: change-me-please-change-me-32bytes - -app: - server-base-url: http://10.100.52.13:${server.port} # 本地应用对外暴露的 IP 和端口 - upload-path: D:/data/imeeting/uploads/ - resource-prefix: /api/static/ - captcha: - ttl-seconds: 120 - max-attempts: 5 - token: - access-default-minutes: 30 - refresh-default-days: 7 +unisbase: + web: + auth-endpoints-enabled: true + management-endpoints-enabled: true + tenant: + ignoreTables: + - biz_ai_tasks + - biz_meeting_transcripts + - biz_speakers + security: + enabled: true + mode: embedded + jwt-secret: change-me-please-change-me-32bytes + auth-header: Authorization + token-prefix: "Bearer " + permit-all-urls: + - /actuator/health + - /api/static/** + internal-auth: + enabled: true + secret: change-me-internal-secret + header-name: X-Internal-Secret + app: + server-base-url: http://10.100.52.13:${server.port} # 本地应用对外暴露的 IP 和端口 + upload-path: D:/data/imeeting/uploads/ + resource-prefix: /api/static/ + captcha: + ttl-seconds: 120 + max-attempts: 5 + token: + access-default-minutes: 30 + refresh-default-days: 7 \ No newline at end of file diff --git a/backend/src/test/java/com/imeeting/service/DictItemServiceTest.java b/backend/src/test/java/com/imeeting/service/DictItemServiceTest.java index ecb478b..2431f69 100644 --- a/backend/src/test/java/com/imeeting/service/DictItemServiceTest.java +++ b/backend/src/test/java/com/imeeting/service/DictItemServiceTest.java @@ -1,45 +1,43 @@ -package com.imeeting.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.imeeting.entity.SysDictItem; -import com.imeeting.mapper.SysDictItemMapper; -import com.imeeting.service.impl.SysDictItemServiceImpl; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class DictItemServiceTest { - - @Mock - private SysDictItemMapper dictItemMapper; - - @InjectMocks - private SysDictItemServiceImpl dictItemService; - - @Test - void testGetItemsByTypeCode() { - String typeCode = "gender"; - SysDictItem item = new SysDictItem(); - item.setTypeCode(typeCode); - item.setItemLabel("Male"); - item.setItemValue("1"); - item.setStatus(1); - item.setSortOrder(1); - - when(dictItemMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(item)); - - List result = dictItemService.getItemsByTypeCode(typeCode); - assertEquals(1, result.size()); - assertEquals("Male", result.get(0).getItemLabel()); - } -} +//package com.imeeting.service; +// +//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +// +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//import java.util.Collections; +//import java.util.List; +// +//import static org.junit.jupiter.api.Assertions.assertEquals; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.when; +// +//@ExtendWith(MockitoExtension.class) +//public class DictItemServiceTest { +// +// @Mock +// private SysDictItemMapper dictItemMapper; +// +// @InjectMocks +// private SysDictItemServiceImpl dictItemService; +// +// @Test +// void testGetItemsByTypeCode() { +// String typeCode = "gender"; +// SysDictItem item = new SysDictItem(); +// item.setTypeCode(typeCode); +// item.setItemLabel("Male"); +// item.setItemValue("1"); +// item.setStatus(1); +// item.setSortOrder(1); +// +// when(dictItemMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.singletonList(item)); +// +// List result = dictItemService.getItemsByTypeCode(typeCode); +// assertEquals(1, result.size()); +// assertEquals("Male", result.get(0).getItemLabel()); +// } +//} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fd5c76c..9fd8ca7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "@ant-design/icons": "^6.1.0", "antd": "^5.13.2", "axios": "^1.6.7", - "html2canvas": "^1.4.1", + "classnames": "^2.5.1", "i18next": "^25.8.6", "i18next-browser-languagedetector": "^8.2.1", "jspdf": "^4.2.0", @@ -26,6 +26,7 @@ "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@vitejs/plugin-react": "^4.2.1", + "less": "^4.4.1", "typescript": "^5.3.3", "vite": "^5.0.12" } @@ -1704,6 +1705,7 @@ "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", + "optional": true, "engines": { "node": ">= 0.6.0" } @@ -1906,6 +1908,22 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -1932,6 +1950,7 @@ "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -2040,6 +2059,20 @@ "dev": true, "license": "ISC" }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2286,6 +2319,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2389,6 +2430,7 @@ "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", + "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -2437,6 +2479,34 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -2511,6 +2581,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2569,6 +2652,32 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/less": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/less/-/less-4.6.4.tgz", + "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", @@ -2601,6 +2710,32 @@ "yallist": "^3.0.2" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3205,6 +3340,20 @@ ], "license": "MIT" }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", @@ -3251,6 +3400,24 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", @@ -3289,6 +3456,16 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", @@ -3303,6 +3480,17 @@ "dev": true, "license": "ISC" }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", @@ -3348,6 +3536,14 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", @@ -4192,6 +4388,25 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", @@ -4220,6 +4435,17 @@ "semver": "bin/semver.js" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4309,6 +4535,7 @@ "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -4494,6 +4721,7 @@ "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", + "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/frontend/package.json b/frontend/package.json index d0e43c1..0d3cedb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "@ant-design/icons": "^6.1.0", "antd": "^5.13.2", "axios": "^1.6.7", - "html2canvas": "^1.4.1", + "classnames": "^2.5.1", "i18next": "^25.8.6", "i18next-browser-languagedetector": "^8.2.1", "jspdf": "^4.2.0", @@ -27,6 +27,7 @@ "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "@vitejs/plugin-react": "^4.2.1", + "less": "^4.4.1", "typescript": "^5.3.3", "vite": "^5.0.12" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 75d1b3e..01fdc9a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,15 @@ import { useEffect, useState } from "react"; +import { ConfigProvider, theme } from "antd"; import AppRoutes from "./routes"; import { getOpenPlatformConfig } from "./api"; +import {useThemeStore} from "./store/themeStore"; import type { SysPlatformConfig } from "./types"; export default function App() { const [config, setConfig] = useState(null); - + const { colorPrimary, themeMode, initTheme } = useThemeStore(); useEffect(() => { + initTheme(); const fetchConfig = async () => { try { const data = await getOpenPlatformConfig(); @@ -30,7 +33,25 @@ export default function App() { } }; fetchConfig(); - }, []); + }, [initTheme]); - return ; + return ( + + + + ); } diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 843a3d1..03de690 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -16,7 +16,6 @@ export interface TokenResponse { refreshToken: string; accessExpiresInMinutes: number; refreshExpiresInDays: number; - pwdResetRequired?: number; availableTenants?: TenantInfo[]; } @@ -38,26 +37,26 @@ export interface DeviceCodePayload { } export async function fetchCaptcha() { - const resp = await http.get("/auth/captcha"); + const resp = await http.get("/sys/auth/captcha"); return resp.data.data as CaptchaResponse; } export async function login(payload: LoginPayload) { - const resp = await http.post("/auth/login", payload); + const resp = await http.post("/sys/auth/login", payload); return resp.data.data as TokenResponse; } export async function createDeviceCode(payload: DeviceCodePayload) { - const resp = await http.post("/auth/device-code", payload); + const resp = await http.post("/sys/auth/device-code", payload); return resp.data.data as string; } export async function refreshToken(refreshToken: string) { - const resp = await http.post("/auth/refresh", { refreshToken }); + const resp = await http.post("/sys/auth/refresh", { refreshToken }); return resp.data.data as TokenResponse; } export async function switchTenant(tenantId: number) { - const resp = await http.post(`/auth/switch-tenant?tenantId=${tenantId}`); + const resp = await http.post(`/sys/auth/switch-tenant?tenantId=${tenantId}`); return resp.data.data as TokenResponse; } diff --git a/frontend/src/api/dict.ts b/frontend/src/api/dict.ts index 7da3575..0bfc9af 100644 --- a/frontend/src/api/dict.ts +++ b/frontend/src/api/dict.ts @@ -3,47 +3,47 @@ import { SysDictType, SysDictItem } from "../types"; // Dictionary Type APIs export async function fetchDictTypes(params?: { current?: number; size?: number; typeCode?: string; typeName?: string }) { - const resp = await http.get("/api/dict-types", { params }); + const resp = await http.get("/sys/api/dict-types", { params }); return resp.data.data; } export async function createDictType(data: Partial) { - const resp = await http.post("/api/dict-types", data); + const resp = await http.post("/sys/api/dict-types", data); return resp.data.data as boolean; } export async function updateDictType(id: number, data: Partial) { - const resp = await http.put(`/api/dict-types/${id}`, data); + const resp = await http.put(`/sys/api/dict-types/${id}`, data); return resp.data.data as boolean; } export async function deleteDictType(id: number) { - const resp = await http.delete(`/api/dict-types/${id}`); + const resp = await http.delete(`/sys/api/dict-types/${id}`); return resp.data.data as boolean; } // Dictionary Item APIs export async function fetchDictItems(typeCode?: string) { - const resp = await http.get("/api/dict-items", { params: { typeCode } }); + const resp = await http.get("/sys/api/dict-items", { params: { typeCode } }); return resp.data.data as SysDictItem[]; } export async function createDictItem(data: Partial) { - const resp = await http.post("/api/dict-items", data); + const resp = await http.post("/sys/api/dict-items", data); return resp.data.data as boolean; } export async function updateDictItem(id: number, data: Partial) { - const resp = await http.put(`/api/dict-items/${id}`, data); + const resp = await http.put(`/sys/api/dict-items/${id}`, data); return resp.data.data as boolean; } export async function deleteDictItem(id: number) { - const resp = await http.delete(`/api/dict-items/${id}`); + const resp = await http.delete(`/sys/api/dict-items/${id}`); return resp.data.data as boolean; } export async function fetchDictItemsByTypeCode(typeCode: string) { - const resp = await http.get(`/api/dict-items/type/${typeCode}`); + const resp = await http.get(`/sys/api/dict-items/type/${typeCode}`); return resp.data.data as SysDictItem[]; } \ No newline at end of file diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts index a64a096..449e8c2 100644 --- a/frontend/src/api/http.ts +++ b/frontend/src/api/http.ts @@ -2,7 +2,7 @@ import { message } from "antd"; const http = axios.create({ - baseURL: "", + baseURL: "/", timeout: 15000 }); diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index b5b2646..c3b3ff4 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,187 +1,198 @@ -import http from "./http"; +import http from "./http"; import { DeviceInfo, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult, PermissionNode } from "../types"; export async function pageParams(params: SysParamQuery) { - const resp = await http.get("/api/params/page", { params }); + const resp = await http.get("/sys/api/params/page", { params }); return resp.data.data as PageResult; } export async function createParam(payload: Partial) { - const resp = await http.post("/api/params", payload); + const resp = await http.post("/sys/api/params", payload); return resp.data.data as boolean; } export async function updateParam(id: number, payload: Partial) { - const resp = await http.put(`/api/params/${id}`, payload); + const resp = await http.put(`/sys/api/params/${id}`, payload); return resp.data.data as boolean; } export async function deleteParam(id: number) { - const resp = await http.delete(`/api/params/${id}`); + const resp = await http.delete(`/sys/api/params/${id}`); return resp.data.data as boolean; } export async function listUsers(params?: { tenantId?: number; orgId?: number }) { - const resp = await http.get("/api/users", { params }); + const resp = await http.get("/sys/api/users", { params }); return resp.data.data as SysUser[]; } export async function createUser(payload: Partial) { - const resp = await http.post("/api/users", payload); + const resp = await http.post("/sys/api/users", payload); return resp.data.data as boolean; } export async function updateUser(id: number, payload: Partial) { - const resp = await http.put(`/api/users/${id}`, payload); + const resp = await http.put(`/sys/api/users/${id}`, payload); return resp.data.data as boolean; } export async function deleteUser(id: number) { - const resp = await http.delete(`/api/users/${id}`); + const resp = await http.delete(`/sys/api/users/${id}`); return resp.data.data as boolean; } export async function getUserDetail(id: number) { - const resp = await http.get(`/api/users/${id}`); + const resp = await http.get(`/sys/api/users/${id}`); return resp.data.data as SysUser; } export async function listRoles(tenantId?: number) { - const resp = await http.get("/api/roles", { params: { tenantId } }); - return resp.data.data as SysRole[]; + const resp = await http.get("/sys/api/roles", { params: { current: 1, size: 1000, tenantId } }); + return (resp.data.data as PageResult).records || []; +} + +export async function pageRoles(params?: { current?: number; size?: number; tenantId?: number; keyword?: string }) { + const resp = await http.get("/sys/api/roles", { params }); + return resp.data.data as PageResult; } export async function createRole(payload: Partial) { - const resp = await http.post("/api/roles", payload); + const resp = await http.post("/sys/api/roles", payload); return resp.data.data as boolean; } export async function updateRole(id: number, payload: Partial) { - const resp = await http.put(`/api/roles/${id}`, payload); + const resp = await http.put(`/sys/api/roles/${id}`, payload); return resp.data.data as boolean; } export async function deleteRole(id: number) { - const resp = await http.delete(`/api/roles/${id}`); + const resp = await http.delete(`/sys/api/roles/${id}`); return resp.data.data as boolean; } export async function listPermissions() { - const resp = await http.get("/api/permissions"); + const resp = await http.get("/sys/api/permissions"); return resp.data.data as SysPermission[]; } export async function getSystemParamValue(key: string, defaultValue?: string) { - const resp = await http.get("/api/params/value", { params: { key, defaultValue } }); + const resp = await http.get("/sys/api/params/value", { params: { key, defaultValue } }); return resp.data.data as string; } export async function listMyPermissions() { - const resp = await http.get("/api/permissions/me"); + const resp = await http.get("/sys/api/permissions/me"); return resp.data.data as SysPermission[]; } export async function fetchMyMenuTree() { - const resp = await http.get("/api/permissions/tree/me"); + const resp = await http.get("/sys/api/permissions/tree/me"); return resp.data.data as PermissionNode[]; } export async function getCurrentUser() { - const resp = await http.get("/api/users/me"); + const resp = await http.get("/sys/api/users/me"); return resp.data.data as UserProfile; } export async function updateMyProfile(payload: Partial) { - const resp = await http.put("/api/users/profile", payload); + const resp = await http.put("/sys/api/users/profile", payload); return resp.data.data as boolean; } export async function updateMyPassword(payload: any) { - const resp = await http.put("/api/users/password", payload); + const resp = await http.put("/sys/api/users/password", payload); return resp.data.data as boolean; } export async function createPermission(payload: Partial) { - const resp = await http.post("/api/permissions", payload); + const resp = await http.post("/sys/api/permissions", payload); return resp.data.data as boolean; } export async function updatePermission(id: number, payload: Partial) { - const resp = await http.put(`/api/permissions/${id}`, payload); + const resp = await http.put(`/sys/api/permissions/${id}`, payload); return resp.data.data as boolean; } export async function deletePermission(id: number) { - const resp = await http.delete(`/api/permissions/${id}`); + const resp = await http.delete(`/sys/api/permissions/${id}`); return resp.data.data as boolean; } export async function listDevices() { - const resp = await http.get("/api/devices"); + const resp = await http.get("/sys/api/devices"); return resp.data.data as DeviceInfo[]; } export async function createDevice(payload: Partial) { - const resp = await http.post("/api/devices", payload); + const resp = await http.post("/sys/api/devices", payload); return resp.data.data as boolean; } export async function updateDevice(id: number, payload: Partial) { - const resp = await http.put(`/api/devices/${id}`, payload); + const resp = await http.put(`/sys/api/devices/${id}`, payload); return resp.data.data as boolean; } export async function deleteDevice(id: number) { - const resp = await http.delete(`/api/devices/${id}`); + const resp = await http.delete(`/sys/api/devices/${id}`); return resp.data.data as boolean; } export async function listUserRoles(userId: number) { - const resp = await http.get(`/api/users/${userId}/roles`); + const resp = await http.get(`/sys/api/users/${userId}/roles`); return resp.data.data as number[]; } export async function saveUserRoles(userId: number, roleIds: number[]) { - const resp = await http.post(`/api/users/${userId}/roles`, { roleIds }); + const resp = await http.post(`/sys/api/users/${userId}/roles`, { roleIds }); return resp.data.data as boolean; } export async function listRolePermissions(roleId: number) { - const resp = await http.get(`/api/roles/${roleId}/permissions`); + const resp = await http.get(`/sys/api/roles/${roleId}/permissions`); return resp.data.data as number[]; } export async function saveRolePermissions(roleId: number, permIds: number[]) { - const resp = await http.post(`/api/roles/${roleId}/permissions`, { permIds }); + const resp = await http.post(`/sys/api/roles/${roleId}/permissions`, { permIds }); return resp.data.data as boolean; } export async function fetchUsersByRoleId(roleId: number) { - const resp = await http.get(`/api/roles/${roleId}/users`); + const resp = await http.get(`/sys/api/roles/${roleId}/users`); return resp.data.data as SysUser[]; } export async function bindUsersToRole(roleId: number, userIds: number[]) { - const resp = await http.post(`/api/roles/${roleId}/users`, { userIds }); + const resp = await http.post(`/sys/api/roles/${roleId}/users`, { userIds }); return resp.data.data as boolean; } export async function unbindUserFromRole(roleId: number, userId: number) { - const resp = await http.delete(`/api/roles/${roleId}/users/${userId}`); + const resp = await http.delete(`/sys/api/roles/${roleId}/users/${userId}`); return resp.data.data as boolean; } export async function fetchLogs(params: any) { - const resp = await http.get("/api/logs", { params }); + const resp = await http.get("/sys/api/logs", { params }); return resp.data.data; } +export async function fetchLogModules() { + const resp = await http.get("/sys/api/logs/modules"); + return resp.data.data as string[]; +} + export * from "./dict"; export * from "./tenant"; export * from "./org"; export * from "./platform"; + diff --git a/frontend/src/api/org.ts b/frontend/src/api/org.ts index a1d9e62..8d1257c 100644 --- a/frontend/src/api/org.ts +++ b/frontend/src/api/org.ts @@ -2,26 +2,26 @@ import http from "./http"; import { SysOrg } from "../types"; export async function listOrgs(tenantId?: number) { - const resp = await http.get("/api/orgs", { params: { tenantId } }); + const resp = await http.get("/sys/api/orgs", { params: { tenantId } }); return resp.data.data as SysOrg[]; } export async function getOrg(id: number) { - const resp = await http.get(`/api/orgs/${id}`); + const resp = await http.get(`/sys/api/orgs/${id}`); return resp.data.data as SysOrg; } export async function createOrg(data: Partial) { - const resp = await http.post("/api/orgs", data); + const resp = await http.post("/sys/api/orgs", data); return resp.data.data as boolean; } export async function updateOrg(id: number, data: Partial) { - const resp = await http.put(`/api/orgs/${id}`, data); + const resp = await http.put(`/sys/api/orgs/${id}`, data); return resp.data.data as boolean; } export async function deleteOrg(id: number) { - const resp = await http.delete(`/api/orgs/${id}`); + const resp = await http.delete(`/sys/api/orgs/${id}`); return resp.data.data as boolean; } diff --git a/frontend/src/api/platform.ts b/frontend/src/api/platform.ts index 560d634..49790ce 100644 --- a/frontend/src/api/platform.ts +++ b/frontend/src/api/platform.ts @@ -5,7 +5,7 @@ import { SysPlatformConfig } from "../types"; * 获取公开平台配置 */ export async function getOpenPlatformConfig() { - const resp = await http.get("/api/open/platform/config"); + const resp = await http.get("/sys/api/open/platform/config"); return resp.data.data as SysPlatformConfig; } @@ -13,7 +13,7 @@ export async function getOpenPlatformConfig() { * 获取管理端平台配置 */ export async function getAdminPlatformConfig() { - const resp = await http.get("/api/admin/platform/config"); + const resp = await http.get("/sys/api/admin/platform/config"); return resp.data.data as SysPlatformConfig; } @@ -21,7 +21,7 @@ export async function getAdminPlatformConfig() { * 更新平台配置 */ export async function updatePlatformConfig(payload: SysPlatformConfig) { - const resp = await http.put("/api/admin/platform/config", payload); + const resp = await http.put("/sys/api/admin/platform/config", payload); return resp.data.data as boolean; } @@ -32,7 +32,7 @@ export async function updatePlatformConfig(payload: SysPlatformConfig) { export async function uploadPlatformAsset(file: File) { const formData = new FormData(); formData.append("file", file); - const resp = await http.post("/api/admin/platform/config/upload", formData, { + const resp = await http.post("/sys/api/admin/platform/config/upload", formData, { headers: { "Content-Type": "multipart/form-data", }, diff --git a/frontend/src/api/tenant.ts b/frontend/src/api/tenant.ts index 41af85b..8ac0a07 100644 --- a/frontend/src/api/tenant.ts +++ b/frontend/src/api/tenant.ts @@ -2,26 +2,26 @@ import http from "./http"; import { SysTenant } from "../types"; export async function listTenants(params: any) { - const resp = await http.get("/api/tenants", { params }); + const resp = await http.get("/sys/api/tenants", { params }); return resp.data.data; } export async function getTenant(id: number) { - const resp = await http.get(`/api/tenants/${id}`); + const resp = await http.get(`/sys/api/tenants/${id}`); return resp.data.data as SysTenant; } export async function createTenant(data: Partial) { - const resp = await http.post("/api/tenants", data); + const resp = await http.post("/sys/api/tenants", data); return resp.data.data as boolean; } export async function updateTenant(id: number, data: Partial) { - const resp = await http.put(`/api/tenants/${id}`, data); + const resp = await http.put(`/sys/api/tenants/${id}`, data); return resp.data.data as boolean; } export async function deleteTenant(id: number) { - const resp = await http.delete(`/api/tenants/${id}`); + const resp = await http.delete(`/sys/api/tenants/${id}`); return resp.data.data as boolean; } diff --git a/frontend/src/components/ThemeSelector/ThemeSelector.tsx b/frontend/src/components/ThemeSelector/ThemeSelector.tsx new file mode 100644 index 0000000..85aed9a --- /dev/null +++ b/frontend/src/components/ThemeSelector/ThemeSelector.tsx @@ -0,0 +1,81 @@ +import { ColorPicker, Space, Drawer, Segmented, Typography, Button } from 'antd'; +import { FormatPainterOutlined, LayoutOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { useThemeStore, type ThemeMode, type LayoutMode } from '@/store/themeStore'; + +const { Text } = Typography; + +export default function ThemeSelector() { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const { colorPrimary, themeMode, layoutMode, setColorPrimary, setThemeMode, setLayoutMode } = useThemeStore(); + + const themeOptions = [ + { label: t('theme.default', 'Default'), value: 'default' }, + { label: t('theme.minimal', 'Minimal'), value: 'minimal' }, + { label: t('theme.tech', 'Tech'), value: 'tech' }, + ]; + + const layoutOptions = [ + { label: t('theme.layoutSide', 'Side Menu'), value: 'side' }, + { label: t('theme.layoutTop', 'Top Menu'), value: 'top' }, + ]; + + return ( + <> + setOpen(true)}> + + + setOpen(false)} + open={open} + width={300} + > + +
+
+ + {t('theme.color', 'Theme Color')} +
+ setColorPrimary(color.toHexString())} + showText + style={{ width: '100%' }} + /> +
+ +
+
+ + {t('theme.style', 'Style Mode')} +
+ setThemeMode(value as ThemeMode)} + /> +
+ +
+
+ + {t('theme.layout', 'Navigation Mode')} +
+ setLayoutMode(value as LayoutMode)} + /> +
+
+
+ + ); +} + diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index ebe493c..4cef054 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -4,18 +4,6 @@ import { UserProfile } from "../types"; export function useAuth() { const [accessToken, setAccessToken] = useState(() => localStorage.getItem("accessToken")); - const parseJwtPayload = (token: string) => { - try { - const payloadPart = token.split(".")[1]; - if (!payloadPart) return null; - const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/"); - const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); - return JSON.parse(atob(padded)); - } catch (e) { - return null; - } - }; - useEffect(() => { const handler = () => setAccessToken(localStorage.getItem("accessToken")); window.addEventListener("storage", handler); @@ -24,17 +12,7 @@ export function useAuth() { const profile = useMemo(() => { const data = sessionStorage.getItem("userProfile"); - if (data) { - return JSON.parse(data); - } - if (!accessToken) { - return null; - } - const payload = parseJwtPayload(accessToken); - if (payload && (payload.pwdResetRequired === 0 || payload.pwdResetRequired === 1)) { - return { pwdResetRequired: Number(payload.pwdResetRequired) } as UserProfile; - } - return null; + return data ? JSON.parse(data) : null; }, [accessToken]); const isAuthed = !!accessToken; diff --git a/frontend/src/index.css b/frontend/src/index.css index ab4bb01..f2e7d72 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,10 +1,37 @@ +:root { + --app-primary-color: #1677ff; + --app-bg-main: radial-gradient(circle at top, rgba(56, 154, 255, 0.08), transparent 26%), linear-gradient(180deg, #f3f7fb 0%, #eef3f8 100%); + --app-bg-card: rgba(255, 255, 255, 0.92); + --app-text-main: #1f2937; + --app-border-color: rgba(15, 93, 166, 0.06); + --app-shadow: 0 10px 24px rgba(15, 23, 42, 0.06); +} + +:root[data-theme="minimal"] { + --app-bg-main: #f9fafb; + --app-bg-card: #ffffff; + --app-text-main: #111827; + --app-border-color: #e5e7eb; + --app-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +:root[data-theme="tech"] { + --app-bg-main: radial-gradient(circle at 50% 0%, rgba(22, 119, 255, 0.15), transparent 40%), #0d1117; + --app-bg-card: rgba(30, 41, 59, 0.7); + --app-text-main: #e2e8f0; + --app-border-color: rgba(22, 119, 255, 0.2); + --app-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); +} + body { margin: 0; padding: 0; - background-color: #f5f7fa; + background: var(--app-bg-main); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, - 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + color: var(--app-text-main); + transition: background 0.3s ease, color 0.3s ease; } .ant-layout { @@ -12,11 +39,18 @@ body { } .ant-layout-sider { - background: #fff !important; + background: var(--app-bg-card) !important; + border-right: 1px solid var(--app-border-color); + transition: background 0.3s ease; } .ant-menu-light { background: transparent !important; + color: var(--app-text-main) !important; +} + +.ant-menu-light .ant-menu-item a { + color: var(--app-text-main) !important; } /* Sider animation refinement */ @@ -39,3 +73,109 @@ body { ::-webkit-scrollbar-track { background: #f1f1f1; } + +#root { + min-height: 100vh; +} + +.app-page { + height: 100%; + padding: 24px; + display: flex; + flex-direction: column; + overflow: auto; +} + +.app-page--contained { + max-width: 1280px; + margin: 0 auto; +} + +.app-page__filter-card, +.app-page__content-card, +.app-page__panel-card { + border: 1px solid var(--app-border-color); + border-radius: 16px !important; + box-shadow: var(--app-shadow); + background: var(--app-bg-card); + backdrop-filter: blur(10px); + transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; +} + +.app-page__filter-card { + margin-bottom: 16px; + flex-shrink: 0; +} + +.app-page__content-card { + min-height: 0; +} + +.app-page__content-card .ant-card-body, +.app-page__panel-card .ant-card-body { + min-height: 0; +} + +.app-page__toolbar { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.app-page__page-actions { + display: flex; + justify-content: flex-end; + align-items: center; + margin: -8px 0 16px; +} + +.app-page__page-actions .ant-btn { + min-width: 96px; + border-radius: 10px !important; +} + +.app-page__toolbar .ant-input, +.app-page__toolbar .ant-input-affix-wrapper, +.app-page__toolbar .ant-select-selector, +.app-page__toolbar .ant-picker, +.app-page__toolbar .ant-input-number, +.app-page__toolbar .ant-btn { + border-radius: 10px !important; +} + +.app-page__toolbar .ant-btn { + min-width: 88px; +} + +.app-page__drawer-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 8px 4px 4px; +} + +.app-page__split { + flex: 1; + min-height: 0; +} + +.app-page__empty-state { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.78); + border-radius: 16px; + border: 1px dashed rgba(148, 163, 184, 0.5); +} + +.tabular-nums { + font-variant-numeric: tabular-nums; +} + +@media (max-width: 768px) { + .app-page { + padding: 16px; + } +} diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 9423779..c6109a1 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -1,45 +1,59 @@ -import { Layout, Menu, Button, Space, Avatar, Dropdown, message, type MenuProps, Select, Tooltip } from "antd"; -import { useEffect, useState, useMemo, useCallback } from "react"; +import * as AntIcons from "@ant-design/icons"; +import { + BellOutlined, + ApartmentOutlined, + BookOutlined, + DashboardOutlined, + DesktopOutlined, + GlobalOutlined, + LayoutOutlined, + LogoutOutlined, + MenuFoldOutlined, + MenuUnfoldOutlined, + SafetyCertificateOutlined, + SettingOutlined, + ShopOutlined, + TeamOutlined, + UserOutlined, + VideoCameraOutlined +} from "@ant-design/icons"; +import { Avatar, Button, Dropdown, Layout, Menu, Space, message, type MenuProps } from "antd"; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { - DashboardOutlined, - VideoCameraOutlined, - UserOutlined, - TeamOutlined, - SafetyCertificateOutlined, - DesktopOutlined, - LogoutOutlined, - MenuUnfoldOutlined, - MenuFoldOutlined, - BellOutlined, - SettingOutlined, - GlobalOutlined, - ShopOutlined, - AudioOutlined, - TagsOutlined, - BulbOutlined, - ApiOutlined -} from "@ant-design/icons"; -import { useAuth } from "../hooks/useAuth"; -import { usePermission } from "../hooks/usePermission"; -import { listMyPermissions, getCurrentUser } from "../api"; -import { switchTenant, type TenantInfo } from "../api/auth"; -import { SysPermission, SysPlatformConfig } from "../types"; +import { getCurrentUser, listMyPermissions } from "@/api"; +import { switchTenant, type TenantInfo } from "@/api/auth"; +import { useAuth } from "@/hooks/useAuth"; +import { usePermission } from "@/hooks/usePermission"; +import type { SysPermission, SysPlatformConfig } from "@/types"; +import ThemeSelector from "@/components/ThemeSelector/ThemeSelector"; +import { useThemeStore } from "@/store/themeStore"; -const { Header, Sider, Content } = Layout; +const { Header, Sider, Content, Footer } = Layout; -const iconMap: Record = { - "dashboard": , - "meeting": , - "user": , - "role": , - "permission": , - "device": , - "audio": , - "hotword": , - "prompt": , - "aimodel": , +const iconMap: Record = { + dashboard: , + meeting: , + user: , + role: , + permission: , + device: , + tenant: , + org: , + dict: , + setting: +}; + +function resolveMenuIcon(icon?: string): ReactNode { + if (!icon) return ; + const aliasIcon = iconMap[icon]; + if (aliasIcon) return aliasIcon; + const IconComponent = (AntIcons as unknown as Record)[icon]; + return IconComponent ? : ; +} + +type PermissionMenuNode = SysPermission & { + children?: PermissionMenuNode[]; }; export default function AppLayout() { @@ -48,99 +62,57 @@ export default function AppLayout() { const [menus, setMenus] = useState([]); const [availableTenants, setAvailableTenants] = useState([]); const [currentTenantId, setCurrentTenantId] = useState(null); - - const platformConfig = useMemo(() => { + const [openKeys, setOpenKeys] = useState([]); + const [platformConfig, setPlatformConfig] = useState(() => { const configStr = sessionStorage.getItem("platformConfig"); return configStr ? JSON.parse(configStr) : null; - }, []); + }); const location = useLocation(); const navigate = useNavigate(); const { logout } = useAuth(); const { load: loadPermissions, can } = usePermission(); + const { layoutMode } = useThemeStore(); - const buildPermissionTree = (list: SysPermission[]) => { - const map = new Map(); - const roots: (SysPermission & { children?: SysPermission[] })[] = []; - list.forEach((m) => map.set(m.permId, { ...m, children: [] })); - map.forEach((node) => { - if (node.parentId && map.has(node.parentId)) { - map.get(node.parentId)!.children!.push(node); - } else { - roots.push(node); - } - }); - const sortNodes = (nodes: (SysPermission & { children?: SysPermission[] })[]) => { - nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - nodes.forEach((n) => n.children && sortNodes(n.children)); - }; - sortNodes(roots); - return roots; - }; - - const findFirstMenuPath = (nodes: (SysPermission & { children?: SysPermission[] })[]): string | null => { - for (const node of nodes) { - if (node.permType === "menu" && node.path) { - return node.path; - } - if (node.children && node.children.length > 0) { - const firstChildPath = findFirstMenuPath(node.children as any); - if (firstChildPath) { - return firstChildPath; - } - } - } - return null; - }; - - const fetchInitialData = async () => { + const fetchInitialData = useCallback(async () => { try { - // Load tenants from localStorage const storedTenants = localStorage.getItem("availableTenants"); if (storedTenants) { - const tenants = JSON.parse(storedTenants) as TenantInfo[]; - setAvailableTenants(tenants); + setAvailableTenants(JSON.parse(storedTenants) as TenantInfo[]); } - // Get current profile to know current tenant - const profileStr = sessionStorage.getItem("userProfile"); - if (profileStr) { - const profile = JSON.parse(profileStr); - // We need to know which tenant is active. The token has it, - // but for UI we can infer from profile if we update profile on switch. - // For now, let's assume we store activeTenantId in localStorage on login/switch - const activeId = localStorage.getItem("activeTenantId"); - if (activeId) setCurrentTenantId(Number(activeId)); + const activeTenantId = localStorage.getItem("activeTenantId"); + if (activeTenantId) { + setCurrentTenantId(Number(activeTenantId)); } const data = await listMyPermissions(); - // Load permissions into localStorage as well await loadPermissions(); - - // Filter visible menus and sort them + const filtered = data - .filter(p => (p.permType === 'menu' || p.permType === 'directory') && p.isVisible === 1 && p.status === 1) + .filter((item) => (item.permType === "menu" || item.permType === "directory") && item.isVisible === 1 && item.status === 1) .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); setMenus(filtered); - - // 如果当前是根路径,自动跳转到第一个有权限的菜单 - if (location.pathname === '/' && filtered.length > 0) { - const menuTree = buildPermissionTree(filtered); - const firstPath = findFirstMenuPath(menuTree); - if (firstPath && firstPath !== '/') { - navigate(firstPath, { replace: true }); - } - } - } catch (e) { - message.error(t('common.error')); + } catch { + message.error(t("common.error")); } - }; + }, [loadPermissions, t]); useEffect(() => { fetchInitialData(); + }, [fetchInitialData]); + + useEffect(() => { + const syncPlatformConfig = () => { + const configStr = sessionStorage.getItem("platformConfig"); + setPlatformConfig(configStr ? JSON.parse(configStr) : null); + }; + + window.addEventListener("platform-config-updated", syncPlatformConfig); + return () => window.removeEventListener("platform-config-updated", syncPlatformConfig); }, []); - const handleSwitchTenant = async (tenantId: number) => { + const handleSwitchTenant = useCallback(async (tenantId: number) => { try { const data = await switchTenant(tenantId); localStorage.setItem("accessToken", data.accessToken); @@ -149,18 +121,16 @@ export default function AppLayout() { if (data.availableTenants) { localStorage.setItem("availableTenants", JSON.stringify(data.availableTenants)); } - - // Refresh profile + const profile = await getCurrentUser(); sessionStorage.setItem("userProfile", JSON.stringify(profile)); - - message.success(t('common.success')); - // Reload to refresh all states and permissions + + message.success(t("common.success")); window.location.reload(); - } catch (e: any) { - message.error(e.message || t('common.error')); + } catch (error: any) { + message.error(error.message || t("common.error")); } - }; + }, [t]); const handleLogout = useCallback(() => { logout(); @@ -169,213 +139,305 @@ export default function AppLayout() { const changeLanguage = useCallback((lng: string) => { i18n.changeLanguage(lng); - message.success(lng === 'zh-CN' ? '已切换至中文' : 'Switched to English'); - }, [i18n]); + message.success(lng === "zh-CN" ? t("layout.switchedToChinese") : t("layout.switchedToEnglish")); + }, [i18n, t]); - const buildMenuTree = useCallback((list: SysPermission[]) => buildPermissionTree(list), []); + const buildMenuTree = useCallback((list: SysPermission[]) => { + const map = new Map(); + const roots: PermissionMenuNode[] = []; - const menuItems = useMemo(() => { - const toMenuItems = (nodes: (SysPermission & { children?: SysPermission[] })[]): any[] => { - if (!Array.isArray(nodes)) return []; - return nodes.map((m) => { - const key = m.path || m.code || String(m.permId); - const icon = m.icon ? (iconMap[m.icon] || ) : ; - - if (m.permType === 'directory' || (m.children && m.children.length > 0)) { - return { - key, - icon, - label: m.name, - children: m.children && m.children.length > 0 ? toMenuItems(m.children as any) : undefined, - }; + list.forEach((item) => map.set(item.permId, { ...item, children: [] })); + + map.forEach((node) => { + if (node.parentId && map.has(node.parentId)) { + map.get(node.parentId)!.children!.push(node); + } else { + roots.push(node); + } + }); + + const sortNodes = (nodes: PermissionMenuNode[]) => { + nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + nodes.forEach((node) => { + if (node.children?.length) { + sortNodes(node.children); } - - return { - key, - icon, - label: {m.name}, - }; }); }; - return toMenuItems(buildMenuTree(menus)); - }, [menus, buildMenuTree]); + sortNodes(roots); + return roots; + }, []); - // Calculate open keys based on current path - const [openKeys, setOpenKeys] = useState([]); + const toMenuItems = useCallback((nodes: PermissionMenuNode[]): MenuProps["items"] => + nodes.map((item) => { + const key = item.path || item.code || String(item.permId); + const icon = resolveMenuIcon(item.icon); + + if (item.permType === "directory" || item.children?.length) { + return { + key, + icon, + label: item.name, + children: item.children?.length ? toMenuItems(item.children) : undefined + }; + } + + return { + key, + icon, + label: {item.name} + }; + }), []); + + const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [buildMenuTree, menus, toMenuItems]); useEffect(() => { - if (menus.length > 0) { - const findParentKeys = (nodes: any[], path: string, parents: string[] = []): string[] | null => { - if (!Array.isArray(nodes)) return null; - for (const node of nodes) { - if (node.key === path) return parents; - if (node.children && Array.isArray(node.children)) { - const found = findParentKeys(node.children, path, [...parents, node.key]); - if (found) return found; + if (!menus.length) { + return; + } + + const findParentKeys = (nodes: NonNullable, path: string, parents: string[] = []): string[] | null => { + for (const node of nodes) { + if (!node || typeof node !== "object" || !("key" in node)) { + continue; + } + + if (String(node.key) === path) { + return parents; + } + + if ("children" in node && node.children) { + const found = findParentKeys(node.children, path, [...parents, String(node.key)]); + if (found) { + return found; } } - return null; - }; - const keys = findParentKeys(menuItems, location.pathname); - if (keys) { - setOpenKeys(prev => Array.from(new Set([...prev, ...keys]))); } + + return null; + }; + + const keys = findParentKeys(menuItems || [], location.pathname); + if (keys?.length) { + setOpenKeys((prev) => Array.from(new Set([...prev, ...keys]))); } - }, [location.pathname, menuItems, menus]); + }, [location.pathname, menuItems, menus.length]); const userMenuItems: MenuProps["items"] = useMemo(() => { - const items: any[] = [ + const items: NonNullable = [ { - key: 'profile', - label: {t('layout.profile')}, + key: "profile", + label: {t("layout.profile")}, icon: } ]; - items.push({ type: 'divider', key: 'd1' }); + let profile: { isPlatformAdmin?: boolean } = {}; + try { + const stored = sessionStorage.getItem("userProfile"); + if (stored) { + profile = JSON.parse(stored) || {}; + } + } catch { + profile = {}; + } + + items.push({ type: "divider", key: "divider" }); items.push({ - key: 'logout', - label: t('layout.logout'), + key: "logout", + label: t("layout.logout"), icon: , onClick: handleLogout }); return items; - }, [t, handleLogout]); + }, [handleLogout, t]); + const langMenuItems: MenuProps["items"] = [ - { key: 'zh-CN', label: '简体中文', onClick: () => changeLanguage('zh-CN') }, - { key: 'en-US', label: 'English', onClick: () => changeLanguage('en-US') }, + { key: "zh-CN", label: "简体中文", onClick: () => changeLanguage("zh-CN") }, + { key: "en-US", label: "English", onClick: () => changeLanguage("en-US") } ]; + const headerRightTools = ( + + + + + + {availableTenants.length > 0 && ( + ({ + key: String(tenant.tenantId), + label: tenant.tenantName, + onClick: () => handleSwitchTenant(tenant.tenantId) + })) + }} + placement="bottomRight" + > + + + )} + + + + } style={{ backgroundColor: "var(--app-primary-color)" }} /> + {localStorage.getItem("displayName") || t("layout.admin")} + + + + ); + + const renderLogo = (isTop: boolean = false) => ( +
+ logo + {(!collapsed || isTop) && ( + + {platformConfig?.projectName || "UnisBase"} + + )} +
+ ); + return ( - - -
- logo - {!collapsed && ( - - {platformConfig?.projectName || "MeetingAI"} - - )} -
- - - -
-
- -
+ + +
- {(platformConfig?.icpInfo || platformConfig?.copyrightInfo) && ( -
- {platformConfig?.icpInfo && {platformConfig.icpInfo}} - {platformConfig?.copyrightInfo && {platformConfig.copyrightInfo}} -
- )}
+ +
+
+ {platformConfig?.icpInfo ? {platformConfig.icpInfo} : null} + {platformConfig?.icpInfo && platformConfig?.copyrightInfo ? : null} + {platformConfig?.copyrightInfo ? {platformConfig.copyrightInfo} : null} +
+
); diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index f8f27d6..f4e8186 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -23,7 +23,12 @@ "settings": "Settings", "logout": "Logout", "language": "Language", - "notification": "Notification" + "notification": "Notification", + "switchTenant": "Switch tenant", + "admin": "Admin", + "switchedToChinese": "Switched to Chinese", + "switchedToEnglish": "Switched to English", + "theme": "Theme" }, "dashboard": { "title": "Dashboard", @@ -226,6 +231,203 @@ "copyright": "Copyright", "desc": "System Description", "uploadHint": "Click or drag to upload", - "uploadLimit": "Recommend 1:1 ratio, max 2MB" + "uploadLimit": "Recommend 1:1 ratio, max 2MB", + "basicInfo": "Basic Information", + "brandAssets": "Brand Assets", + "complianceFooter": "Compliance and Footer", + "saveSettings": "Save Settings", + "projectNameRequired": "Enter the project name", + "projectNamePlaceholder": "Example: UnisBase Admin Console", + "systemDescriptionPlaceholder": "Short description for login or platform introduction areas", + "logoUrl": "Logo URL", + "iconUrl": "Favicon URL", + "loginBgUrl": "Login Background URL", + "uploadLogo": "Upload Logo", + "uploadIcon": "Upload Icon", + "uploadBackground": "Upload Background", + "previewHint": "Preview appears here after upload", + "icpPlaceholder": "Example: ICP 12345678", + "copyrightPlaceholder": "Example: © 2026 UnisBase Team" + }, + "profile": { + "title": "Profile", + "subtitle": "Manage your account information and password settings.", + "basicInfo": "Basic Info", + "security": "Security", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmNewPassword": "Confirm New Password", + "saveChanges": "Save Changes", + "updatePassword": "Update Password", + "passwordsDoNotMatch": "Passwords do not match.", + "standardUser": "Standard User" + }, + "rolesExt": { + "roleList": "Role List", + "filterTenant": "Filter by tenant", + "noRolesFound": "No roles found", + "systemTenant": "System", + "disabled": "Disabled", + "savePermissions": "Save Permissions", + "bindUser": "Bind User", + "assignedUsers": "Assigned Users", + "bindUsersToRole": "Bind users to role", + "searchUser": "Search username or display name", + "displayName": "Display Name", + "phone": "Phone", + "removeBinding": "Remove this binding?", + "editRole": "Edit role", + "deleteRole": "Delete this role?", + "tenantAdminMinOne": "Tenant admin role must keep at least one bound user.", + "tenantLabel": "Tenant", + "enterRoleName": "Enter role name", + "roleCodePlaceholder": "Letters, numbers, and underscores", + "roleScopePlaceholder": "Describe the role scope", + "buttonShort": "BTN", + "tenantAdminWarning": "Tenant admin role must keep at least one bound user.", + "membersTab": "Members" + }, + "logsExt": { + "filterModule": "Filter module", + "module": "Module", + "uncategorized": "Uncategorized", + "actionLabel": "Action", + "success": "Success", + "failed": "Failed", + "system": "System", + "platform": "Platform", + "close": "Close" + }, + "devicesExt": { + "operationSucceeded": "Operation succeeded", + "searchPlaceholder": "Search by device code, name, or owner", + "searchLabel": "Search devices", + "newDevice": "New Device", + "device": "Device", + "unnamedDevice": "Unnamed device", + "ownerId": "ID", + "enabled": "Enabled", + "disabled": "Disabled", + "updatedAt": "Updated At", + "editDevice": "Edit device", + "deleteDevice": "Delete this device?", + "drawerTitleCreate": "New Device", + "drawerTitleEdit": "Edit Device", + "owner": "Owner", + "selectOwner": "Select an owner", + "searchSelectUser": "Search and select a user", + "deviceCodeRequired": "Enter the device code", + "deviceCodePlaceholder": "Enter a unique device code", + "deviceNamePlaceholder": "Example: Meeting Room A Recorder" + }, + "dashboardExt": { + "processing": "Processing", + "completed": "Completed", + "pending": "Pending", + "chartLoading": "Chart engine loading..." + }, + "dictsExt": { + "searchTypes": "Search dictionary types", + "deleteType": "Delete type \"{{name}}\"? This will affect related dictionary items.", + "enabled": "Enabled", + "disabled": "Disabled", + "typeCodePlaceholder": "Example: user_status", + "typeNamePlaceholder": "Example: User Status", + "typeRemarkPlaceholder": "Describe the purpose of this dictionary type", + "itemLabelPlaceholder": "Example: Enabled or Disabled", + "itemValuePlaceholder": "Example: 1", + "itemRemarkPlaceholder": "Optional notes for this item", + "deleteItem": "Delete item \"{{name}}\"?" + }, + "tenantsExt": { + "emptyText": "No tenant data", + "expired": "Expired", + "tenantNamePlaceholder": "Example: Cloud Intelligence", + "tenantCodePlaceholder": "Example: UNIS", + "contactNamePlaceholder": "Contact person", + "contactPhonePlaceholder": "Mobile or phone", + "remarkPlaceholder": "Optional tenant description", + "deleteConfirm": "Delete tenant \"{{name}}\"?" + }, + "usersExt": { + "selectDepartment": "Select department", + "noTenant": "No tenant", + "noRoles": "No roles", + "enabled": "Enabled", + "disabled": "Disabled", + "deleteConfirm": "Delete this user?", + "basicInfo": "Basic Information", + "emailPlaceholder": "example@domain.com", + "passwordKeepPlaceholder": "Leave blank to keep current password", + "passwordInitPlaceholder": "Set initial password", + "selectOrgPlaceholder": "Select organization or department", + "membershipsTitle": "Tenant Memberships", + "membershipTitle": "Membership #{{index}}", + "membershipRequired": "Required", + "selectTenant": "Select tenant", + "addMembership": "Add membership" + }, + "sysParamsExt": { + "defaultType": "DEFAULT", + "enabled": "Enabled", + "disabled": "Disabled", + "deleteConfirm": "Delete this parameter?", + "paramKeyPlaceholder": "sys.config.example", + "paramValuePlaceholder": "Enter parameter value", + "systemHint": "System parameters are usually protected from deletion and used directly by the platform.", + "descriptionPlaceholder": "Describe the purpose of this parameter" + }, + "orgsExt": { + "enabled": "Enabled", + "disabled": "Disabled", + "currentTenant": "Current Tenant", + "deleteConfirm": "Delete \"{{name}}\"?", + "orgNamePlaceholder": "Enter organization name", + "orgCodePlaceholder": "Example: DEPT_TECH" + }, + "permissionsExt": { + "visible": "Visible", + "hidden": "Hidden", + "enabled": "Enabled", + "disabled": "Disabled", + "addChild": "Add Child", + "deleteConfirm": "Delete permission \"{{name}}\"?", + "parentPlaceholder": "Leave blank for top-level permission", + "namePlaceholder": "Example: User Management or Export Report", + "codeHelp": "Used by backend authorization and frontend button visibility.", + "buttonCodeRequired": "Button permissions require a permission code.", + "codePlaceholder": "Example: sys:user:export", + "pathPlaceholder": "Example: /users", + "componentPlaceholder": "Example: pages/access/users", + "iconPlaceholder": "Select an icon", + "descriptionPlaceholder": "Enter a short description for this permission", + "icons": { + "dashboard": "Dashboard", + "meeting": "Meeting", + "user": "User", + "role": "Role", + "permission": "Permission", + "device": "Device", + "tenant": "Tenant", + "org": "Organization", + "dict": "Dictionary", + "setting": "Setting" + }, + "iconSearchPlaceholder": "Search icon name", + "iconEmpty": "No matching icons", + "showIconLibrary": "Show Icons", + "hideIconLibrary": "Hide Icons", + "iconLoadingMore": "Loaded {{current}} / {{total}} icons" + }, + "theme": { + "settings": "Theme Settings", + "color": "Theme Color", + "style": "Style Mode", + "default": "Default", + "minimal": "Minimal", + "tech": "Tech", + "layout": "Navigation Mode", + "layoutSide": "Side Menu", + "layoutTop": "Top Menu" } -} +} \ No newline at end of file diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index b51fe3b..502f547 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -23,7 +23,12 @@ "settings": "系统设置", "logout": "退出登录", "language": "语言", - "notification": "通知" + "notification": "通知", + "switchTenant": "切换租户", + "admin": "管理员", + "switchedToChinese": "已切换到中文", + "switchedToEnglish": "已切换到英文", + "theme": "主题" }, "dashboard": { "title": "系统总览", @@ -44,7 +49,7 @@ "subtitle": "维护系统多租户下的用户信息、组织归属及权限角色", "userInfo": "用户信息", "org": "所属租户/组织", - "platformAdmin": "平台管理", + "platformAdmin": "平台管理员", "searchPlaceholder": "搜索用户名、姓名或邮箱...", "tenantFilter": "按租户筛选...", "drawerTitleCreate": "创建系统用户", @@ -129,7 +134,7 @@ "dictType": "字典类型", "dictItem": "字典项内容", "typeName": "类型名称", - "typeCode": "类型Code", + "typeCode": "类型编码", "itemLabel": "显示标签", "itemValue": "存储数值", "sort": "排序", @@ -220,12 +225,209 @@ "subtitle": "管理系统的品牌标识、视觉风格及法律合规信息", "projectName": "项目名称", "logo": "系统 Logo", - "icon": "浏览器图标 (Icon)", + "icon": "浏览器图标", "loginBg": "登录页背景图", "icp": "ICP 备案号", "copyright": "版权信息", "desc": "系统描述", "uploadHint": "点击或拖拽上传图片", - "uploadLimit": "建议比例 1:1,大小不超过 2MB" + "uploadLimit": "建议比例 1:1,大小不超过 2MB", + "basicInfo": "基础信息", + "brandAssets": "品牌资源", + "complianceFooter": "合规与页脚", + "saveSettings": "保存设置", + "projectNameRequired": "请输入项目名称", + "projectNamePlaceholder": "例如:UnisBase 管理后台", + "systemDescriptionPlaceholder": "用于登录页或平台介绍区域的简短说明", + "logoUrl": "Logo 地址", + "iconUrl": "图标地址", + "loginBgUrl": "登录背景图地址", + "uploadLogo": "上传 Logo", + "uploadIcon": "上传图标", + "uploadBackground": "上传背景图", + "previewHint": "上传后将在此处预览", + "icpPlaceholder": "例如:京 ICP 12345678 号", + "copyrightPlaceholder": "例如:© 2026 UnisBase Team" + }, + "profile": { + "title": "个人中心", + "subtitle": "管理你的账户信息与密码设置。", + "basicInfo": "基础信息", + "security": "安全设置", + "currentPassword": "当前密码", + "newPassword": "新密码", + "confirmNewPassword": "确认新密码", + "saveChanges": "保存修改", + "updatePassword": "更新密码", + "passwordsDoNotMatch": "两次输入的密码不一致。", + "standardUser": "普通用户" + }, + "rolesExt": { + "roleList": "角色列表", + "filterTenant": "按租户筛选", + "noRolesFound": "未找到角色", + "systemTenant": "系统", + "disabled": "已禁用", + "savePermissions": "保存权限", + "bindUser": "绑定用户", + "assignedUsers": "已授权用户", + "bindUsersToRole": "绑定用户到角色", + "searchUser": "搜索用户名或显示名称", + "displayName": "显示名称", + "phone": "电话", + "removeBinding": "确认解除该绑定?", + "editRole": "编辑角色", + "deleteRole": "确认删除该角色?", + "tenantAdminMinOne": "租户管理员角色至少需要保留一个绑定用户。", + "tenantLabel": "租户", + "enterRoleName": "输入角色名称", + "roleCodePlaceholder": "支持字母、数字和下划线", + "roleScopePlaceholder": "描述该角色的使用范围", + "buttonShort": "按钮", + "tenantAdminWarning": "租户管理员角色至少需要保留一个绑定用户。", + "membersTab": "成员" + }, + "logsExt": { + "filterModule": "按模块筛选", + "module": "模块", + "uncategorized": "未分类", + "actionLabel": "动作", + "success": "成功", + "failed": "失败", + "system": "系统", + "platform": "平台", + "close": "关闭" + }, + "devicesExt": { + "operationSucceeded": "操作成功", + "searchPlaceholder": "按设备编码、名称或所属人搜索", + "searchLabel": "搜索设备", + "newDevice": "新建设备", + "device": "设备", + "unnamedDevice": "未命名设备", + "ownerId": "ID", + "enabled": "启用", + "disabled": "禁用", + "updatedAt": "更新时间", + "editDevice": "编辑设备", + "deleteDevice": "确认删除该设备?", + "drawerTitleCreate": "新建设备", + "drawerTitleEdit": "编辑设备", + "owner": "所属人", + "selectOwner": "请选择所属人", + "searchSelectUser": "搜索并选择用户", + "deviceCodeRequired": "请输入设备编码", + "deviceCodePlaceholder": "输入唯一设备编码", + "deviceNamePlaceholder": "例如:A 会议室录音设备" + }, + "dashboardExt": { + "processing": "处理中", + "completed": "已完成", + "pending": "待处理", + "chartLoading": "图表引擎加载中..." + }, + "dictsExt": { + "searchTypes": "搜索字典类型", + "deleteType": "确认删除类型“{{name}}”?这会影响关联字典项。", + "enabled": "启用", + "disabled": "禁用", + "typeCodePlaceholder": "例如:user_status", + "typeNamePlaceholder": "例如:用户状态", + "typeRemarkPlaceholder": "描述该字典类型的用途", + "itemLabelPlaceholder": "例如:启用或禁用", + "itemValuePlaceholder": "例如:1", + "itemRemarkPlaceholder": "可选,填写该字典项的备注", + "deleteItem": "确认删除字典项“{{name}}”?" + }, + "tenantsExt": { + "emptyText": "暂无租户数据", + "expired": "已过期", + "tenantNamePlaceholder": "例如:云智协同", + "tenantCodePlaceholder": "例如:UNIS", + "contactNamePlaceholder": "请输入联系人姓名", + "contactPhonePlaceholder": "请输入手机号或座机号", + "remarkPlaceholder": "选填,填写租户说明", + "deleteConfirm": "确认删除租户“{{name}}”?" + }, + "usersExt": { + "selectDepartment": "请选择部门", + "noTenant": "暂无租户", + "noRoles": "暂无角色", + "enabled": "启用", + "disabled": "禁用", + "deleteConfirm": "确认删除该用户?", + "basicInfo": "基本信息", + "emailPlaceholder": "example@domain.com", + "passwordKeepPlaceholder": "不填写则保留当前密码", + "passwordInitPlaceholder": "请设置初始密码", + "selectOrgPlaceholder": "请选择组织或部门", + "membershipsTitle": "租户归属", + "membershipTitle": "归属 #{{index}}", + "membershipRequired": "必填", + "selectTenant": "请选择租户", + "addMembership": "新增归属" + }, + "sysParamsExt": { + "defaultType": "默认", + "enabled": "启用", + "disabled": "禁用", + "deleteConfirm": "确认删除该参数?", + "paramKeyPlaceholder": "sys.config.example", + "paramValuePlaceholder": "请输入参数值", + "systemHint": "系统参数通常受保护,并会被平台直接使用。", + "descriptionPlaceholder": "请描述该参数的用途" + }, + "orgsExt": { + "enabled": "启用", + "disabled": "禁用", + "currentTenant": "当前租户", + "deleteConfirm": "确认删除“{{name}}”?", + "orgNamePlaceholder": "请输入组织名称", + "orgCodePlaceholder": "例如:DEPT_TECH" + }, + "permissionsExt": { + "visible": "显示", + "hidden": "隐藏", + "enabled": "启用", + "disabled": "禁用", + "addChild": "新增下级", + "deleteConfirm": "确认删除权限“{{name}}”?", + "parentPlaceholder": "不选则为顶级权限", + "namePlaceholder": "例如:用户管理或导出报表", + "codeHelp": "用于后端授权判断和前端按钮可见性控制。", + "buttonCodeRequired": "按钮权限必须填写权限编码。", + "codePlaceholder": "例如:sys:user:export", + "pathPlaceholder": "例如:/users", + "componentPlaceholder": "例如:pages/access/users", + "iconPlaceholder": "请选择图标", + "descriptionPlaceholder": "请输入权限的简要说明", + "icons": { + "dashboard": "仪表盘", + "meeting": "会议", + "user": "用户", + "role": "角色", + "permission": "权限", + "device": "设备", + "tenant": "租户", + "org": "组织", + "dict": "字典", + "setting": "设置" + }, + "iconSearchPlaceholder": "搜索图标名称", + "iconEmpty": "未找到匹配的图标", + "showIconLibrary": "显示图标库", + "hideIconLibrary": "收起图标库", + "iconLoadingMore": "已加载 {{current}} / {{total}} 个图标" + }, + "theme": { + "settings": "主题设置", + "color": "主题色", + "style": "整体风格", + "default": "默认风", + "minimal": "极简风", + "tech": "科技风", + "layout": "导航布局", + "layoutSide": "左侧菜单", + "layoutTop": "顶部菜单" } -} +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx deleted file mode 100644 index 573c32e..0000000 --- a/frontend/src/pages/Dashboard.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; -import { - HistoryOutlined, - CheckCircleOutlined, - LoadingOutlined, - AudioOutlined, - RobotOutlined, - CalendarOutlined, - TeamOutlined, - RiseOutlined, - ClockCircleOutlined, - PlayCircleOutlined, - FileTextOutlined, -} from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; -import dayjs from 'dayjs'; -import { getDashboardStats, getRecentTasks, DashboardStats } from '../api/business/dashboard'; -import { MeetingVO, getMeetingProgress, MeetingProgress } from '../api/business/meeting'; - -const { Title, Text } = Typography; - -const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) => { - const [progress, setProgress] = useState(null); - - useEffect(() => { - if (meeting.status !== 1 && meeting.status !== 2) return; - - const fetchProgress = async () => { - try { - const res = await getMeetingProgress(meeting.id); - if (res.data?.data) { - setProgress(res.data.data); - } - } catch (err) { - // ignore - } - }; - - fetchProgress(); - const timer = setInterval(fetchProgress, 3000); - return () => clearInterval(timer); - }, [meeting.id, meeting.status]); - - if (meeting.status !== 1 && meeting.status !== 2) return null; - - const percent = progress?.percent || 0; - const isError = percent < 0; - - return ( -
-
- - - {progress?.message || '准备分析中...'} - - {!isError && {percent}%} -
- -
- ); -}; - -export const Dashboard: React.FC = () => { - const navigate = useNavigate(); - const [stats, setStats] = useState(null); - const [recentTasks, setRecentTasks] = useState([]); - const [loading, setLoading] = useState(true); - - const processingCount = Number(stats?.processingTasks || 0); - const dashboardLoading = loading && processingCount > 0; - - useEffect(() => { - fetchDashboardData(); - const timer = setInterval(fetchDashboardData, 5000); - return () => clearInterval(timer); - }, []); - - const fetchDashboardData = async () => { - try { - const [statsRes, tasksRes] = await Promise.all([getDashboardStats(), getRecentTasks()]); - setStats(statsRes.data.data); - setRecentTasks(tasksRes.data.data || []); - } catch (err) { - console.error('Dashboard data load failed', err); - } finally { - setLoading(false); - } - }; - - const renderTaskProgress = (item: MeetingVO) => { - const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status); - - return ( -
- : , - description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI转录中' : '排队中') - }, - { - title: '智能总结', - icon: item.status === 2 ? : , - description: item.status === 3 ? '总结完成' : (item.status === 2 ? '正在生成' : '待执行') - }, - { - title: '分析完成', - icon: item.status === 3 ? : , - } - ]} - /> -
- ); - }; - - const statCards = [ - { label: '累计会议记录', value: stats?.totalMeetings, icon: , color: '#1890ff' }, - { - label: '当前分析中任务', - value: stats?.processingTasks, - icon: processingCount > 0 ? : , - color: '#faad14' - }, - { label: '今日新增分析', value: stats?.todayNew, icon: , color: '#52c41a' }, - { label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: , color: '#13c2c2' }, - ]; - - return ( -
-
- - {statCards.map((s, idx) => ( - - - {s.label}} - value={s.value || 0} - valueStyle={{ color: s.color, fontWeight: 700 }} - prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} - /> - - - ))} - - - - 最近任务动态 - -
- } - bordered={false} - style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.04)' }} - > - ( - -
- - - - navigate(`/meetings/${item.id}`)}> - {item.title} - - }> - {dayjs(item.meetingTime).format('MM-DD HH:mm')} - {item.participants || item.creatorName || '未指定'} - -
- {item.tags?.split(',').filter(Boolean).map((t) => ( - {t} - ))} -
-
- - - - {renderTaskProgress(item)} - - - - - -
- - -
-
- )} - locale={{ emptyText: }} - /> - -
- - -
- ); -}; - -export default Dashboard; diff --git a/frontend/src/pages/Devices.tsx b/frontend/src/pages/Devices.tsx deleted file mode 100644 index 01fc8ee..0000000 --- a/frontend/src/pages/Devices.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { - Button, - Form, - Input, - Drawer, - Popconfirm, - Space, - Table, - Tag, - Select, - Typography, - Card, - message -} from "antd"; -import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { createDevice, deleteDevice, listDevices, updateDevice, listUsers } from "../api"; -import type { DeviceInfo, SysUser } from "../types"; -import { usePermission } from "../hooks/usePermission"; -import { useDict } from "../hooks/useDict"; -import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - SearchOutlined, - DesktopOutlined, - UserOutlined -} from "@ant-design/icons"; -import PageHeader from "../components/shared/PageHeader"; -import { getStandardPagination } from "../utils/pagination"; - -const { Title, Text } = Typography; - -export default function Devices() { - const { t } = useTranslation(); - const { can } = usePermission(); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [data, setData] = useState([]); - const [users, setUsers] = useState([]); - - // Dictionaries - const { items: statusDict } = useDict("sys_common_status"); - - // Search state - const [searchText, setSearchText] = useState(""); - - // Drawer state - const [open, setOpen] = useState(false); - const [editing, setEditing] = useState(null); - const [form] = Form.useForm(); - - const loadData = async () => { - setLoading(true); - try { - const [deviceList, usersList] = await Promise.all([listDevices(), listUsers()]); - setData(deviceList || []); - setUsers(usersList || []); - } catch (e) { - // Handled by interceptor - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, []); - - const userMap = useMemo(() => { - const map: Record = {}; - users.forEach(u => map[u.userId] = u); - return map; - }, [users]); - - const filteredData = useMemo(() => { - if (!searchText) return data; - const lower = searchText.toLowerCase(); - return data.filter(d => { - const user = userMap[d.userId]; - return d.deviceCode.toLowerCase().includes(lower) || - (d.deviceName && d.deviceName.toLowerCase().includes(lower)) || - (user && user.displayName.toLowerCase().includes(lower)) || - String(d.userId).includes(lower); - }); - }, [data, searchText, userMap]); - - const openCreate = () => { - setEditing(null); - form.resetFields(); - form.setFieldsValue({ status: 1 }); - setOpen(true); - }; - - const openEdit = (record: DeviceInfo) => { - setEditing(record); - form.setFieldsValue(record); - setOpen(true); - }; - - const submit = async () => { - try { - const values = await form.validateFields(); - setSaving(true); - const payload: Partial = { - userId: values.userId, - deviceCode: values.deviceCode, - deviceName: values.deviceName, - status: values.status - }; - if (editing) { - await updateDevice(editing.deviceId, payload); - message.success(t('common.success')); - } else { - await createDevice(payload); - message.success(t('common.success')); - } - setOpen(false); - loadData(); - } catch (e) { - // Handled by interceptor - } finally { - setSaving(false); - } - }; - - const remove = async (id: number) => { - try { - await deleteDevice(id); - message.success(t('common.success')); - loadData(); - } catch (e) { - // Handled by interceptor - } - }; - - const columns = [ - { - title: t('devices.deviceInfo'), - key: "device", - render: (_: any, record: DeviceInfo) => ( - -
-
-
-
{record.deviceName || "未命名设备"}
-
{record.deviceCode}
-
-
- ), - }, - { - title: t('devices.owner'), - key: "user", - render: (_: any, record: DeviceInfo) => { - const user = userMap[record.userId]; - return user ? ( - - - ) : ( - ID: {record.userId} - ); - } - }, - { - title: t('common.status'), - dataIndex: "status", - width: 100, - render: (status: number) => { - const item = statusDict.find(i => i.itemValue === String(status)); - return ( - - {item ? item.itemLabel : (status === 1 ? "启用" : "禁用")} - - ); - }, - }, - { - title: t('devices.updateTime'), - dataIndex: "updatedAt", - width: 180, - render: (text: string) => {text?.replace('T', ' ').substring(0, 19)} - }, - { - title: t('common.action'), - key: "action", - width: 120, - fixed: "right" as const, - render: (_: any, record: DeviceInfo) => ( - - {can("device:update") && ( - - )} - /> - - -
- } - style={{ width: 350 }} - value={searchText} - onChange={(e) => setSearchText(e.target.value)} - allowClear - aria-label={t('common.search')} - /> -
-
- - - - - - -
setTypeParams({ ...typeParams, current: page, size })), - simple: true, - size: 'small', - position: ['bottomCenter'] - }} - size="small" - showHeader={false} - scroll={{ y: 'calc(100vh - 480px)' }} - onRow={(record) => ({ - onClick: () => setSelectedType(record), - className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}` - })} - columns={[ - { - render: (_, record) => ( -
-
-
{record.typeName}
-
{record.typeCode}
-
-
- {can("sys_dict:type:update") && ( -
-
- ) - } - ]} - /> - - - - - - -
{text} - }, - { - title: t('dicts.itemValue'), - dataIndex: "itemValue", - className: "tabular-nums" - }, - { - title: t('dicts.sort'), - dataIndex: "sortOrder", - width: 80, - className: "tabular-nums" - }, - { - title: t('common.status'), - dataIndex: "status", - width: 100, - render: (v) => { - const item = statusDict.find(i => i.itemValue === String(v)); - return ( - - {item ? item.itemLabel : (v === 1 ? "启用" : "禁用")} - - ); - } - }, - { - title: t('common.action'), - width: 120, - fixed: "right" as const, - render: (_, record) => ( - - {can("sys_dict:item:update") && ( - - - - } - > - - - - - - - - - - - - - - {/* Item Drawer */} - - - } - open={itemDrawerVisible} - onClose={() => setItemDrawerVisible(false)} - width={400} - destroyOnClose - footer={ -
- - -
- } - > - - - - - - - - - - - - - - - } - placeholder={t('login.username')} - autoComplete="username" - spellCheck={false} - aria-label={t('login.username')} - /> - - - - - - {captchaEnabled && ( - -
- } - placeholder={t('login.captcha')} - maxLength={6} - aria-label={t('login.captcha')} - /> - -
-
- )} - -
- - {t('login.rememberMe')} - - {t('login.forgotPassword')} -
- - - - - - - - - - ); -} diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx deleted file mode 100644 index b01a6c2..0000000 --- a/frontend/src/pages/Logs.tsx +++ /dev/null @@ -1,359 +0,0 @@ -import { Card, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd"; -import { useEffect, useState, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { fetchLogs } from "../api"; -import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined, FileTextOutlined } from "@ant-design/icons"; -import { SysLog, UserProfile } from "../types"; -import { useDict } from "../hooks/useDict"; -import PageHeader from "../components/shared/PageHeader"; -import { getStandardPagination } from "../utils/pagination"; -import ListTable from "../components/shared/ListTable/ListTable"; - -const { RangePicker } = DatePicker; -const { Text, Title } = Typography; - -export default function Logs() { - const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState("OPERATION"); - const [loading, setLoading] = useState(false); - const [data, setData] = useState([]); - const [total, setTotal] = useState(0); - const [params, setParams] = useState({ - current: 1, - size: 20, - username: "", - status: undefined, - startDate: "", - endDate: "", - operation: "", - sortField: "createdAt", - sortOrder: "descend" as any - }); - - // Dictionaries - const { items: logTypeDict } = useDict("sys_log_type"); - const { items: logStatusDict } = useDict("sys_log_status"); - - // Get user profile to check platform admin - const userProfile = useMemo(() => { - const stored = sessionStorage.getItem("userProfile"); - if (!stored) return null; - try { - return JSON.parse(stored) as UserProfile; - } catch (e) { - return null; - } - }, []); - - const isPlatformAdmin = userProfile?.isPlatformAdmin && userProfile?.tenantId === 0; - - // Modal for detail view - const [detailModalVisible, setDetailModalVisible] = useState(false); - const [selectedLog, setSelectedLog] = useState(null); - - const loadData = async (currentParams = params) => { - setLoading(true); - try { - // Use logType for precise filtering - const result = await fetchLogs({ ...currentParams, logType: activeTab }); - setData(result.records || []); - setTotal(result.total || 0); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, [activeTab, params.current, params.size, params.sortField, params.sortOrder]); - - const onTabChange = (key: string) => { - setActiveTab(key); - setParams(prev => ({ ...prev, current: 1 })); - }; - - const handleTableChange = (pagination: any, filters: any, sorter: any) => { - setParams({ - ...params, - current: pagination.current, - size: pagination.pageSize, - sortField: sorter.field || "createdAt", - sortOrder: sorter.order || "descend" - }); - }; - - const handleSearch = () => { - setParams({ ...params, current: 1 }); - loadData({ ...params, current: 1 }); - }; - - const handleReset = () => { - const resetParams = { - current: 1, - size: 20, - username: "", - status: undefined, - startDate: "", - endDate: "", - operation: "", - sortField: "createdAt", - sortOrder: "descend" as any - }; - setParams(resetParams); - loadData(resetParams); - }; - - const showDetail = (record: SysLog) => { - setSelectedLog(record); - setDetailModalVisible(true); - }; - - const renderDuration = (ms: number) => { - if (!ms && ms !== 0) return "-"; - let color = ""; - if (ms > 1000) color = "#ff4d4f"; // 红色 (慢) - else if (ms > 300) color = "#faad14"; // 橘色 (中) - - return ( - 300 ? 600 : 400 }}> - {ms}ms - - ); - }; - - const columns = [ - ...(isPlatformAdmin ? [{ - title: t('users.tenant'), - dataIndex: "tenantName", - key: "tenantName", - width: 150, - render: (text: string) => {text || "系统平台"} - }] : []), - { - title: t('logs.opAccount'), - dataIndex: "username", - key: "username", - width: 120, - render: (text: string) => {text || "系统"} - }, - { - title: t('logs.opDetail'), - dataIndex: "operation", - key: "operation", - ellipsis: true, - render: (text: string) => {text} - }, - { - title: t('logs.ip'), - dataIndex: "ip", - key: "ip", - width: 130, - className: "tabular-nums" - }, - { - title: t('logs.duration'), - dataIndex: "duration", - key: "duration", - width: 100, - sorter: true, - sortOrder: params.sortField === 'duration' ? params.sortOrder : null, - render: renderDuration - }, - { - title: t('common.status'), - dataIndex: "status", - key: "status", - width: 90, - render: (status: number) => { - const item = logStatusDict.find(i => i.itemValue === String(status)); - return ( - - {item ? item.itemLabel : (status === 1 ? "成功" : "失败")} - - ); - } - }, - { - title: t('logs.time'), - dataIndex: "createdAt", - key: "createdAt", - width: 180, - sorter: true, - sortOrder: params.sortField === 'createdAt' ? params.sortOrder : null, - className: "tabular-nums", - render: (text: string) => text?.replace('T', ' ').substring(0, 19) - }, - { - title: t('common.action'), - key: "action", - width: 60, - fixed: "right" as const, - render: (_: any, record: SysLog) => ( - - - - )} - - - {selectedTenantId !== undefined ? ( -
- ) : ( -
- -
- )} - - - - - - - - - - - setQuery({ ...query, name: e.target.value })} - prefix={
record.permType !== 'button' && !!record.children?.length - }} - /> - - - - - - - - - - - - - - {t('permissions.permCode')} - - - - - } - name="code" - dependencies={["permType"]} - rules={[ - ({ getFieldValue }) => ({ - required: getFieldValue("permType") === "button", - message: "按钮权限必须填写编码" - }) - ]} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ value: Number(i.itemValue), label: i.itemLabel }))} /> - - - - - - - - - - - ); -} diff --git a/frontend/src/pages/PlatformSettings.tsx b/frontend/src/pages/PlatformSettings.tsx deleted file mode 100644 index f6034fc..0000000 --- a/frontend/src/pages/PlatformSettings.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { - Button, - Card, - Col, - Divider, - Form, - Input, - Row, - Typography, - Upload, - message, -} from "antd"; -import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - FileTextOutlined, - GlobalOutlined, - PictureOutlined, - SaveOutlined, - UploadOutlined, -} from "@ant-design/icons"; -import { getAdminPlatformConfig, updatePlatformConfig, uploadPlatformAsset } from "../api"; -import type { SysPlatformConfig } from "../types"; -import PageHeader from "../components/shared/PageHeader"; - -const { Text } = Typography; - -const cardStyle = { - boxShadow: "0 1px 2px rgba(0,0,0,0.08), 0 1px 6px -1px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.05)", -}; - -export default function PlatformSettings() { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [form] = Form.useForm(); - - const logoUrl = Form.useWatch("logoUrl", form); - const iconUrl = Form.useWatch("iconUrl", form); - const loginBgUrl = Form.useWatch("loginBgUrl", form); - const projectName = Form.useWatch("projectName", form); - const icpInfo = Form.useWatch("icpInfo", form); - const copyrightInfo = Form.useWatch("copyrightInfo", form); - - useEffect(() => { - const loadConfig = async () => { - setLoading(true); - try { - const data = await getAdminPlatformConfig(); - form.setFieldsValue(data); - } finally { - setLoading(false); - } - }; - - void loadConfig(); - }, [form]); - - const handleUpload = async (file: File, fieldName: keyof SysPlatformConfig) => { - try { - const url = await uploadPlatformAsset(file); - form.setFieldValue(fieldName, url); - message.success(t("common.success")); - } catch { - // handled by interceptor - } - return false; - }; - - const onFinish = async (values: SysPlatformConfig) => { - setSaving(true); - try { - await updatePlatformConfig(values); - sessionStorage.setItem("platformConfig", JSON.stringify(values)); - message.success(t("common.success")); - } finally { - setSaving(false); - } - }; - - const footerPreview = useMemo(() => { - return [icpInfo, copyrightInfo].filter(Boolean).join(" | "); - }, [copyrightInfo, icpInfo]); - - const renderImagePreview = (url?: string, label?: string) => ( -
- {url ? ( - {label} - ) : ( -
- -
{t("platformSettings.uploadHint")}
-
- )} -
- ); - - return ( -
-
- } - loading={saving} - onClick={() => form.submit()} - > - {t("common.save")} - - } - /> -
- -
-
- -
- - - 基础信息 - - } - > - - - - - - - - - - - - - 视觉资源 - - } - > - - - - - - {renderImagePreview(logoUrl, "Logo")} - handleUpload(file, "logoUrl")} - > - - - - - - - - - {renderImagePreview(iconUrl, "Icon")} - handleUpload(file, "iconUrl")} - > - - - - - - - - - {renderImagePreview(loginBgUrl, "Background")} - handleUpload(file, "loginBgUrl")} - > - - - - - - - - - - - 合规与版权 - - } - > - - - - - - - - - - - - - - - 页面展示预览 - -
-
- {projectName || "iMeeting"} -
- - 保存后将展示在系统底部与登录页底部。 - - -
- {icpInfo || "未填写 ICP 备案号"} - {copyrightInfo || "未填写版权信息"} -
- {footerPreview ? ( -
- {footerPreview} -
- ) : null} -
- - - - - - - ); -} diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx deleted file mode 100644 index c51505f..0000000 --- a/frontend/src/pages/Profile.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { - Button, - Card, - Form, - Input, - message, - Tabs, - Typography, - Row, - Col, - Space, - Avatar -} from "antd"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { getCurrentUser, updateMyProfile, updateMyPassword } from "../api"; -import { UserOutlined, LockOutlined, SaveOutlined, SolutionOutlined } from "@ant-design/icons"; -import type { UserProfile } from "../types"; -import PageHeader from "../components/shared/PageHeader"; - -const { Title, Text } = Typography; - -export default function Profile() { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [user, setUser] = useState(null); - const [profileForm] = Form.useForm(); - const [pwdForm] = Form.useForm(); - - const loadUser = async () => { - setLoading(true); - try { - const data = await getCurrentUser(); - setUser(data); - profileForm.setFieldsValue(data); - } catch (e) { - // Interceptor handles error - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadUser(); - }, []); - - const handleUpdateProfile = async () => { - try { - const values = await profileForm.validateFields(); - setSaving(true); - await updateMyProfile(values); - message.success(t('common.success')); - loadUser(); - } catch (e) { - } finally { - setSaving(false); - } - }; - - const handleUpdatePassword = async () => { - try { - const values = await pwdForm.validateFields(); - setSaving(true); - await updateMyPassword(values); - message.success(t('common.success')); - pwdForm.resetFields(); - } catch (e) { - } finally { - setSaving(false); - } - }; - - return ( -
- - - -
- - } style={{ backgroundColor: '#1677ff', marginBottom: 16 }} /> - {user?.displayName} - @{user?.username} -
- {user?.isPlatformAdmin ? 平台管理员 : 普通用户} -
-
- - - - - - 基本信息} - key="basic" - > -
- - - - - - - - - - - -
- - 安全设置} - key="password" - > -
- - - - - - - ({ - validator(_, value) { - if (!value || getFieldValue('newPassword') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致')); - }, - }), - ]} - > - - - - -
-
-
- - - - ); -} - -import { Tag } from "antd"; diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx deleted file mode 100644 index 4b2ea7d..0000000 --- a/frontend/src/pages/ResetPassword.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Button, Card, Form, Input, message, Typography, Layout } from "antd"; -import { useState } from "react"; -import { updateMyPassword } from "../api"; -import { LockOutlined, LogoutOutlined } from "@ant-design/icons"; -import { useNavigate } from "react-router-dom"; - -const { Title, Text } = Typography; - -export default function ResetPassword() { - const [loading, setLoading] = useState(false); - const navigate = useNavigate(); - const [form] = Form.useForm(); - - const onFinish = async (values: any) => { - setLoading(true); - try { - await updateMyPassword({ - oldPassword: values.oldPassword, - newPassword: values.newPassword - }); - message.success("密码修改成功,请重新登录"); - // 清理并重新登录 - localStorage.clear(); - sessionStorage.clear(); - navigate("/login"); - } catch (e) { - } finally { - setLoading(false); - } - }; - - const handleLogout = () => { - localStorage.clear(); - sessionStorage.clear(); - navigate("/login"); - }; - - return ( - - -
- - 强制修改密码 - 为了您的账户安全,首次登录或密码被重置后需要修改密码方可继续使用系统。 -
- -
- - } /> - - - - } /> - - - ({ - validator(_, value) { - if (!value || getFieldValue('newPassword') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致')); - }, - }), - ]} - > - } /> - - - - - - -
-
- ); -} diff --git a/frontend/src/pages/Roles.css b/frontend/src/pages/Roles.css deleted file mode 100644 index 3d8071d..0000000 --- a/frontend/src/pages/Roles.css +++ /dev/null @@ -1,134 +0,0 @@ -.roles-page-v2 { - background-color: #f0f2f5; -} - -.shadow-sm { - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02); -} - -/* Role List Styling */ -.role-list-container-v3 { - scrollbar-width: thin; - scrollbar-color: #e8e8e8 transparent; -} - -.role-list-container-v3::-webkit-scrollbar { - width: 6px; -} - -.role-list-container-v3::-webkit-scrollbar-thumb { - background-color: #e8e8e8; - border-radius: 3px; -} - -.role-item-card-v3 { - padding: 12px 16px; - margin-bottom: 8px; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); - display: flex; - justify-content: space-between; - align-items: center; - border: 1px solid transparent; - background: #fafafa; -} - -.role-item-card-v3:hover { - background: #f0f7ff; - border-color: #e6f4ff; -} - -.role-item-card-v3.active { - background: #e6f4ff; - border-color: #1890ff; -} - -.role-item-card-v3.active .role-name { - color: #1890ff; -} - -.role-item-main { - flex: 1; - min-width: 0; -} - -.role-item-name-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 2px; -} - -.role-name { - font-size: 14px; - color: #262626; - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.role-code { - font-size: 12px; - color: #8c8c8c; - display: block; -} - -.role-item-actions { - opacity: 0; - transition: opacity 0.2s; - flex-shrink: 0; - margin-left: 8px; -} - -.role-item-card-v3:hover .role-item-actions { - opacity: 1; -} - -/* Tabs Styling */ -.role-detail-tabs .ant-tabs-nav { - margin-bottom: 0 !important; - padding: 0 24px; - background: #fff; -} - -.role-detail-tabs .ant-tabs-content-holder { - background: #fff; - padding-top: 24px; -} - -/* Tree Styling */ -.permission-tree-wrapper { - background: #fafafa; - border: 1px solid #f0f0f0; - border-radius: 8px; - padding: 20px; -} - -.role-permission-node { - display: flex; - align-items: center; -} - -.ant-tree-treenode { - padding: 4px 0 !important; -} - -.ant-tree-node-content-wrapper { - transition: background-color 0.2s; -} - -.ant-tree-node-content-wrapper:hover { - background-color: #e6f4ff !important; -} - -/* Table Styling */ -.ant-table-small { - background: transparent; -.full-height-card { - height: 100%; -} - -.shadow-sm { - diff --git a/frontend/src/pages/Roles.tsx b/frontend/src/pages/Roles.tsx deleted file mode 100644 index d93ee63..0000000 --- a/frontend/src/pages/Roles.tsx +++ /dev/null @@ -1,647 +0,0 @@ -import { - Button, - Card, - Drawer, - Form, - Input, - message, - Popconfirm, - Space, - Table, - Tag, - Typography, - Tree, - Row, - Col, - Tabs, - Empty, - Select, - Modal, - Tooltip, - Divider, - Switch, - Badge, - Avatar, - List -} from "antd"; -import type { DataNode } from "antd/es/tree"; -import { useEffect, useMemo, useState, useCallback } from "react"; -import { useTranslation } from "react-i18next"; -import { - createRole, - listPermissions, - listRolePermissions, - listRoles, - saveRolePermissions, - updateRole, - deleteRole, - fetchUsersByRoleId, - bindUsersToRole, - unbindUserFromRole, - listUsers -} from "../api"; -import { SysPermission, SysRole, SysTenant, SysUser } from "../types"; -import { usePermission } from "../hooks/usePermission"; -import { useDict } from "../hooks/useDict"; -import { - EditOutlined, - PlusOutlined, - SafetyCertificateOutlined, - SearchOutlined, - DeleteOutlined, - KeyOutlined, - UserOutlined, - SaveOutlined, - UserAddOutlined, - TeamOutlined, - FilterOutlined, - ApartmentOutlined -} from "@ant-design/icons"; -import PageHeader from "../components/shared/PageHeader"; -import "./Roles.css"; - -const { Title, Text } = Typography; - -const DEFAULT_STATUS = 1; - -type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; - -const buildPermissionTree = (list: SysPermission[]): PermissionNode[] => { - if (!list || list.length === 0) return []; - const active = list.filter((p) => p.status !== 0); - const map = new Map(); - const roots: PermissionNode[] = []; - - active.forEach((item) => { - map.set(item.permId, { ...item, key: item.permId, children: [] }); - }); - - map.forEach((node) => { - if (node.parentId && node.parentId !== 0) { - const parent = map.get(node.parentId); - if (parent) { - parent.children!.push(node); - } - } else { - roots.push(node); - } - }); - - const sortNodes = (nodes: PermissionNode[]) => { - nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - nodes.forEach((n) => n.children && sortNodes(n.children)); - }; - sortNodes(roots); - return roots; -}; - -const toTreeData = (nodes: PermissionNode[], t: any): DataNode[] => - nodes.map((node) => ({ - key: node.permId, - title: ( - - {node.name} - {node.permType === "button" && ( - - {t('permissions.permType') === '按钮' ? '按钮' : 'BTN'} - - )} - - ), - children: node.children && node.children.length > 0 ? toTreeData(node.children, t) : undefined - })); - -const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`; - -export default function Roles() { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [data, setData] = useState([]); - const [permissions, setPermissions] = useState([]); - const [selectedRole, setSelectedRole] = useState(null); - - const { items: statusDict } = useDict("sys_common_status"); - - const isPlatformMode = useMemo(() => { - const profileStr = sessionStorage.getItem("userProfile"); - if (profileStr) { - const profile = JSON.parse(profileStr); - return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0"; - } - return false; - }, []); - - const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []); - - const [selectedPermIds, setSelectedPermIds] = useState([]); - const [halfCheckedIds, setHalfCheckedIds] = useState([]); - const [roleUsers, setRoleUsers] = useState([]); - const [loadingUsers, setLoadingUsers] = useState(false); - - const [allUsers, setAllUsers] = useState([]); - const [userModalOpen, setUserModalOpen] = useState(false); - const [selectedUserKeys, setSelectedUserKeys] = useState([]); - const [userSearchText, setUserSearchText] = useState(""); - - const [searchText, setSearchText] = useState(""); - const [filterTenantId, setFilterTenantId] = useState(undefined); - - const [drawerOpen, setDrawerOpen] = useState(false); - const [editing, setEditing] = useState(null); - const [tenants, setTenants] = useState([]); - const [form] = Form.useForm(); - - const { can } = usePermission(); - - const loadTenants = async () => { - if (!isPlatformMode) return; - try { - const resp = await (await import("../api")).listTenants({ current: 1, size: 100 }); - setTenants(resp.records || []); - } catch (e) {} - }; - - useEffect(() => { - loadTenants(); - }, [isPlatformMode]); - - const permissionTreeData = useMemo( - () => toTreeData(buildPermissionTree(permissions), t), - [permissions, t] - ); - - const loadAllUsers = async () => { - try { - const list = await listUsers(); - setAllUsers(list || []); - } catch (e) { - console.error(e); - } - }; - - const openUserModal = () => { - loadAllUsers(); - setSelectedUserKeys([]); - setUserModalOpen(true); - }; - - const handleAddUsers = async () => { - if (!selectedRole || selectedUserKeys.length === 0) return; - try { - await bindUsersToRole(selectedRole.roleId, selectedUserKeys); - message.success(t('common.success')); - setUserModalOpen(false); - selectRole(selectedRole); - } catch (e) {} - }; - - const handleUnbindUser = async (userId: number) => { - if (!selectedRole) return; - - // 安全校验:租户管理员角色至少保留一个关联用户 - if (selectedRole.roleCode === 'TENANT_ADMIN' && roleUsers.length <= 1) { - message.warning('租户管理员角色必须至少保留一个关联用户,以防止租户孤立'); - return; - } - - try { - await unbindUserFromRole(selectedRole.roleId, userId); - message.success(t('common.success')); - selectRole(selectedRole); - } catch (e) {} - }; - - const filteredModalUsers = useMemo(() => { - const existingIds = new Set(roleUsers.map(u => u.userId)); - return allUsers.filter(u => - !existingIds.has(u.userId) && - (u.username.toLowerCase().includes(userSearchText.toLowerCase()) || - u.displayName.toLowerCase().includes(userSearchText.toLowerCase())) - ); - }, [allUsers, roleUsers, userSearchText]); - - const loadPermissions = async () => { - try { - const list = await listPermissions(); - setPermissions(list || []); - } catch (e) { - setPermissions([]); - } - }; - - const loadRoles = async () => { - setLoading(true); - try { - const list = await listRoles(isPlatformMode ? filterTenantId : activeTenantId); - let roles = list || []; - setData(roles); - if (roles.length > 0 && !selectedRole) { - selectRole(roles[0]); - } else if (selectedRole) { - const updated = roles.find(r => r.roleId === selectedRole.roleId); - if (updated) setSelectedRole(updated); - } - await loadPermissions(); - } finally { - setLoading(false); - } - }; - - const selectRole = async (role: SysRole) => { - setSelectedRole(role); - try { - const ids = await listRolePermissions(role.roleId); - const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); - - const leafIds = normalized.filter(id => { - return !permissions.some(p => p.parentId === id); - }); - setSelectedPermIds(leafIds); - setHalfCheckedIds([]); - - setLoadingUsers(true); - const users = await fetchUsersByRoleId(role.roleId); - setRoleUsers(users || []); - } catch (e) {} finally { - setLoadingUsers(false); - } - }; - - useEffect(() => { - loadRoles(); - }, [filterTenantId]); - - useEffect(() => { - if (selectedRole && permissions.length > 0) { - const leafIds = selectedPermIds.filter(id => { - return !permissions.some(p => p.parentId === id); - }); - if (leafIds.length !== selectedPermIds.length) { - setSelectedPermIds(leafIds); - } - } - }, [permissions]); - - const filteredData = useMemo(() => { - if (!searchText) return data; - const lower = searchText.toLowerCase(); - return data.filter(r => - r.roleName.toLowerCase().includes(lower) || - r.roleCode.toLowerCase().includes(lower) - ); - }, [data, searchText]); - - const openCreate = () => { - setEditing(null); - form.resetFields(); - form.setFieldsValue({ - status: 1, - tenantId: isPlatformMode ? undefined : activeTenantId - }); - setDrawerOpen(true); - }; - - const openEditBasic = (e: React.MouseEvent, record: SysRole) => { - e.stopPropagation(); - setEditing(record); - form.setFieldsValue(record); - setDrawerOpen(true); - }; - - const handleRemove = async (e: React.MouseEvent, id: number) => { - e.stopPropagation(); - try { - await deleteRole(id); - message.success(t('common.success')); - if (selectedRole?.roleId === id) setSelectedRole(null); - loadRoles(); - } catch (e) {} - }; - - const submitBasic = async () => { - try { - const values = await form.validateFields(); - setSaving(true); - const payload: Partial = { - roleCode: editing?.roleCode || values.roleCode || generateRoleCode(), - roleName: values.roleName, - remark: values.remark, - status: values.status ?? DEFAULT_STATUS, - tenantId: values.tenantId - }; - - if (editing) { - await updateRole(editing.roleId, payload); - message.success(t('common.success')); - } else { - await createRole(payload); - message.success(t('common.success')); - } - - setDrawerOpen(false); - loadRoles(); - } catch (e) {} finally { - setSaving(false); - } - }; - - const savePermissions = async () => { - if (!selectedRole) return; - setSaving(true); - try { - const allPermIds = Array.from(new Set([...selectedPermIds, ...halfCheckedIds])); - await saveRolePermissions(selectedRole.roleId, allPermIds); - message.success(t('common.success')); - } catch (e) {} finally { - setSaving(false); - } - }; - - return ( -
- } - onClick={openCreate} - > - {t('common.create')} - - )} - /> - -
- - {/* Left: Role List Side */} -
- - - 角色列表 - - - } - bordered={false} - className="shadow-sm" - style={{ height: '100%', borderRadius: '12px', display: 'flex', flexDirection: 'column' }} - bodyStyle={{ flex: 1, overflow: 'hidden', padding: '16px', display: 'flex', flexDirection: 'column' }} - > - - {isPlatformMode && ( - } - value={searchText} - onChange={e => setSearchText(e.target.value)} - allowClear - style={{ borderRadius: '6px' }} - /> - - -
- }} - renderItem={(item) => ( -
selectRole(item)} - > -
-
- {item.roleName} - {isPlatformMode && ( - - {item.tenantId === 0 ? '系统' : (tenants.find(t => t.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`)} - - )} - {item.status === 0 && 已禁用} -
- {item.roleCode} -
-
- - -
-
- )} - /> -
-
- - - {/* Right: Permission and User Management */} - - {selectedRole ? ( - -
- -
-
-
{selectedRole.roleName}
- {selectedRole.roleCode} -
- - } - extra={ - - } - > - - 功能权限} - key="permissions" - > -
-
- { - const checked = Array.isArray(keys) ? keys : keys.checked; - const halfChecked = info.halfCheckedKeys || []; - setSelectedPermIds(checked.map(k => Number(k))); - setHalfCheckedIds(halfChecked.map(k => Number(k))); - }} - defaultExpandAll - /> -
-
-
- - 成员管理 ({roleUsers.length})} - key="users" - > -
-
- 已分配用户 - -
-
( - - } style={{ backgroundColor: '#f0f2f5', color: '#8c8c8c' }} /> -
-
{r.displayName}
-
@{r.username}
-
-
- ) - }, - { title: '手机号', dataIndex: 'phone', className: 'tabular-nums' }, - { title: '状态', dataIndex: 'status', width: 80, render: (s: number) => }, - { - title: '操作', - key: 'action', - width: 80, - render: (_, record) => ( - handleUnbindUser(record.userId)} disabled={!can("sys:role:update")}> -
setSelectedUserKeys(keys as number[]) - }} - columns={[ - { title: '显示名称', dataIndex: 'displayName' }, - { title: '用户名', dataIndex: 'username' }, - { title: '手机号', dataIndex: 'phone' } - ]} - /> - - - setDrawerOpen(false)} - width={420} - destroyOnClose - footer={ -
- - - - -
- } - > -
- - - - - - } - allowClear - style={{ width: 200 }} - /> - - -
- - - - - {editing ? t('sysParams.drawerTitleEdit') : t('sysParams.drawerTitleCreate')} - - } - open={drawerOpen} - onClose={() => setDrawerOpen(false)} - width={500} - destroyOnClose - footer={ -
- - -
- } - > - - - - - - - - - - -
- - ({ label: i.itemLabel, value: Number(i.itemValue) }))} - /> - - - - - - {t('sysParams.isSystem')} - - - - - } - name="isSystem" - valuePropName="checked" - getValueProps={(value) => ({ checked: value === 1 })} - getValueFromEvent={(checked) => (checked ? 1 : 0)} - > - - - - - - - - - - ); -} diff --git a/frontend/src/pages/Tenants.tsx b/frontend/src/pages/Tenants.tsx deleted file mode 100644 index 9efa186..0000000 --- a/frontend/src/pages/Tenants.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import { - Button, - Card, - Drawer, - Form, - Input, - message, - Popconfirm, - Space, - Tag, - Typography, - DatePicker, - Row, - Col, - Select, - List, - Avatar, - Tooltip, - Divider, - Empty -} from "antd"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { createTenant, deleteTenant, listTenants, updateTenant } from "../api"; -import { usePermission } from "../hooks/usePermission"; -import { useDict } from "../hooks/useDict"; -import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - SearchOutlined, - ReloadOutlined, - ShopOutlined, - CalendarOutlined, - PhoneOutlined, - UserOutlined, - ClockCircleOutlined -} from "@ant-design/icons"; -import type { SysTenant } from "../types"; -import PageHeader from "../components/shared/PageHeader"; -import dayjs from "dayjs"; -import { getStandardPagination } from "../utils/pagination"; - -const { Title, Text, Paragraph } = Typography; - -export default function Tenants() { - const { t } = useTranslation(); - const { can } = usePermission(); - - // Dictionaries - const { items: statusDict } = useDict("sys_common_status"); - - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [data, setData] = useState([]); - const [total, setTotal] = useState(0); - const [params, setParams] = useState({ - current: 1, - size: 12, - name: "", - code: "" - }); - - const [drawerOpen, setDrawerOpen] = useState(false); - const [editing, setEditing] = useState(null); - const [form] = Form.useForm(); - - const loadData = async (currentParams = params) => { - setLoading(true); - try { - const result = await listTenants(currentParams); - setData(result.records || []); - setTotal(result.total || 0); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, [params.current, params.size]); - - const handleSearch = () => { - setParams({ ...params, current: 1 }); - loadData({ ...params, current: 1 }); - }; - - const handleReset = () => { - const resetParams = { - current: 1, - size: 12, - name: "", - code: "" - }; - setParams(resetParams); - loadData(resetParams); - }; - - const openCreate = () => { - setEditing(null); - form.resetFields(); - form.setFieldsValue({ status: 1 }); - setDrawerOpen(true); - }; - - const openEdit = (record: SysTenant) => { - setEditing(record); - form.setFieldsValue({ - ...record, - expireTime: record.expireTime ? dayjs(record.expireTime) : null - }); - setDrawerOpen(true); - }; - - const handleDelete = async (id: number) => { - try { - await deleteTenant(id); - message.success(t('common.success')); - loadData(); - } catch (e) { - // Handled by interceptor - } - }; - - const submit = async () => { - try { - const values = await form.validateFields(); - setSaving(true); - const payload = { - ...values, - expireTime: values.expireTime ? values.expireTime.format("YYYY-MM-DD HH:mm:ss") : null - }; - - if (editing) { - await updateTenant(editing.id, payload); - message.success(t('common.success')); - } else { - await createTenant(payload); - message.success(t('common.success')); - } - setDrawerOpen(false); - loadData(); - } catch (e) { - // Handled by interceptor - } finally { - setSaving(false); - } - }; - - const renderTenantCard = (item: SysTenant) => { - const statusItem = statusDict.find(i => i.itemValue === String(item.status)); - const isExpired = item.expireTime && dayjs().isAfter(dayjs(item.expireTime)); - - return ( - - - openEdit(item)} style={{ color: '#1677ff' }} /> - - ), - can("sys_tenant:delete") && ( - handleDelete(item.id)}> - - - ) - ].filter(Boolean) as React.ReactNode[]} - > -
- } - style={{ backgroundColor: item.status === 1 ? '#e6f4ff' : '#fff1f0', color: item.status === 1 ? '#1677ff' : '#ff4d4f', marginRight: 12, borderRadius: '8px' }} - /> -
-
- - {item.tenantName} - - - {statusItem ? statusItem.itemLabel : (item.status === 1 ? "正常" : "禁用")} - -
- - CODE: {item.tenantCode} - -
-
- -
- -
- - {item.contactName || "-"} -
-
- - {item.contactPhone || "-"} -
-
- - - {item.expireTime ? item.expireTime.substring(0, 10) : t('tenants.forever')} - {isExpired && 已过期} - -
-
-
- - {item.remark && ( - <> - - - {item.remark} - - - )} -
-
- ); - }; - - return ( -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ label: t.tenantName, value: t.id }))} - suffixIcon={
{ - setCurrent(p); - setPageSize(s); - })} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ label: i.itemLabel, value: Number(i.itemValue) }))} /> - - - {isPlatformMode && ( - - - - - - )} - - - {isPlatformMode && ( - <> - 租户成员身份 - - - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - 1 && ( - - - setQuery({ ...query, name: event.target.value })} prefix={
record.permType !== "button" && !!record.children?.length }} /> + + + + + + + + + + {t("permissions.permCode")}} name="code" rules={[{ required: true, message: t("permissions.permCode") }]}> + + + + + + + + + + + + + + + + + + + + + + + { + setIconSearchKeyword(""); + setIconPickerOpen(true); + }} + onFocus={() => { + setIconSearchKeyword(""); + setIconPickerOpen(true); + }} + placeholder={t("permissionsExt.iconPlaceholder")} + prefix={renderSelectableIcon(selectedIcon) || + {iconPickerOpen ?
setIconSearchKeyword(event.target.value)} placeholder={t("permissionsExt.iconSearchPlaceholder")} prefix={
: null} +
+
+ + + + + + + + + + + + + ({ value: Number(item.itemValue), label: item.itemLabel }))} /> + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/access/roles/index.less b/frontend/src/pages/access/roles/index.less new file mode 100644 index 0000000..5a271dc --- /dev/null +++ b/frontend/src/pages/access/roles/index.less @@ -0,0 +1,328 @@ +.roles-page-v2 { + background: transparent; + display: flex; + flex-direction: column; +} + +.roles-layout { + flex: 1; + min-height: 0; + display: flex; + padding-bottom: 24px; +} + +.roles-layout__row { + width: 100%; + margin: 0 !important; + height: 100%; +} + +.roles-layout__side, +.roles-layout__detail { + height: 100%; + display: flex; + flex-direction: column; +} + +.roles-side-card, +.roles-detail-card { + flex: 1; + min-height: 0; + border-radius: 18px !important; + display: flex; + flex-direction: column; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important; + border: 1px solid rgba(226, 232, 240, 0.8) !important; +} + +.roles-side-card .ant-card-body, +.roles-detail-card .ant-card-body { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + padding: 16px !important; + overflow: hidden; +} + +.role-search-panel { + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 16px; +} + +.role-search-bar { + display: flex; + gap: 10px; +} + +.role-search-bar .ant-input-affix-wrapper { + flex: 1; + border-radius: 8px; +} + +.role-search-bar .ant-btn { + border-radius: 8px; +} + +.role-list-container-v3 { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding-right: 8px; + margin-right: -8px; + + /* Modern scrollbar */ + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: #e2e8f0; + border-radius: 10px; + &:hover { + background: #cbd5e1; + } + } +} + +.role-item-card-v3 { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + margin-bottom: 10px; + border: 1px solid #f1f5f9; + border-radius: 12px; + background: #ffffff; + transition: all 0.2s ease; + cursor: pointer; + position: relative; + overflow: hidden; + + &:hover { + border-color: #cbd5e1; + background: #f8fafc; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03); + } + + &.active { + border-color: #bfdbfe; + background: #eff6ff; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: #3b82f6; + } + + .role-name { + color: #1e3a8a; + font-weight: 600; + } + + .role-item-symbol { + background: #3b82f6; + color: #ffffff; + } + } +} + +.role-item-symbol { + width: 40px; + height: 40px; + border-radius: 10px; + background: #f1f5f9; + color: #64748b; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; + transition: all 0.2s ease; +} + +.role-item-main { + flex: 1; + min-width: 0; +} + +.role-item-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.role-name { + font-size: 14px; + font-weight: 500; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.role-code { + font-size: 12px; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.role-item-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.2s ease; + + .ant-btn { + color: #64748b; + &:hover { + color: #3b82f6; + background: #e2e8f0; + } + &.ant-btn-dangerous:hover { + color: #ef4444; + background: #fee2e2; + } + } +} + +.role-item-card-v3:hover .role-item-actions, +.role-item-card-v3.active .role-item-actions { + opacity: 1; +} + +.role-list-pagination { + flex-shrink: 0; + padding-top: 16px; + margin-top: auto; + border-top: 1px solid #f1f5f9; + display: flex; + justify-content: flex-end; +} + +/* Detail Card Adjustments */ +.role-detail-header { + display: flex; + align-items: center; + gap: 16px; +} + +.role-detail-icon { + width: 48px; + height: 48px; + border-radius: 12px; + background: #eff6ff; + color: #3b82f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.role-detail-heading { + display: flex; + flex-direction: column; + min-width: 0; +} + +.role-detail-title { + font-size: 18px; + font-weight: 600; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.role-detail-code { + font-size: 13px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.role-detail-tabs { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + + .ant-tabs-nav { + margin-bottom: 16px !important; + &::before { + border-bottom: 1px solid #f1f5f9; + } + } + + .ant-tabs-content-holder { + flex: 1; + min-height: 0; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: #e2e8f0; + border-radius: 10px; + } + } +} + +.role-detail-pane { + padding: 4px; +} + +.permission-tree-wrapper { + padding: 16px; + background: #f8fafc; + border-radius: 12px; + border: 1px solid #e2e8f0; +} + +.role-members-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + + h5.ant-typography { + color: #334155; + font-weight: 600; + } +} + +.roles-count-badge { + background: #f1f5f9 !important; + color: #64748b !important; + border: 1px solid #e2e8f0; +} + +.app-page__empty-state { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.78); + border-radius: 16px; + border: 1px dashed rgba(148, 163, 184, 0.5); + margin: 0; +} diff --git a/frontend/src/pages/access/roles/index.tsx b/frontend/src/pages/access/roles/index.tsx new file mode 100644 index 0000000..0ce2b03 --- /dev/null +++ b/frontend/src/pages/access/roles/index.tsx @@ -0,0 +1,537 @@ +import { Avatar, Badge, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd"; +import type { DataNode } from "antd/es/tree"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ApartmentOutlined, + CheckCircleFilled, + DeleteOutlined, + EditOutlined, + FilterOutlined, + KeyOutlined, + PlusOutlined, + SafetyCertificateOutlined, + SaveOutlined, + SearchOutlined, + TeamOutlined, + UserAddOutlined, + UserOutlined +} from "@ant-design/icons"; +import { + bindUsersToRole, + createRole, + deleteRole, + fetchUsersByRoleId, + listPermissions, + listRolePermissions, + listTenants, + listUsers, + pageRoles, + saveRolePermissions, + unbindUserFromRole, + updateRole +} from "@/api"; +import { useDict } from "@/hooks/useDict"; +import { usePermission } from "@/hooks/usePermission"; +import PageHeader from "@/components/shared/PageHeader"; +import type { SysPermission, SysRole, SysTenant, SysUser } from "@/types"; +import "./index.less"; + +const { Text, Title } = Typography; + +type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; +const DEFAULT_STATUS = 1; +const DEFAULT_ROLE_PAGE_SIZE = 10; + +function normalizeNumber(value: unknown): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; + } + if (value && typeof value === "object" && "value" in value) { + return normalizeNumber((value as { value?: unknown }).value); + } + return undefined; +} +function buildPermissionTree(list: SysPermission[]): PermissionNode[] { + const active = (list || []).filter((permission) => permission.status !== 0); + const map = new Map(); + const roots: PermissionNode[] = []; + + active.forEach((item) => { + map.set(item.permId, { ...item, key: item.permId, children: [] }); + }); + + map.forEach((node) => { + if (node.parentId && node.parentId !== 0) { + const parent = map.get(node.parentId); + if (parent) { + parent.children!.push(node); + } + } else { + roots.push(node); + } + }); + + const sortNodes = (nodes: PermissionNode[]) => { + nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)); + nodes.forEach((node) => node.children && sortNodes(node.children)); + }; + + sortNodes(roots); + return roots; +} + +function toTreeData(nodes: PermissionNode[], t: (key: string, options?: Record) => string, buttonShortLabel: string): DataNode[] { + return nodes.map((node) => ({ + key: node.permId, + title: ( + + {node.name} + {node.permType === "button" ? {buttonShortLabel} : null} + + ), + children: node.children?.length ? toTreeData(node.children, t, buttonShortLabel) : undefined + })); +} + +const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`; + +export default function Roles() { + const { t } = useTranslation(); + const { can } = usePermission(); + const { items: statusDict } = useDict("sys_common_status"); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [data, setData] = useState([]); + const [permissions, setPermissions] = useState([]); + const [selectedRole, setSelectedRole] = useState(null); + const [selectedPermIds, setSelectedPermIds] = useState([]); + const [halfCheckedIds, setHalfCheckedIds] = useState([]); + const [roleUsers, setRoleUsers] = useState([]); + const [loadingUsers, setLoadingUsers] = useState(false); + const [allUsers, setAllUsers] = useState([]); + const [userModalOpen, setUserModalOpen] = useState(false); + const [selectedUserKeys, setSelectedUserKeys] = useState([]); + const [userSearchText, setUserSearchText] = useState(""); + const [searchText, setSearchText] = useState(""); + const [rolePage, setRolePage] = useState({ current: 1, size: DEFAULT_ROLE_PAGE_SIZE, total: 0 }); + + const handleSearch = () => { + setSearchText((value) => value.trim()); + setRolePage((prev) => ({ ...prev, current: 1 })); + }; + + const handleResetSearch = () => { + setSearchText(""); + setFilterTenantId(undefined); + setRolePage((prev) => ({ ...prev, current: 1 })); + }; + const [filterTenantId, setFilterTenantId] = useState(undefined); + const [drawerOpen, setDrawerOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [tenants, setTenants] = useState([]); + const [form] = Form.useForm(); + + const isPlatformMode = useMemo(() => { + const profileStr = sessionStorage.getItem("userProfile"); + if (!profileStr) return false; + const profile = JSON.parse(profileStr); + return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0"; + }, []); + + const activeTenantId = useMemo(() => normalizeNumber(localStorage.getItem("activeTenantId")) ?? 0, []); + const buttonShortLabel = useMemo(() => { + const label = t("buttonShort"); + return !label || label === "buttonShort" ? "BTN" : label; + }, [t]); + const permissionTreeData = useMemo(() => toTreeData(buildPermissionTree(permissions), t, buttonShortLabel), [buttonShortLabel, permissions, t]); + + + useEffect(() => { + if (!isPlatformMode) return; + listTenants({ current: 1, size: 100 }).then((response) => setTenants(response.records || [])).catch(() => {}); + }, [isPlatformMode]); + + const loadPermissions = async () => { + try { + const list = await listPermissions(); + setPermissions(list || []); + } catch { + setPermissions([]); + } + }; + + const selectRole = async (role: SysRole) => { + setSelectedRole(role); + try { + const ids = await listRolePermissions(role.roleId); + const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); + const leafIds = normalized.filter((id) => !permissions.some((permission) => permission.parentId === id)); + setSelectedPermIds(leafIds); + setHalfCheckedIds([]); + + setLoadingUsers(true); + const users = await fetchUsersByRoleId(role.roleId); + setRoleUsers(users || []); + } finally { + setLoadingUsers(false); + } + }; + + const loadRoles = async (page = rolePage.current, size = rolePage.size) => { + setLoading(true); + try { + const response = await pageRoles({ + current: page, + size, + tenantId: isPlatformMode ? filterTenantId : activeTenantId, + keyword: searchText || undefined + }); + const roles = response?.records || []; + setRolePage({ current: page, size, total: response?.total || 0 }); + setData(roles); + if (roles.length === 0) { + setSelectedRole(null); + setRoleUsers([]); + setSelectedPermIds([]); + setHalfCheckedIds([]); + } else if (!selectedRole) { + await selectRole(roles[0]); + } else { + const updated = roles.find((role) => role.roleId === selectedRole.roleId); + if (updated) { + setSelectedRole(updated); + } else { + await selectRole(roles[0]); + } + } + await loadPermissions(); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadRoles(rolePage.current, rolePage.size); + }, [filterTenantId, rolePage.current, rolePage.size, searchText]); + + const loadAllUsers = async () => { + try { + const list = await listUsers(); + setAllUsers(list || []); + } catch { + setAllUsers([]); + } + }; + + const openUserModal = () => { + loadAllUsers(); + setSelectedUserKeys([]); + setUserModalOpen(true); + }; + + const handleAddUsers = async () => { + if (!selectedRole || selectedUserKeys.length === 0) return; + await bindUsersToRole(selectedRole.roleId, selectedUserKeys); + message.success(t("common.success")); + setUserModalOpen(false); + selectRole(selectedRole); + }; + + const handleUnbindUser = async (userId: number) => { + if (!selectedRole) return; + if (selectedRole.roleCode === "TENANT_ADMIN" && roleUsers.length <= 1) { + message.warning(t("rolesExt.tenantAdminWarning")); + return; + } + await unbindUserFromRole(selectedRole.roleId, userId); + message.success(t("common.success")); + selectRole(selectedRole); + }; + + const filteredModalUsers = useMemo(() => { + const existingIds = new Set(roleUsers.map((user) => user.userId)); + return allUsers.filter( + (user) => + !existingIds.has(user.userId) && + (user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase())) + ); + }, [allUsers, roleUsers, userSearchText]); + + + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ status: 1, tenantId: isPlatformMode ? undefined : activeTenantId }); + setDrawerOpen(true); + }; + + const openEditBasic = (event: React.MouseEvent, record: SysRole) => { + event.stopPropagation(); + setEditing(record); + form.setFieldsValue(record); + setDrawerOpen(true); + }; + + const handleRemove = async (event: React.MouseEvent, id: number) => { + event.stopPropagation(); + await deleteRole(id); + message.success(t("common.success")); + if (selectedRole?.roleId === id) setSelectedRole(null); + loadRoles(rolePage.current, rolePage.size); + }; + + const submitBasic = async () => { + const values = await form.validateFields(); + setSaving(true); + try { + const payload: Partial = { + roleCode: editing?.roleCode || values.roleCode || generateRoleCode(), + roleName: values.roleName, + remark: values.remark, + status: values.status ?? DEFAULT_STATUS, + tenantId: values.tenantId + }; + if (editing) { + await updateRole(editing.roleId, payload); + } else { + await createRole(payload); + } + message.success(t("common.success")); + setDrawerOpen(false); + loadRoles(rolePage.current, rolePage.size); + } finally { + setSaving(false); + } + }; + + const handleRolePageChange = (page: number, pageSize: number) => { + setRolePage((prev) => ({ ...prev, current: page, size: pageSize })); + }; + + const savePermissions = async () => { + if (!selectedRole) return; + setSaving(true); + try { + await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds]))); + message.success(t("common.success")); + } finally { + setSaving(false); + } + }; + + return ( +
+ + +
+ {can("sys:role:create") && } +
+ +
+ +
+ {t("rolesExt.roleList")} + {/**/} + } bordered={false} className="app-page__panel-card roles-side-card"> +
+ {isPlatformMode && ( + } value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear /> + +
+ + +
+ }} + renderItem={(item) => ( +
selectRole(item)}> + +
+
+ {item.roleName} + {isPlatformMode && {item.tenantId === 0 ? t("rolesExt.systemTenant") : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `${t("rolesExt.tenantLabel")}:${item.tenantId}`}} + {item.status === 0 && {t("rolesExt.disabled")}} +
+ {item.roleCode} +
+ {selectedRole?.roleId === item.roleId ? ( + + ) : null} +
+ + +
+
+ )} + /> +
+
+ +
+
+ + + + {selectedRole ? ( +
{selectedRole.roleName}
{selectedRole.roleCode}
} + extra={} + > + + {t("roles.funcPerms")}} key="permissions"> +
+
+ { + const checked = Array.isArray(keys) ? keys : keys.checked; + const halfChecked = info.halfCheckedKeys || []; + setSelectedPermIds(checked.map((key) => Number(key))); + setHalfCheckedIds(halfChecked.map((key) => Number(key))); + }} + defaultExpandAll + /> +
+
+
+ {t("rolesExt.membersTab")} ({roleUsers.length})} key="users"> +
+
+ {t("rolesExt.assignedUsers")} + +
+
( + + } style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} /> +
+
{user.displayName}
+
@{user.username}
+
+
+ ) + }, + { title: t("rolesExt.phone"), dataIndex: "phone", className: "tabular-nums" }, + { title: t("common.status"), dataIndex: "status", width: 80, render: (status: number) => {status === 1 ? t("logsExt.success") : t("rolesExt.disabled")} }, + { + title: t("common.action"), + key: "action", + width: 80, + render: (_: any, user: SysUser) => ( + handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}> +
setSelectedUserKeys(keys as number[]) }} columns={[{ title: t("rolesExt.displayName"), dataIndex: "displayName" }, { title: t("users.username"), dataIndex: "username" }, { title: t("rolesExt.phone"), dataIndex: "phone" }]} /> + + + setDrawerOpen(false)} width={420} destroyOnClose footer={
}> +
+ + + + + + ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={
{ setCurrent(page); setPageSize(size); })} /> + + + + + + + + + + + + ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> + {isPlatformMode && } + + {isPlatformMode && ( + <> + {t("usersExt.membershipsTitle")} + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + 1 && + + } + placeholder={t("login.username")} + autoComplete="username" + spellCheck={false} + aria-label={t("login.username")} + /> + + + + + + {captchaEnabled ? ( + +
+ } + placeholder={t("login.captcha")} + maxLength={6} + aria-label={t("login.captcha")} + /> + +
+
+ ) : null} + +
+ + {t("login.rememberMe")} + + {t("login.forgotPassword")} +
+ + + + + + +
+ + {t("login.demoAccount")} admin / {t("login.password")}{" "} + 123456 + +
+ + + + ); +} diff --git a/frontend/src/pages/auth/reset-password/index.tsx b/frontend/src/pages/auth/reset-password/index.tsx new file mode 100644 index 0000000..6786870 --- /dev/null +++ b/frontend/src/pages/auth/reset-password/index.tsx @@ -0,0 +1,90 @@ +import { Button, Card, Form, Input, Layout, Typography, message } from "antd"; +import { LockOutlined, LogoutOutlined } from "@ant-design/icons"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { updateMyPassword } from "@/api"; + +const { Title, Text } = Typography; + +type ResetPasswordFormValues = { + oldPassword: string; + newPassword: string; + confirmPassword: string; +}; + +export default function ResetPassword() { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const [form] = Form.useForm(); + + const goToLogin = () => { + localStorage.clear(); + sessionStorage.clear(); + navigate("/login"); + }; + + const onFinish = async (values: ResetPasswordFormValues) => { + setLoading(true); + try { + await updateMyPassword({ + oldPassword: values.oldPassword, + newPassword: values.newPassword + }); + message.success("密码已更新,请重新登录"); + goToLogin(); + } finally { + setLoading(false); + } + }; + + return ( + + +
+ + + 首次登录请修改密码 + + 当前账号被要求更新初始密码,提交成功后会跳转到登录页。 +
+ +
+ + } /> + + + + } /> + + + ({ + validator(_, value) { + if (!value || getFieldValue("newPassword") === value) { + return Promise.resolve(); + } + return Promise.reject(new Error("两次输入的新密码不一致")); + } + }) + ]} + > + } /> + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/RolePermissionBinding.tsx b/frontend/src/pages/bindings/role-permission/index.tsx similarity index 55% rename from frontend/src/pages/RolePermissionBinding.tsx rename to frontend/src/pages/bindings/role-permission/index.tsx index 874b08b..d672d82 100644 --- a/frontend/src/pages/RolePermissionBinding.tsx +++ b/frontend/src/pages/bindings/role-permission/index.tsx @@ -2,31 +2,31 @@ import { Button, Card, Col, + Empty, + Input, message, Row, Space, Table, Tag, Tree, - Typography, - Input, - Empty + Typography } from "antd"; import type { DataNode } from "antd/es/tree"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "../api"; -import { SearchOutlined, SafetyCertificateOutlined, SaveOutlined, KeyOutlined, ClusterOutlined } from "@ant-design/icons"; -import type { SysPermission, SysRole } from "../types"; -import PageHeader from "../components/shared/PageHeader"; +import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons"; +import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api"; +import PageHeader from "@/components/shared/PageHeader"; +import type { SysPermission, SysRole } from "@/types"; -const { Title, Text } = Typography; +const { Text } = Typography; type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] }; function buildPermissionTree(list: SysPermission[]): PermissionNode[] { - if (!list || list.length === 0) return []; - const active = list.filter((p) => p.status !== 0); + if (!list.length) return []; + const active = list.filter((item) => item.status !== 0); const map = new Map(); const roots: PermissionNode[] = []; @@ -46,23 +46,32 @@ function buildPermissionTree(list: SysPermission[]): PermissionNode[] { }); const sortNodes = (nodes: PermissionNode[]) => { - nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - nodes.forEach((n) => n.children && sortNodes(n.children)); + nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)); + nodes.forEach((node) => { + if (node.children?.length) { + sortNodes(node.children); + } + }); }; + sortNodes(roots); return roots; } -function toTreeData(nodes: PermissionNode[], t: any): DataNode[] { +function toTreeData(nodes: PermissionNode[]): DataNode[] { return nodes.map((node) => ({ key: node.permId, title: ( {node.name} - {node.permType === "button" && {t('permissions.permType') === '按钮' ? '按钮' : 'Button'}} + {node.permType === "button" ? ( + + Button + + ) : null} ), - children: node.children && node.children.length > 0 ? toTreeData(node.children, t) : undefined + children: node.children?.length ? toTreeData(node.children) : undefined })); } @@ -74,29 +83,34 @@ export default function RolePermissionBinding() { const [loadingPerms, setLoadingPerms] = useState(false); const [saving, setSaving] = useState(false); const [selectedRoleId, setSelectedRoleId] = useState(null); - - // Platform admin check - const isPlatformMode = useMemo(() => { - const profileStr = sessionStorage.getItem("userProfile"); - if (profileStr) { - const profile = JSON.parse(profileStr); - return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0"; - } - return false; - }, []); - - // Selection states const [checkedPermIds, setCheckedPermIds] = useState([]); const [halfCheckedIds, setHalfCheckedIds] = useState([]); - - // Search const [searchText, setSearchText] = useState(""); + const isPlatformMode = useMemo(() => { + const profileStr = sessionStorage.getItem("userProfile"); + if (!profileStr) { + return false; + } + const profile = JSON.parse(profileStr); + return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0"; + }, []); + const selectedRole = useMemo( - () => roles.find((r) => r.roleId === selectedRoleId) || null, + () => roles.find((role) => role.roleId === selectedRoleId) || null, [roles, selectedRoleId] ); + const filteredRoles = useMemo(() => { + if (!searchText) { + return roles; + } + const lower = searchText.toLowerCase(); + return roles.filter((role) => role.roleName.toLowerCase().includes(lower) || role.roleCode.toLowerCase().includes(lower)); + }, [roles, searchText]); + + const treeData = useMemo(() => toTreeData(buildPermissionTree(permissions)), [permissions]); + const loadRoles = async () => { setLoadingRoles(true); try { @@ -112,8 +126,6 @@ export default function RolePermissionBinding() { try { const list = await listPermissions(); setPermissions(list || []); - } catch (e) { - // Handled by interceptor } finally { setLoadingPerms(false); } @@ -123,15 +135,12 @@ export default function RolePermissionBinding() { try { const list = await listRolePermissions(roleId); const normalized = (list || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)); - - const leafIds = normalized.filter(id => { - return !permissions.some(p => p.parentId === id); - }); - + const leafIds = normalized.filter((id) => !permissions.some((permission) => permission.parentId === id)); setCheckedPermIds(leafIds); setHalfCheckedIds([]); - } catch (e) { + } catch { setCheckedPermIds([]); + setHalfCheckedIds([]); } }; @@ -145,73 +154,57 @@ export default function RolePermissionBinding() { loadRolePermissions(selectedRoleId); } else { setCheckedPermIds([]); + setHalfCheckedIds([]); } }, [selectedRoleId, permissions]); - const filteredRoles = useMemo(() => { - if (!searchText) return roles; - const lower = searchText.toLowerCase(); - return roles.filter(r => - r.roleName.toLowerCase().includes(lower) || - r.roleCode.toLowerCase().includes(lower) - ); - }, [roles, searchText]); - - const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]); - const antdTreeData = useMemo(() => toTreeData(treeData, t), [treeData, t]); - const handleSave = async () => { if (!selectedRoleId) { - message.warning(t('rolePerm.selectRole')); + message.warning(t("rolePerm.selectRole")); return; } setSaving(true); try { - const allPermIds = Array.from(new Set([...checkedPermIds, ...halfCheckedIds])); - await saveRolePermissions(selectedRoleId, allPermIds); - message.success(t('common.success')); - } catch (e) { - // Handled by interceptor + await saveRolePermissions(selectedRoleId, Array.from(new Set([...checkedPermIds, ...halfCheckedIds]))); + message.success(t("common.success")); } finally { setSaving(false); } }; return ( -
+
- +
setSelectedRoleId(keys[0] as number), + onChange: (keys) => setSelectedRoleId(keys[0] as number) }} onRow={(record) => ({ onClick: () => setSelectedRoleId(record.roleId), className: "cursor-pointer" })} - pagination={{ pageSize: 10, showTotal: (total) => t('common.total', { total }) }} + pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }} columns={[ - { - title: t('roles.roleName'), + { + title: t("roles.roleName"), key: "role", - render: (_, r) => ( + render: (_, record: SysRole) => (
-
{r.roleName}
-
{r.roleCode}
+
{record.roleName}
+
{record.roleCode}
) }, { - title: t('common.status'), + title: t("common.status"), dataIndex: "status", width: 80, - render: (v) => (v === 1 ? 正常 : 禁用) + render: (value: number) => (value === 1 ? 鍚敤 : 绂佺敤) } ]} /> - - + + - +
setSelectedUserId(keys[0] as number), + onChange: (keys) => setSelectedUserId(keys[0] as number) }} onRow={(record) => ({ onClick: () => setSelectedUserId(record.userId), className: "cursor-pointer" })} - pagination={{ pageSize: 10, showTotal: (total) => t('common.total', { total }) }} + pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }} columns={[ - { - title: t('users.userInfo'), + { + title: t("users.userInfo"), key: "user", - render: (_, r) => ( + render: (_: unknown, record: SysUser) => (
-
{r.displayName}
-
@{r.username}
+
{record.displayName}
+
@{record.username}
) }, { - title: t('common.status'), + title: t("common.status"), dataIndex: "status", width: 80, - render: (v) => (v === 1 ? 正常 : 禁用) + render: (value: number) => (value === 1 ? Enabled : Disabled) } ]} /> - - + + @@ -212,14 +187,12 @@ export default function UserRoleBinding() { ))} - {!roles.length && !loadingRoles && ( - - )} + {!roles.length && !loadingRoles && } ) : (
-
)} @@ -227,4 +200,4 @@ export default function UserRoleBinding() { ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx new file mode 100644 index 0000000..0d218f6 --- /dev/null +++ b/frontend/src/pages/dashboard/index.tsx @@ -0,0 +1,224 @@ +import React, { useState, useEffect } from 'react'; +import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; +import { + HistoryOutlined, + CheckCircleOutlined, + LoadingOutlined, + AudioOutlined, + RobotOutlined, + CalendarOutlined, + TeamOutlined, + RiseOutlined, + ClockCircleOutlined, + PlayCircleOutlined, + FileTextOutlined, +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import dayjs from 'dayjs'; +import { getDashboardStats, getRecentTasks, DashboardStats } from '@/api/business/dashboard'; +import { MeetingVO, getMeetingProgress, MeetingProgress } from '@/api/business/meeting'; + +const { Title, Text } = Typography; + +const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) => { + const [progress, setProgress] = useState(null); + + useEffect(() => { + if (meeting.status !== 1 && meeting.status !== 2) return; + + const fetchProgress = async () => { + try { + const res = await getMeetingProgress(meeting.id); + if (res.data?.data) { + setProgress(res.data.data); + } + } catch (err) { + // ignore + } + }; + + fetchProgress(); + const timer = setInterval(fetchProgress, 3000); + return () => clearInterval(timer); + }, [meeting.id, meeting.status]); + + if (meeting.status !== 1 && meeting.status !== 2) return null; + + const percent = progress?.percent || 0; + const isError = percent < 0; + + return ( +
+
+ + + {progress?.message || '准备分析中...'} + + {!isError && {percent}%} +
+ +
+ ); +}; + +export const Dashboard: React.FC = () => { + const navigate = useNavigate(); + const [stats, setStats] = useState(null); + const [recentTasks, setRecentTasks] = useState([]); + const [loading, setLoading] = useState(true); + + const processingCount = Number(stats?.processingTasks || 0); + const dashboardLoading = loading && processingCount > 0; + + useEffect(() => { + fetchDashboardData(); + const timer = setInterval(fetchDashboardData, 5000); + return () => clearInterval(timer); + }, []); + + const fetchDashboardData = async () => { + try { + const [statsRes, tasksRes] = await Promise.all([getDashboardStats(), getRecentTasks()]); + setStats(statsRes.data.data); + setRecentTasks(tasksRes.data.data || []); + } catch (err) { + console.error('Dashboard data load failed', err); + } finally { + setLoading(false); + } + }; + + const renderTaskProgress = (item: MeetingVO) => { + const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status); + + return ( +
+ : , + description: item.status > 1 ? '识别完成' : (item.status === 1 ? 'AI转录中' : '排队中') + }, + { + title: '智能总结', + icon: item.status === 2 ? : , + description: item.status === 3 ? '总结完成' : (item.status === 2 ? '正在生成' : '待执行') + }, + { + title: '分析完成', + icon: item.status === 3 ? : , + } + ]} + /> +
+ ); + }; + + const statCards = [ + { label: '累计会议记录', value: stats?.totalMeetings, icon: , color: '#1890ff' }, + { + label: '当前分析中任务', + value: stats?.processingTasks, + icon: processingCount > 0 ? : , + color: '#faad14' + }, + { label: '今日新增分析', value: stats?.todayNew, icon: , color: '#52c41a' }, + { label: 'AI 处理成功率', value: `${stats?.successRate || 100}%`, icon: , color: '#13c2c2' }, + ]; + + return ( +
+
+ + {statCards.map((s, idx) => ( +
+ + {s.label}} + value={s.value || 0} + valueStyle={{ color: s.color, fontWeight: 700 }} + prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} + /> + + + ))} + + + + 最近任务动态 + + + } + bordered={false} + style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.04)' }} + > + ( + +
+ +
+ + navigate(`/meetings/${item.id}`)}> + {item.title} + + }> + {dayjs(item.meetingTime).format('MM-DD HH:mm')} + {item.participants || item.creatorName || '未指定'} + +
+ {item.tags?.split(',').filter(Boolean).map((t) => ( + {t} + ))} +
+
+ + + + {renderTaskProgress(item)} + + + + + + + + + + + )} + locale={{ emptyText: }} + /> + + + + + + ); +}; + +export default Dashboard; diff --git a/frontend/src/pages/Devices.css b/frontend/src/pages/devices/index.less similarity index 100% rename from frontend/src/pages/Devices.css rename to frontend/src/pages/devices/index.less diff --git a/frontend/src/pages/devices/index.tsx b/frontend/src/pages/devices/index.tsx new file mode 100644 index 0000000..d1a44e1 --- /dev/null +++ b/frontend/src/pages/devices/index.tsx @@ -0,0 +1,281 @@ +import { Button, Card, Drawer, Form, Input, Popconfirm, Select, Space, Table, Tag, Typography, message } from "antd"; +import { DeleteOutlined, DesktopOutlined, EditOutlined, PlusOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { createDevice, deleteDevice, listDevices, listUsers, updateDevice } from "@/api"; +import PageHeader from "@/components/shared/PageHeader"; +import { useDict } from "@/hooks/useDict"; +import { usePermission } from "@/hooks/usePermission"; +import type { DeviceInfo, SysUser } from "@/types"; +import { getStandardPagination } from "@/utils/pagination"; +import "./index.less"; + +const { Text } = Typography; + +type DeviceFormValues = { + userId: number; + deviceCode: string; + deviceName?: string; + status: number; +}; + +export default function Devices() { + const { t } = useTranslation(); + const { can } = usePermission(); + const { items: statusDict } = useDict("sys_common_status"); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [searchText, setSearchText] = useState(""); + + const handleSearch = () => {}; + + const handleResetSearch = () => { + setSearchText(""); + }; + const [devices, setDevices] = useState([]); + const [users, setUsers] = useState([]); + const [open, setOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + + const loadData = async () => { + setLoading(true); + try { + const [deviceList, userList] = await Promise.all([listDevices(), listUsers()]); + setDevices(deviceList || []); + setUsers(userList || []); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, []); + + const userMap = useMemo(() => { + const map: Record = {}; + users.forEach((user) => { + map[user.userId] = user; + }); + return map; + }, [users]); + + const filteredData = useMemo(() => { + if (!searchText) { + return devices; + } + const lower = searchText.toLowerCase(); + return devices.filter((device) => { + const owner = userMap[device.userId]; + return ( + device.deviceCode.toLowerCase().includes(lower) || + (device.deviceName || "").toLowerCase().includes(lower) || + (owner?.displayName || "").toLowerCase().includes(lower) || + String(device.userId).includes(lower) + ); + }); + }, [devices, searchText, userMap]); + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ status: 1 }); + setOpen(true); + }; + + const openEdit = (record: DeviceInfo) => { + setEditing(record); + form.setFieldsValue({ + userId: record.userId, + deviceCode: record.deviceCode, + deviceName: record.deviceName, + status: record.status ?? 1 + }); + setOpen(true); + }; + + const submit = async () => { + const values = await form.validateFields(); + setSaving(true); + try { + const payload: Partial = { + userId: values.userId, + deviceCode: values.deviceCode, + deviceName: values.deviceName, + status: values.status + }; + + if (editing) { + await updateDevice(editing.deviceId, payload); + } else { + await createDevice(payload); + } + + message.success(t("devicesExt.operationSucceeded")); + setOpen(false); + await loadData(); + } finally { + setSaving(false); + } + }; + + const remove = async (id: number) => { + await deleteDevice(id); + message.success(t("devicesExt.operationSucceeded")); + await loadData(); + }; + + return ( +
+ + + +
+
+ } + style={{ width: 360 }} + value={searchText} + onChange={(event) => setSearchText(event.target.value)} + allowClear + aria-label={t("devicesExt.searchLabel")} + /> + + +
+ {can("device:create") ? ( + + ) : null} +
+
+ + + + rowKey="deviceId" + dataSource={filteredData} + loading={loading} + size="middle" + scroll={{ y: "calc(100vh - 350px)" }} + pagination={getStandardPagination(filteredData.length, 1, 1000)} + columns={[ + { + title: t("devicesExt.device"), + key: "device", + render: (_value: unknown, record) => ( + +
+
+
+
{record.deviceName || t("devicesExt.unnamedDevice")}
+
{record.deviceCode}
+
+
+ ) + }, + { + title: t("devices.owner"), + key: "user", + render: (_value: unknown, record) => { + const owner = userMap[record.userId]; + return owner ? ( + + + ) : ( + {t("devicesExt.ownerId")}: {record.userId} + ); + } + }, + { + title: t("common.status"), + dataIndex: "status", + width: 100, + render: (status: number) => { + const item = statusDict.find((dictItem) => dictItem.itemValue === String(status)); + return {item?.itemLabel || (status === 1 ? t("devicesExt.enabled") : t("devicesExt.disabled"))}; + } + }, + { + title: t("devices.updateTime"), + dataIndex: "updatedAt", + width: 180, + render: (text: string) => ( + + {text?.replace("T", " ").substring(0, 19)} + + ) + }, + { + title: t("common.action"), + key: "action", + width: 120, + fixed: "right", + render: (_value: unknown, record) => ( + + {can("device:update") ? ( +
+ } + open={open} + onClose={() => setOpen(false)} + width={420} + destroyOnClose + footer={ +
+ + +
+ } + > +
+ + + + + + + + ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={
+ ) : ( +
+ )} + + + + + + + + + + } style={{ width: 220, borderRadius: "6px" }} value={params.name} onChange={(event) => setParams({ ...params, name: event.target.value })} allowClear /> + setParams({ ...params, code: event.target.value })} allowClear /> + + + + {can("sys_tenant:create") && } + + + +
+ setParams({ ...params, current: page, size: size || params.size }), + showSizeChanger: true, + showQuickJumper: true, + showTotal: (count) => t("common.total", { total: count }), + pageSizeOptions: ["10", "20", "50", "100"], + style: { marginTop: "24px", marginBottom: "24px" } + }} + locale={{ emptyText: }} + /> +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + ) + }, + { + key: "password", + label: {t("profile.security")}, + children: ( + + + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue("newPassword") === value) { + return Promise.resolve(); + } + return Promise.reject(new Error(t("profile.passwordsDoNotMatch"))); + } + }) + ]} + > + + +
+ +
+ + ) + } + ]} + /> + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Dictionaries.css b/frontend/src/pages/system/dictionaries/index.less similarity index 100% rename from frontend/src/pages/Dictionaries.css rename to frontend/src/pages/system/dictionaries/index.less diff --git a/frontend/src/pages/system/dictionaries/index.tsx b/frontend/src/pages/system/dictionaries/index.tsx new file mode 100644 index 0000000..f50eaa0 --- /dev/null +++ b/frontend/src/pages/system/dictionaries/index.tsx @@ -0,0 +1,278 @@ +import { Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { BookOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ProfileOutlined, SearchOutlined } from "@ant-design/icons"; +import { createDictItem, createDictType, deleteDictItem, deleteDictType, fetchDictItems, fetchDictTypes, updateDictItem, updateDictType } from "@/api"; +import { useDict } from "@/hooks/useDict"; +import { usePermission } from "@/hooks/usePermission"; +import PageHeader from "@/components/shared/PageHeader"; +import { getStandardPagination } from "@/utils/pagination"; +import type { SysDictItem, SysDictType } from "@/types"; +import "./index.less"; + +const { Text } = Typography; + +export default function Dictionaries() { + const { t } = useTranslation(); + const { can } = usePermission(); + const { items: statusDict } = useDict("sys_common_status"); + const [types, setTypes] = useState([]); + const [items, setItems] = useState([]); + const [selectedType, setSelectedType] = useState(null); + const [loadingTypes, setLoadingTypes] = useState(false); + const [loadingItems, setLoadingItems] = useState(false); + const [typeTotal, setTypeTotal] = useState(0); + const [typeParams, setTypeParams] = useState({ current: 1, size: 10, typeCode: "", typeName: "" }); + const [typeKeyword, setTypeKeyword] = useState(""); + const [typeDrawerVisible, setTypeDrawerVisible] = useState(false); + const [editingType, setEditingType] = useState(null); + const [typeForm] = Form.useForm(); + const [itemDrawerVisible, setItemDrawerVisible] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [itemForm] = Form.useForm(); + + const loadTypes = useCallback(async (params = typeParams) => { + setLoadingTypes(true); + try { + const data = await fetchDictTypes(params); + setTypes(data.records || []); + setTypeTotal(data.total || 0); + if (data.records?.length && !selectedType) { + setSelectedType(data.records[0]); + } else if (selectedType) { + const updatedSelected = data.records.find((type: SysDictType) => type.dictTypeId === selectedType.dictTypeId); + if (updatedSelected) setSelectedType(updatedSelected); + } + } finally { + setLoadingTypes(false); + } + }, [selectedType, typeParams]); + + const loadItems = async (typeCode: string) => { + setLoadingItems(true); + try { + const data = await fetchDictItems(typeCode); + setItems(data || []); + } finally { + setLoadingItems(false); + } + }; + + useEffect(() => { + loadTypes(); + }, [typeParams.current, typeParams.size]); + + useEffect(() => { + if (selectedType) { + loadItems(selectedType.typeCode); + } else { + setItems([]); + } + }, [selectedType]); + + const handleAddType = () => { + setEditingType(null); + typeForm.resetFields(); + setTypeDrawerVisible(true); + }; + + const handleEditType = (record: SysDictType) => { + setEditingType(record); + typeForm.setFieldsValue(record); + setTypeDrawerVisible(true); + }; + + const handleDeleteType = async (id: number) => { + await deleteDictType(id); + message.success(t("common.success")); + loadTypes(); + }; + + const handleTypeSubmit = async () => { + const values = await typeForm.validateFields(); + if (editingType) { + await updateDictType(editingType.dictTypeId, values); + } else { + await createDictType(values); + } + message.success(t("common.success")); + setTypeDrawerVisible(false); + loadTypes(); + }; + + const handleTypeSearch = (value: string) => { + const next = { ...typeParams, current: 1, typeName: value }; + setTypeParams(next); + loadTypes(next); + }; + + const handleTypeReset = () => { + setTypeKeyword(""); + const next = { ...typeParams, current: 1, typeName: "" }; + setTypeParams(next); + loadTypes(next); + }; + + const handleAddItem = () => { + if (!selectedType) { + message.warning(t("dicts.selectType")); + return; + } + setEditingItem(null); + itemForm.resetFields(); + itemForm.setFieldsValue({ typeCode: selectedType.typeCode, sortOrder: 0, status: 1 }); + setItemDrawerVisible(true); + }; + + const handleEditItem = (record: SysDictItem) => { + setEditingItem(record); + itemForm.setFieldsValue(record); + setItemDrawerVisible(true); + }; + + const handleDeleteItem = async (id: number) => { + await deleteDictItem(id); + message.success(t("common.success")); + if (selectedType) loadItems(selectedType.typeCode); + }; + + const handleItemSubmit = async () => { + const values = await itemForm.validateFields(); + if (editingItem) { + await updateDictItem(editingItem.dictItemId, values); + } else { + await createDictItem(values); + } + message.success(t("common.success")); + setItemDrawerVisible(false); + if (selectedType) loadItems(selectedType.typeCode); + }; + + return ( +
+ + + +
+
setTypeParams({ ...typeParams, current: page, size })), + simple: true, + size: "small", + position: ["bottomCenter"] + }} + size="small" + showHeader={false} + scroll={{ y: "calc(100vh - 480px)" }} + onRow={(record) => ({ onClick: () => setSelectedType(record), className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}` })} + columns={[ + { + render: (_: any, record: SysDictType) => ( +
+
+
{record.typeName}
+
{record.typeCode}
+
+
+ {can("sys_dict:type:update") &&
+
+ ) + } + ]} + /> + + + + + +
{text} }, + { title: t("dicts.itemValue"), dataIndex: "itemValue", className: "tabular-nums" }, + { title: t("dicts.sort"), dataIndex: "sortOrder", width: 80, className: "tabular-nums" }, + { + title: t("common.status"), + dataIndex: "status", + width: 100, + render: (value: number) => { + const item = statusDict.find((dictItem) => dictItem.itemValue === String(value)); + return {item ? item.itemLabel : value === 1 ? t("dictsExt.enabled") : t("dictsExt.disabled")}; + } + }, + { + title: t("common.action"), + width: 120, + fixed: "right" as const, + render: (_: any, record: SysDictItem) => ( + + {can("sys_dict:item:update") && }> +
+ + + + + + + + + + + + +
} open={itemDrawerVisible} onClose={() => setItemDrawerVisible(false)} width={400} destroyOnClose footer={
}> + + + + + + setParams({ ...params, operation: event.target.value })} + prefix={+ {t("platformSettings.basicInfo")}} className="app-page__content-card mb-6" loading={loading}> + + + + + + + + + + + {t("platformSettings.brandAssets")}} className="app-page__content-card mb-6" loading={loading}> + + + + + + + handleUpload(file, "logoUrl")}> + + + + + + + + + handleUpload(file, "iconUrl")}> + + + + + + + + + handleUpload(file, "loginBgUrl")}> + + + + + + + + + {t("platformSettings.complianceFooter")}} className="app-page__content-card" loading={loading}> + + + + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/SysParams.css b/frontend/src/pages/system/sys-params/index.less similarity index 100% rename from frontend/src/pages/SysParams.css rename to frontend/src/pages/system/sys-params/index.less diff --git a/frontend/src/pages/system/sys-params/index.tsx b/frontend/src/pages/system/sys-params/index.tsx new file mode 100644 index 0000000..16fd77e --- /dev/null +++ b/frontend/src/pages/system/sys-params/index.tsx @@ -0,0 +1,229 @@ +import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Table, Tag, Tooltip, Typography, message } from "antd"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons"; +import { createParam, deleteParam, pageParams, updateParam } from "@/api"; +import { useDict } from "@/hooks/useDict"; +import { usePermission } from "@/hooks/usePermission"; +import PageHeader from "@/components/shared/PageHeader"; +import { getStandardPagination } from "@/utils/pagination"; +import type { SysParamQuery, SysParamVO } from "@/types"; +import "./index.less"; + +const { Text } = Typography; + +export default function SysParams() { + const { t } = useTranslation(); + const { can } = usePermission(); + const { items: statusDict } = useDict("sys_common_status"); + const { items: paramTypeDict } = useDict("sys_param_type"); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [queryParams, setQueryParams] = useState({ pageNum: 1, pageSize: 10 }); + const [drawerOpen, setDrawerOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + + const loadData = useCallback(async (query = queryParams) => { + setLoading(true); + try { + const response = await pageParams(query); + setData(response.records || []); + setTotal(response.total || 0); + } finally { + setLoading(false); + } + }, [queryParams]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const handleSearch = (values: any) => { + setQueryParams({ ...queryParams, ...values, pageNum: 1 }); + }; + + const handleReset = () => { + form.resetFields(); + setQueryParams({ pageNum: 1, pageSize: 10 }); + }; + + const handlePageChange = (page: number, pageSize: number) => { + setQueryParams((prev) => ({ ...prev, pageNum: page, pageSize })); + }; + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ isSystem: false, status: 1 }); + setDrawerOpen(true); + }; + + const openEdit = (record: SysParamVO) => { + setEditing(record); + form.setFieldsValue(record); + setDrawerOpen(true); + }; + + const handleDelete = async (id: number) => { + try { + await deleteParam(id); + message.success(t("common.success")); + loadData(); + } catch { + } + }; + + const submit = async () => { + try { + const values = await form.validateFields(); + setSaving(true); + if (editing) { + await updateParam(editing.paramId, values); + } else { + await createParam(values); + } + message.success(t("common.success")); + setDrawerOpen(false); + loadData(); + } finally { + setSaving(false); + } + }; + + const columns = [ + { + title: t("sysParams.paramKey"), + dataIndex: "paramKey", + key: "paramKey", + render: (text: string, record: SysParamVO) => ( + + {text} + {record.isSystem === 1 && {t("sysParams.isSystem")}} + + ) + }, + { + title: t("sysParams.paramValue"), + dataIndex: "paramValue", + key: "paramValue", + ellipsis: true, + render: (text: string) => {text} + }, + { + title: t("sysParams.paramType"), + dataIndex: "paramType", + key: "paramType", + width: 120, + render: (type: string) => {type || t("sysParamsExt.defaultType")} + }, + { title: t("sysParams.description"), dataIndex: "description", key: "description", ellipsis: true }, + { + title: t("common.status"), + dataIndex: "status", + width: 80, + render: (status: number) => { + const item = statusDict.find((dictItem) => dictItem.itemValue === String(status)); + return {item ? item.itemLabel : status === 1 ? t("sysParamsExt.enabled") : t("sysParamsExt.disabled")}; + } + }, + { + title: t("common.action"), + key: "action", + width: 110, + fixed: "right" as const, + render: (_: any, record: SysParamVO) => ( + + {can("sys_param:update") &&
+ + + {editing ? t("sysParams.drawerTitleEdit") : t("sysParams.drawerTitleCreate")}} + open={drawerOpen} + onClose={() => setDrawerOpen(false)} + width={500} + destroyOnClose + footer={
} + > + + + + + + + + +
+ + ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> + + + + {t("sysParams.isSystem")}} + name="isSystem" + valuePropName="checked" + getValueProps={(value) => ({ checked: value === 1 })} + getValueFromEvent={(checked) => (checked ? 1 : 0)} + > + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 84bad25..3d491f7 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,43 +1,53 @@ +import { Suspense, lazy } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; -import Login from "../pages/Login"; -import ResetPassword from "../pages/ResetPassword"; -import AppLayout from "../layouts/AppLayout"; -import { menuRoutes, extraRoutes } from "./routes"; -import { useAuth } from "../hooks/useAuth"; +import AppLayout from "@/layouts/AppLayout"; +import { useAuth } from "@/hooks/useAuth"; +import { menuRoutes,extraRoutes } from "./routes"; + +const Login = lazy(() => import("@/pages/auth/login")); +const ResetPassword = lazy(() => import("@/pages/auth/reset-password")); + +function RouteFallback() { + return
Loading...
; +} function RequireAuth({ children }: { children: JSX.Element }) { const { isAuthed, profile } = useAuth(); + if (!isAuthed) { return ; } - // 强制改密拦截 + if (profile?.pwdResetRequired === 1) { return ; } + return children; } export default function AppRoutes() { return ( - - } /> - } /> - - - - } - > - {menuRoutes.map((route) => ( - - ))} - {extraRoutes && extraRoutes.map((route) => ( - - ))} - - } /> - + }> + + } /> + } /> + + + + } + > + {menuRoutes.map((route) => ( + + ))} + {extraRoutes && extraRoutes.map((route) => ( + + ))} + + } /> + + ); } diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index f7d88c5..c3ac727 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -1,17 +1,22 @@ -import { Dashboard } from "../pages/Dashboard"; -import Users from "../pages/Users"; -import Roles from "../pages/Roles"; -import Permissions from "../pages/Permissions"; -import Devices from "../pages/Devices"; -import Dictionaries from "../pages/Dictionaries"; -import Logs from "../pages/Logs"; -import Tenants from "../pages/Tenants"; -import Orgs from "../pages/Orgs"; -import UserRoleBinding from "../pages/UserRoleBinding"; -import RolePermissionBinding from "../pages/RolePermissionBinding"; -import SysParams from "../pages/SysParams"; -import PlatformSettings from "../pages/PlatformSettings"; -import Profile from "../pages/Profile"; +import { Spin } from "antd"; +import { Suspense, lazy } from "react"; +import type { MenuRoute } from "@/types"; + +const Dashboard = lazy(() => import("@/pages/dashboard")); +const Profile = lazy(() => import("@/pages/profile")); +const Tenants = lazy(() => import("@/pages/organization/tenants")); +const Orgs = lazy(() => import("@/pages/organization/orgs")); +const Users = lazy(() => import("@/pages/access/users")); +const Roles = lazy(() => import("@/pages/access/roles")); +const Permissions = lazy(() => import("@/pages/access/permissions")); +const SysParams = lazy(() => import("@/pages/system/sys-params")); +const PlatformSettings = lazy(() => import("@/pages/system/platform-settings")); +const Dictionaries = lazy(() => import("@/pages/system/dictionaries")); +const Logs = lazy(() => import("@/pages/system/logs")); +const Devices = lazy(() => import("@/pages/devices")); +const UserRoleBinding = lazy(() => import("@/pages/bindings/user-role")); +const RolePermissionBinding = lazy(() => import("@/pages/bindings/role-permission")); + import SpeakerReg from "../pages/business/SpeakerReg"; import RealtimeAsr from "../pages/business/RealtimeAsr"; import RealtimeAsrSession from "../pages/business/RealtimeAsrSession"; @@ -22,8 +27,18 @@ import Meetings from "../pages/business/Meetings"; import MeetingDetail from "../pages/business/MeetingDetail"; import MeetingCreate from "../pages/business/MeetingCreate"; -import type { MenuRoute } from "../types"; +function RouteFallback() { + return ( +
+ +
+ ); +} + +function LazyPage({ children }: { children: JSX.Element }) { + return }>{children}; +} export const menuRoutes: MenuRoute[] = [ { path: "/", label: "总览", element: , perm: "menu:dashboard" }, { path: "/profile", label: "个人中心", element: }, diff --git a/frontend/src/store/themeStore.ts b/frontend/src/store/themeStore.ts new file mode 100644 index 0000000..4606fc4 --- /dev/null +++ b/frontend/src/store/themeStore.ts @@ -0,0 +1,74 @@ +import { create } from 'zustand'; + +export type ThemeMode = 'minimal' | 'tech' | 'default'; +export type LayoutMode = 'side' | 'top'; + +interface ThemeState { + colorPrimary: string; + themeMode: ThemeMode; + layoutMode: LayoutMode; + setColorPrimary: (color: string) => void; + setThemeMode: (mode: ThemeMode) => void; + setLayoutMode: (mode: LayoutMode) => void; + initTheme: () => void; +} + +const DEFAULT_COLOR = '#1677ff'; +const DEFAULT_MODE: ThemeMode = 'default'; +const DEFAULT_LAYOUT: LayoutMode = 'side'; + +const getColorStorageKey = () => { + const username = localStorage.getItem("username") || "default"; + return `unis_theme_color_${username}`; +}; + +const getModeStorageKey = () => { + const username = localStorage.getItem("username") || "default"; + return `unis_theme_mode_${username}`; +}; + +const getLayoutStorageKey = () => { + const username = localStorage.getItem("username") || "default"; + return `unis_layout_mode_${username}`; +}; + +export const useThemeStore = create((set) => ({ + colorPrimary: DEFAULT_COLOR, + themeMode: DEFAULT_MODE, + layoutMode: DEFAULT_LAYOUT, + setColorPrimary: (color: string) => { + set({ colorPrimary: color }); + const key = getColorStorageKey(); + localStorage.setItem(key, color); + document.documentElement.style.setProperty('--app-primary-color', color); + }, + setThemeMode: (mode: ThemeMode) => { + set({ themeMode: mode }); + const key = getModeStorageKey(); + localStorage.setItem(key, mode); + document.documentElement.setAttribute('data-theme', mode); + }, + setLayoutMode: (mode: LayoutMode) => { + set({ layoutMode: mode }); + const key = getLayoutStorageKey(); + localStorage.setItem(key, mode); + }, + initTheme: () => { + const colorKey = getColorStorageKey(); + const modeKey = getModeStorageKey(); + const layoutKey = getLayoutStorageKey(); + + const storedColor = localStorage.getItem(colorKey); + const color = storedColor || DEFAULT_COLOR; + + const storedMode = localStorage.getItem(modeKey) as ThemeMode; + const mode = storedMode || DEFAULT_MODE; + + const storedLayout = localStorage.getItem(layoutKey) as LayoutMode; + const layout = storedLayout || DEFAULT_LAYOUT; + + set({ colorPrimary: color, themeMode: mode, layoutMode: layout }); + document.documentElement.style.setProperty('--app-primary-color', color); + document.documentElement.setAttribute('data-theme', mode); + } +})); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b7cfebb..a4972ee 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -110,6 +110,9 @@ export interface SysLog { tenantName?: string; userId?: number; username?: string; + logType?: string; + moduleName?: string; + actionName?: string; operation: string; method?: string; params?: string; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 2962f57..102de16 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,7 +8,10 @@ "strict": true, "allowJs": true, "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "paths": { + "@/*": ["src/*"] + } }, "include": ["src"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index f5e2689..e7f0c4a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,12 +1,21 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; - +import { fileURLToPath, URL } from "node:url"; export default defineConfig({ plugins: [react()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)) + } + }, + build: { + chunkSizeWarningLimit: 700 + }, server: { port: 5174, proxy: { "/auth": "http://localhost:8081", + "/sys": "http://localhost:8081", "/api": "http://localhost:8081" } }