Browse Source

Merge branch 'master' of http://git.yangzhiqiang.tech/jiayq/ai-tagging

jiayongqiang 3 ngày trước cách đây
mục cha
commit
dd0a2409eb
34 tập tin đã thay đổi với 1647 bổ sung2 xóa
  1. 11 0
      server/pom.xml
  2. 36 1
      server/yusp-tagging-core/pom.xml
  3. 33 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/annotation/RequireRole.java
  4. 34 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/aop/ApiLogAspect.java
  5. 13 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/condition/AuthEnabledCondition.java
  6. 190 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/AuthInterceptor.java
  7. 31 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/FastApiConfig.java
  8. 40 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/WebMvcConfig.java
  9. 2 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagAppController.java
  10. 13 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagTagCategoryController.java
  11. 153 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagUserController.java
  12. 105 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/FastApiController.java
  13. 26 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/LoginRequestDto.java
  14. 35 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingFeedbackRequestDto.java
  15. 23 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingQueryRequestDto.java
  16. 32 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingRequestDto.java
  17. 57 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/entity/AitagUser.java
  18. 41 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/LoginResponseVo.java
  19. 36 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingQueryResponseVo.java
  20. 16 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingResponseVo.java
  21. 6 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/mapper/AitagTagCategoryMapper.java
  22. 23 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/mapper/AitagUserMapper.java
  23. 3 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/AitagTagCategoryService.java
  24. 46 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/AuthService.java
  25. 31 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/FastApiService.java
  26. 29 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AitagTagCategoryServiceImpl.java
  27. 118 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AuthServiceImpl.java
  28. 199 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/FastApiServiceImpl.java
  29. 81 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/util/AuthContextUtil.java
  30. 126 0
      server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/util/JwtUtil.java
  31. 12 0
      server/yusp-tagging-core/src/main/resources/mapper/AitagTagCategoryMapper.xml
  32. 10 0
      server/yusp-tagging-core/src/main/resources/mapper/AitagUserMapper.xml
  33. 21 0
      server/yusp-tagging-starter/src/main/java/cn/com/yusys/yusp/detail/App.java
  34. 15 1
      server/yusp-tagging-starter/src/main/resources/application.yml

+ 11 - 0
server/pom.xml

@@ -26,6 +26,17 @@
         <ojdbc8.version>19.3.0.0</ojdbc8.version>
         <dynamic.datasource.version>3.4.1</dynamic.datasource.version>
     </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>io.jsonwebtoken</groupId>
+                <artifactId>jjwt</artifactId>
+                <version>0.9.1</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
     <!-- 模块 -->
     <modules>
         <module>yusp-tagging-core</module>

+ 36 - 1
server/yusp-tagging-core/pom.xml

@@ -13,7 +13,12 @@
     <version>1.0.0-SNAPSHOT</version>
 
     <dependencies>
-
+        <!-- Spring Web MVC依赖 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <version>5.3.33</version>
+        </dependency>
 
         <dependency>
             <groupId>cn.com.yusys.yusp.common</groupId>
@@ -186,6 +191,36 @@
             <artifactId>aspectjrt</artifactId>
             <version>1.9.7</version>
         </dependency>
+
+        <!-- JWT支持 -->
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.1</version>
+        </dependency>
+
+        <!-- 用于JWT签名算法 -->
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+
+        <!-- Servlet API -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>4.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.13</version>
+        </dependency>
+
+
     </dependencies>
 
     <dependencyManagement>

+ 33 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/annotation/RequireRole.java

@@ -0,0 +1,33 @@
+package cn.com.yusys.yusp.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 权限校验注解
+ * 用于控制接口访问权限
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireRole {
+
+    /**
+     * 允许访问的角色数组
+     * 0: 客户经理
+     * 1: 标签管理人员
+     * 2: 业务管理员
+     * 3: 运维管理员
+     * 4: 测试管理员
+     */
+    String[] value() default {};
+
+    /**
+     * 是否需要登录(默认需要)
+     */
+    boolean requireLogin() default true;
+}

+ 34 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/aop/ApiLogAspect.java

@@ -3,6 +3,7 @@ package cn.com.yusys.yusp.aop;
 import cn.com.yusys.yusp.annotation.ApiOperationType;
 import cn.com.yusys.yusp.domain.entity.AitagApiLog;
 import cn.com.yusys.yusp.service.AitagApiLogService;
+import cn.com.yusys.yusp.util.AuthContextUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import org.aspectj.lang.ProceedingJoinPoint;
@@ -10,6 +11,7 @@ import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.reflect.MethodSignature;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 import java.lang.reflect.Method;
@@ -22,6 +24,9 @@ public class ApiLogAspect {
     @Autowired
     private AitagApiLogService apiLogService;
 
+    @Value("${auth.enabled:false}")
+    private boolean authEnabled;
+
     @Around("@annotation(cn.com.yusys.yusp.annotation.ApiOperationType)")
     public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
         long startTime = System.currentTimeMillis();
@@ -30,6 +35,9 @@ public class ApiLogAspect {
         apiLog.setId(apiLogService.generateLogId());
         apiLog.setCreateDate(new Date());
 
+        // 设置用户信息 - 根据认证状态决定
+        setUserInfo(apiLog);
+
         // 获取操作类型
         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
         Method method = signature.getMethod();
@@ -101,6 +109,32 @@ public class ApiLogAspect {
     }
 
     /**
+     * 设置用户信息
+     * @param apiLog API日志对象
+     */
+    private void setUserInfo(AitagApiLog apiLog) {
+        if (authEnabled) {
+            // 认证启用时,使用当前登录用户信息
+            String currentUserId = AuthContextUtil.getCurrentUser();
+            String currentUserName = AuthContextUtil.getCurrentUserName();
+
+            if (currentUserId != null) {
+                apiLog.setUserId(currentUserId);
+                // 如果有用户名就用用户名,否则用用户ID
+                apiLog.setUserName(currentUserName != null ? currentUserName : currentUserId);
+            } else {
+                // fallback到默认值
+                apiLog.setUserId("unknown");
+                apiLog.setUserName("unknown");
+            }
+        } else {
+            // 认证关闭时,使用固定测试值
+            apiLog.setUserId("testID");
+            apiLog.setUserName("testName");
+        }
+    }
+
+    /**
      * 隐藏敏感数据
      * @param jsonStr 原始JSON字符串
      * @return 隐藏敏感数据后的JSON字符串

+ 13 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/condition/AuthEnabledCondition.java

@@ -0,0 +1,13 @@
+package cn.com.yusys.yusp.condition;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+public class AuthEnabledCondition implements Condition {
+    @Override
+    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+        String authEnabled = context.getEnvironment().getProperty("auth.enabled");
+        return "true".equals(authEnabled);
+    }
+}

+ 190 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/AuthInterceptor.java

@@ -0,0 +1,190 @@
+package cn.com.yusys.yusp.config;
+
+import cn.com.yusys.yusp.annotation.RequireRole;
+import cn.com.yusys.yusp.condition.AuthEnabledCondition;
+import cn.com.yusys.yusp.domain.entity.AitagUser;
+import cn.com.yusys.yusp.mapper.AitagUserMapper;
+import cn.com.yusys.yusp.model.Result;
+import cn.com.yusys.yusp.util.AuthContextUtil;
+import cn.com.yusys.yusp.util.JwtUtil;
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * 认证拦截器
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Slf4j
+@Component
+@Conditional(AuthEnabledCondition.class)
+public class AuthInterceptor implements HandlerInterceptor {
+
+    @Autowired
+    private JwtUtil jwtUtil;
+
+    @Autowired
+    private AitagUserMapper userMapper;
+
+    @Value("${auth.enabled:true}")
+    private boolean authEnabled;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 如果认证功能未启用,直接放行
+        if (!authEnabled) {
+            log.debug("认证功能已关闭,放行请求: {}", request.getRequestURI());
+            return true;
+        }
+
+        // 只处理Controller方法
+        if (!(handler instanceof HandlerMethod)) {
+            return true;
+        }
+
+        HandlerMethod handlerMethod = (HandlerMethod) handler;
+        RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
+
+        // 如果没有权限注解,直接放行
+        if (requireRole == null) {
+            requireRole = handlerMethod.getBeanType().getAnnotation(RequireRole.class);
+            if (requireRole == null) {
+                return true;
+            }
+        }
+
+        // 检查是否需要登录
+        if (!requireRole.requireLogin()) {
+            return true;
+        }
+
+        // 获取token
+        String token = getTokenFromRequest(request);
+        if (token == null || token.isEmpty()) {
+            log.warn("请求缺少认证token: {}", request.getRequestURI());
+            handleUnauthorized(response, "未提供认证信息");
+            return false;
+        }
+
+        // 验证token
+        if (!jwtUtil.validateToken(token)) {
+            log.warn("token验证失败: {}", request.getRequestURI());
+            handleUnauthorized(response, "认证信息无效或已过期");
+            return false;
+        }
+
+        // 获取用户信息
+        String userId = jwtUtil.getUserIdFromToken(token);
+        String role = jwtUtil.getRoleFromToken(token);
+
+        if (userId == null || role == null) {
+            log.warn("token中缺少必要的用户信息: {}", request.getRequestURI());
+            handleUnauthorized(response, "认证信息不完整");
+            return false;
+        }
+
+        // 获取完整的用户信息(包括用户名)
+        AitagUser user = userMapper.selectByUserId(userId);
+        if (user == null) {
+            log.warn("用户不存在: {}", userId);
+            handleUnauthorized(response, "用户信息不存在");
+            return false;
+        }
+
+        // 检查角色权限
+        String[] allowedRoles = requireRole.value();
+        if (allowedRoles.length > 0) {
+            boolean hasPermission = false;
+            for (String allowedRole : allowedRoles) {
+                if (allowedRole.equals(role)) {
+                    hasPermission = true;
+                    break;
+                }
+            }
+            if (!hasPermission) {
+                log.warn("用户{}角色{}无权访问: {}", userId, role, request.getRequestURI());
+                handleForbidden(response, "权限不足");
+                return false;
+            }
+        }
+
+        // 设置当前用户信息到上下文(包括用户名)
+        AuthContextUtil.setCurrentUser(userId);
+        AuthContextUtil.setCurrentRole(role);
+        AuthContextUtil.setCurrentUserName(user.getUserName()); // 新增:存储用户名
+
+        log.debug("用户{}认证通过,角色: {}, 用户名: {}, 访问: {}", userId, role, user.getUserName(), request.getRequestURI());
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        // 清除线程本地变量
+        AuthContextUtil.clear();
+    }
+
+    /**
+     * 从请求中获取token
+     *
+     * @param request HTTP请求
+     * @return token
+     */
+    private String getTokenFromRequest(HttpServletRequest request) {
+        // 优先从Header获取
+        String token = request.getHeader("Authorization");
+        if (token != null && token.startsWith("Bearer ")) {
+            return token.substring(7);
+        }
+
+        // 从参数获取
+        token = request.getParameter("token");
+        if (token != null) {
+            return token;
+        }
+
+        return null;
+    }
+
+    /**
+     * 处理未授权响应
+     *
+     * @param response HTTP响应
+     * @param message 错误消息
+     */
+    private void handleUnauthorized(HttpServletResponse response, String message) throws IOException {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType("application/json;charset=UTF-8");
+        PrintWriter writer = response.getWriter();
+        Result<Void> result = Result.error("401", message);
+        writer.write(JSON.toJSONString(result));
+        writer.flush();
+    }
+
+    /**
+     * 处理禁止访问响应
+     *
+     * @param response HTTP响应
+     * @param message 错误消息
+     */
+    private void handleForbidden(HttpServletResponse response, String message) throws IOException {
+        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        response.setContentType("application/json;charset=UTF-8");
+        PrintWriter writer = response.getWriter();
+        Result<Void> result = Result.error("403", message);
+        writer.write(JSON.toJSONString(result));
+        writer.flush();
+    }
+}

+ 31 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/FastApiConfig.java

@@ -0,0 +1,31 @@
+package cn.com.yusys.yusp.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "fastapi.service")
+public class FastApiConfig {
+
+    /**
+     * FastAPI服务地址
+     */
+    private String url;
+
+    /**
+     * 请求超时时间(毫秒)
+     */
+    private int timeout = 10000;
+
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private int connectTimeout = 5000;
+
+    /**
+     * 读取超时时间(毫秒)
+     */
+    private int readTimeout = 10000;
+}

+ 40 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/config/WebMvcConfig.java

@@ -0,0 +1,40 @@
+package cn.com.yusys.yusp.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Web MVC配置类
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Autowired(required = false)  // 改为可选注入,避免启动时报错
+    private AuthInterceptor authInterceptor;
+
+    @Value("${auth.enabled:false}")
+    private boolean authEnabled;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 只有当认证功能启用且拦截器存在时才注册
+        if (authEnabled && authInterceptor != null) {
+            registry.addInterceptor(authInterceptor)
+                    .addPathPatterns("/**")
+                    .excludePathPatterns(
+                            "/api/auth/login",
+                            "/api/auth/logout",
+                            "/health",
+                            "/swagger-ui/**",
+                            "/v2/api-docs",
+                            "/webjars/**"
+                    );
+        }
+    }
+}

+ 2 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagAppController.java

@@ -1,5 +1,6 @@
 package cn.com.yusys.yusp.controller;
 
+import cn.com.yusys.yusp.annotation.RequireRole;
 import cn.com.yusys.yusp.model.Result;
 import cn.com.yusys.yusp.domain.dto.AitagAppCreateDto;
 import cn.com.yusys.yusp.domain.entity.AitagApp;
@@ -21,6 +22,7 @@ public class AitagAppController {
     private AitagAppService aiTagAppService;
 
     @ApiOperationType("应用列表一览")
+    @RequireRole({"1", "4"})
     @GetMapping("/list")
     public Result<List<AitagApp>> listApps(
             @RequestParam(defaultValue = "1") int page,

+ 13 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagTagCategoryController.java

@@ -99,4 +99,17 @@ public class AitagTagCategoryController {
             return Result.error("500", "删除标签体系失败:" + e.getMessage());
         }
     }
+
+    @ApiOperationType("已启用标签体系列表一览")
+    @GetMapping("/enablelist")
+    public Result<List<AitagTagCategoryVo>> listEnabledCategories(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "5") int size) {
+        try {
+            Page<AitagTagCategoryVo> pageResult = aiTagCategoryService.listEnabledCategories(page, size);
+            return Result.pageSuccess(pageResult.getRecords(), pageResult.getTotal());
+        } catch (Exception e) {
+            return Result.error("500", "分页查询失败:" + e.getMessage());
+        }
+    }
 }

+ 153 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/AitagUserController.java

@@ -0,0 +1,153 @@
+package cn.com.yusys.yusp.controller;
+
+import cn.com.yusys.yusp.annotation.ApiOperationType;
+import cn.com.yusys.yusp.condition.AuthEnabledCondition;
+import cn.com.yusys.yusp.model.Result;
+import cn.com.yusys.yusp.domain.dto.LoginRequestDto;
+import cn.com.yusys.yusp.domain.vo.LoginResponseVo;
+import cn.com.yusys.yusp.service.AuthService;
+import cn.com.yusys.yusp.util.AuthContextUtil;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * 认证控制器
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Slf4j
+@Api(tags = "认证管理")
+@RestController
+@RequestMapping("/api/auth")
+@Conditional(AuthEnabledCondition.class)
+public class AitagUserController {
+
+    @Autowired
+    private AuthService authService;
+
+    @Value("${auth.enabled:true}")
+    private boolean authEnabled;
+
+    /**
+     * 用户登录
+     *
+     * @param loginRequest 登录请求
+     * @return 登录结果
+     */
+    @ApiOperationType("用户登录")
+    @PostMapping("/login")
+    public Result<LoginResponseVo> login(@Valid @RequestBody LoginRequestDto loginRequest) {
+        try {
+            log.info("收到登录请求: {}", loginRequest.getUserId());
+
+            if (!authEnabled) {
+                log.warn("认证功能已关闭,但仍允许登录");
+            }
+
+            LoginResponseVo response = authService.login(loginRequest);
+            return Result.success(response);
+        } catch (Exception e) {
+            log.error("登录失败", e);
+            return Result.error("500", "登录失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 用户登出
+     *
+     * @return 登出结果
+     */
+    @ApiOperationType("用户登出")
+    @PostMapping("/logout")
+    public Result<Void> logout() {
+        try {
+            String currentUserId = AuthContextUtil.getCurrentUser();
+            if (currentUserId != null) {
+                authService.logout(currentUserId);
+                log.info("用户登出成功: {}", currentUserId);
+            }
+            return Result.success();
+        } catch (Exception e) {
+            log.error("登出失败", e);
+            return Result.error("500", "登出失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取当前用户信息
+     *
+     * @return 用户信息
+     */
+    @ApiOperationType("获取当前用户信息")
+    @GetMapping("/current-user")
+    public Result<Object> getCurrentUser() {
+        try {
+            String userId = AuthContextUtil.getCurrentUser();
+            String role = AuthContextUtil.getCurrentRole();
+
+            if (userId == null) {
+                return Result.error("401", "未登录");
+            }
+
+            return Result.success(new CurrentUserInfo(userId, role));
+        } catch (Exception e) {
+            log.error("获取当前用户信息失败", e);
+            return Result.error("500", "获取用户信息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 检查认证状态
+     *
+     * @return 认证状态
+     */
+    @ApiOperationType("检查认证状态")
+    @GetMapping("/status")
+    public Result<AuthStatusVo> getStatus() {
+        AuthStatusVo status = new AuthStatusVo();
+        status.setEnabled(authEnabled);
+        status.setCurrentUser(AuthContextUtil.getCurrentUser());
+        status.setCurrentRole(AuthContextUtil.getCurrentRole());
+        return Result.success(status);
+    }
+
+    /**
+     * 当前用户信息内部类
+     */
+    private static class CurrentUserInfo {
+        private String userId;
+        private String role;
+
+        public CurrentUserInfo(String userId, String role) {
+            this.userId = userId;
+            this.role = role;
+        }
+
+        public String getUserId() { return userId; }
+        public String getRole() { return role; }
+    }
+
+    /**
+     * 认证状态VO
+     */
+    private static class AuthStatusVo {
+        private boolean enabled;
+        private String currentUser;
+        private String currentRole;
+
+        // Getters and setters
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public String getCurrentUser() { return currentUser; }
+        public void setCurrentUser(String currentUser) { this.currentUser = currentUser; }
+        public String getCurrentRole() { return currentRole; }
+        public void setCurrentRole(String currentRole) { this.currentRole = currentRole; }
+    }
+}

+ 105 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/controller/FastApiController.java

@@ -0,0 +1,105 @@
+package cn.com.yusys.yusp.controller;
+
+import cn.com.yusys.yusp.annotation.ApiOperationType;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingFeedbackRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingQueryRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingRequestDto;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingQueryResponseVo;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingResponseVo;
+import cn.com.yusys.yusp.model.Result;
+import cn.com.yusys.yusp.service.FastApiService;
+import cn.com.yusys.yusp.util.AuthContextUtil;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+@Slf4j
+@Api(tags = "AI打标接口")
+@RestController
+@RequestMapping("/api/fastapi")
+public class FastApiController {
+
+    @Autowired
+    private FastApiService fastApiService;
+
+    @ApiOperationType("AI打标")
+    @PostMapping("/tagging")
+    public Result<AiTaggingResponseVo> tagging(@RequestBody AiTaggingRequestDto request) {
+        try {
+            log.info("收到AI打标请求: appId={}, businessAttr={}", request.getAppId(), request.getBusinessAttr());
+            AiTaggingResponseVo response = fastApiService.tagging(request);
+            return Result.success(response);
+        } catch (Exception e) {
+            log.error("AI打标接口调用失败", e);
+            return Result.error("500", "AI打标接口调用失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperationType("AI打标查询")
+    @GetMapping("/query")
+    public Result<AiTaggingQueryResponseVo> query(
+            @RequestParam(required = false) String appId,
+            @RequestParam(required = false) String timestamp,
+            @RequestParam(required = false) String sign,
+            @Valid @RequestParam String businessAttr) {
+        try {
+            log.info("收到AI打标查询请求: appId={}, businessAttr={}", appId, businessAttr);
+
+            AiTaggingQueryRequestDto request = new AiTaggingQueryRequestDto();
+            request.setAppId(appId);
+            request.setTimestamp(timestamp);
+            request.setSign(sign);
+            request.setBusinessAttr(businessAttr);
+
+            AiTaggingQueryResponseVo response = fastApiService.query(request);
+            return Result.success(response);
+        } catch (Exception e) {
+            log.error("AI打标查询接口调用失败", e);
+            return Result.error("500", "AI打标查询接口调用失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperationType("AI打标反馈")
+    @GetMapping("/feedback")
+    public Result<AiTaggingResponseVo> feedback(
+            @RequestParam(required = false) String userId,
+            @RequestParam(required = false) String userNm,
+            @RequestParam(required = true) String feedback,
+            @RequestParam(required = false) String contractNo,
+            @RequestParam(required = false) String userOrg,
+            @RequestParam(required = false) String userEndpoint,
+            @RequestParam(required = true) String businessAttr) {
+        try {
+            log.info("收到AI打标反馈请求: userId={}, businessAttr={}, feedback={}", userId, businessAttr, feedback);
+
+            AiTaggingFeedbackRequestDto request = new AiTaggingFeedbackRequestDto();
+
+            // 设置用户信息 - 优先使用登录信息,否则使用默认值
+            String currentUserId = AuthContextUtil.getCurrentUser();
+            String currentUserName = AuthContextUtil.getCurrentUserName();
+
+            if (currentUserId != null && currentUserName != null) {
+                request.setUserId(currentUserId);
+                request.setUserNm(currentUserName);
+            } else {
+                request.setUserId(userId != null ? userId : "无登录测试ID");
+                request.setUserNm(userNm != null ? userNm : "无登录测试");
+            }
+
+            request.setFeedback(feedback);
+            request.setContractNo(contractNo);
+            request.setUserOrg(userOrg);
+            request.setUserEndpoint(userEndpoint);
+            request.setBusinessAttr(businessAttr);
+
+            AiTaggingResponseVo response = fastApiService.feedback(request);
+            return Result.success(response);
+        } catch (Exception e) {
+            log.error("AI打标反馈接口调用失败", e);
+            return Result.error("500", "AI打标反馈接口调用失败: " + e.getMessage());
+        }
+    }
+}

+ 26 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/LoginRequestDto.java

@@ -0,0 +1,26 @@
+package cn.com.yusys.yusp.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 登录请求DTO
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Data
+@ApiModel("登录请求")
+public class LoginRequestDto {
+
+    @NotBlank(message = "用户ID不能为空")
+    @ApiModelProperty(value = "用户ID", required = true)
+    private String userId;
+
+    @NotBlank(message = "密码不能为空")
+    @ApiModelProperty(value = "密码", required = true)
+    private String password;
+}

+ 35 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingFeedbackRequestDto.java

@@ -0,0 +1,35 @@
+package cn.com.yusys.yusp.domain.dto.fastapidto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@ApiModel("AI打标反馈请求")
+public class AiTaggingFeedbackRequestDto {
+
+    @ApiModelProperty(value = "用户ID", required = false)
+    private String userId;
+
+    @ApiModelProperty(value = "用户姓名", required = false)
+    private String userNm;
+
+    @NotBlank(message = "反馈结果不能为空")
+    @ApiModelProperty(value = "反馈结果(agree/reject)", required = true)
+    private String feedback;
+
+    @ApiModelProperty(value = "合同编号", required = false)
+    private String contractNo;
+
+    @ApiModelProperty(value = "用户机构", required = false)
+    private String userOrg;
+
+    @ApiModelProperty(value = "用户终端", required = false)
+    private String userEndpoint;
+
+    @NotBlank(message = "业务属性不能为空")
+    @ApiModelProperty(value = "业务属性", required = true)
+    private String businessAttr;
+}

+ 23 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingQueryRequestDto.java

@@ -0,0 +1,23 @@
+
+package cn.com.yusys.yusp.domain.dto.fastapidto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("AI打标查询请求")
+public class AiTaggingQueryRequestDto {
+
+    @ApiModelProperty(value = "后台分配的APPID", required = false)
+    private String appId;
+
+    @ApiModelProperty(value = "时间戳", required = false)
+    private String timestamp;
+
+    @ApiModelProperty(value = "签名", required = false)
+    private String sign;
+
+    @ApiModelProperty(value = "业务属性", required = true)
+    private String businessAttr;
+}

+ 32 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/dto/fastapidto/AiTaggingRequestDto.java

@@ -0,0 +1,32 @@
+package cn.com.yusys.yusp.domain.dto.fastapidto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+@ApiModel("AI打标请求")
+public class AiTaggingRequestDto {
+
+    @NotBlank(message = "appId不能为空")
+    @ApiModelProperty(value = "后台分配的APPID", required = true)
+    private String appId;
+
+    @NotBlank(message = "时间戳不能为空")
+    @ApiModelProperty(value = "时间戳", required = true)
+    private String timestamp;
+
+    @NotBlank(message = "签名不能为空")
+    @ApiModelProperty(value = "签名", required = true)
+    private String sign;
+
+    @NotBlank(message = "业务属性不能为空")
+    @ApiModelProperty(value = "业务属性", required = true)
+    private String businessAttr;
+
+    @NotBlank(message = "投向+用途+职业不能为空")
+    @ApiModelProperty(value = "投向+用途+职业", required = true)
+    private String phrase;
+}

+ 57 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/entity/AitagUser.java

@@ -0,0 +1,57 @@
+package cn.com.yusys.yusp.domain.entity;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import javax.validation.constraints.NotNull;
+
+import java.io.Serializable;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+/**
+* 
+* @TableName aitag_user
+*/
+@Data
+public class AitagUser implements Serializable {
+
+    /**
+    * 主键
+    */
+    @NotBlank(message="[主键]不能为空")
+    @Size(max= 100,message="编码长度不能超过100")
+    @ApiModelProperty("主键")
+    @Length(max= 100,message="编码长度不能超过100")
+    private String id;
+    /**
+    * 登录用的用户ID,具备唯一性
+    */
+    @Size(max= 100,message="编码长度不能超过100")
+    @ApiModelProperty("登录用的用户ID,具备唯一性")
+    @Length(max= 100,message="编码长度不能超过100")
+    private String userId;
+    /**
+    * 登陆密码
+    */
+    @Size(max= 100,message="编码长度不能超过100")
+    @ApiModelProperty("登陆密码")
+    @Length(max= 100,message="编码长度不能超过100")
+    private String password;
+    /**
+    * 用户名称
+    */
+    @Size(max= 100,message="编码长度不能超过100")
+    @ApiModelProperty("用户名称")
+    @Length(max= 100,message="编码长度不能超过100")
+    private String userName;
+    /**
+    * 角色权限(0客户经理,1标签管理人员,2业务管理员,3运维管理员,4测试管理员)
+    */
+    @Size(max= 100,message="编码长度不能超过100")
+    @ApiModelProperty("角色权限(0客户经理,1标签管理人员,2业务管理员,3运维管理员,4测试管理员)")
+    @Length(max= 100,message="编码长度不能超过100")
+    private String role;
+
+}

+ 41 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/LoginResponseVo.java

@@ -0,0 +1,41 @@
+package cn.com.yusys.yusp.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 登录响应VO
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Data
+@ApiModel("登录响应")
+public class LoginResponseVo {
+
+    @ApiModelProperty("访问令牌")
+    private String token;
+
+    @ApiModelProperty("用户ID")
+    private String userId;
+
+    @ApiModelProperty("用户名称")
+    private String userName;
+
+    @ApiModelProperty("用户角色")
+    private String role;
+
+    @ApiModelProperty("过期时间(毫秒)")
+    private Long expiresIn;
+
+    public static LoginResponseVo success(String token, String userId, String userName, String role, Long expiresIn) {
+        LoginResponseVo response = new LoginResponseVo();
+        response.setToken(token);
+        response.setUserId(userId);
+        response.setUserName(userName);
+        response.setRole(role);
+        response.setExpiresIn(expiresIn);
+        return response;
+    }
+}

+ 36 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingQueryResponseVo.java

@@ -0,0 +1,36 @@
+package cn.com.yusys.yusp.domain.vo.fastapivo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("AI打标查询响应")
+public class AiTaggingQueryResponseVo {
+
+    @ApiModelProperty(value = "状态码 200(查询成功)/300(正在处理中)/500(处理失败)")
+    private String code;
+
+    @ApiModelProperty(value = "消息内容")
+    private String message;
+
+    @ApiModelProperty(value = "标签数据")
+    private AiTagDataVo[] data;
+
+    @Data
+    @ApiModel("标签数据")
+    public static class AiTagDataVo {
+
+        @ApiModelProperty(value = "标签名称")
+        private String tag;
+
+        @ApiModelProperty(value = "标签代码")
+        private String tagcode;
+
+        @ApiModelProperty(value = "是否AI推荐 true: AI推荐的标签, false: 算法筛选出来但是被LLM淘汰的候选标签")
+        private Boolean passr;
+
+        @ApiModelProperty(value = "解释说明")
+        private String desc;
+    }
+}

+ 16 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/domain/vo/fastapivo/AiTaggingResponseVo.java

@@ -0,0 +1,16 @@
+package cn.com.yusys.yusp.domain.vo.fastapivo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("AI打标响应")
+public class AiTaggingResponseVo {
+
+    @ApiModelProperty(value = "状态码 200/500")
+    private String code;
+
+    @ApiModelProperty(value = "消息内容")
+    private String message;
+}

+ 6 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/mapper/AitagTagCategoryMapper.java

@@ -38,4 +38,10 @@ public interface AitagTagCategoryMapper {
 
     // 全表计数(分页总数)
     long selectCountAll();
+
+    // 分页查询已启用的标签体系
+    List<AitagTagCategory> selectPageEnabledCategories(@Param("offset") int offset, @Param("limit") int limit);
+
+    // 统计已启用标签体系的数量
+    long selectCountEnabled();
 }

+ 23 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/mapper/AitagUserMapper.java

@@ -0,0 +1,23 @@
+package cn.com.yusys.yusp.mapper;
+
+import cn.com.yusys.yusp.domain.entity.AitagUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 用户Mapper接口
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+public interface AitagUserMapper extends BaseMapper<AitagUser> {
+
+    /**
+     * 根据用户ID查询用户
+     *
+     * @param userId 用户ID
+     * @return 用户信息
+     */
+    AitagUser selectByUserId(@Param("userId") String userId);
+}

+ 3 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/AitagTagCategoryService.java

@@ -16,4 +16,7 @@ public interface AitagTagCategoryService {
     void enableCategory(String id);
     void disableCategory(String id);
     void deleteCategory(String id);
+    // 分页查询已启用的标签体系
+    Page<AitagTagCategoryVo> listEnabledCategories(int page, int size);
+
 }

+ 46 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/AuthService.java

@@ -0,0 +1,46 @@
+package cn.com.yusys.yusp.service;
+
+import cn.com.yusys.yusp.domain.dto.LoginRequestDto;
+import cn.com.yusys.yusp.domain.entity.AitagUser;
+import cn.com.yusys.yusp.domain.vo.LoginResponseVo;
+
+/**
+ * 认证服务接口
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+public interface AuthService {
+
+    /**
+     * 用户登录
+     *
+     * @param loginRequest 登录请求
+     * @return 登录响应
+     */
+    LoginResponseVo login(LoginRequestDto loginRequest);
+
+    /**
+     * 用户登出
+     *
+     * @param userId 用户ID
+     */
+    void logout(String userId);
+
+    /**
+     * 根据用户ID获取用户信息
+     *
+     * @param userId 用户ID
+     * @return 用户信息
+     */
+    AitagUser getUserById(String userId);
+
+    /**
+     * 验证用户密码
+     *
+     * @param userId 用户ID
+     * @param password 密码
+     * @return 是否验证通过
+     */
+    boolean validatePassword(String userId, String password);
+}

+ 31 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/FastApiService.java

@@ -0,0 +1,31 @@
+package cn.com.yusys.yusp.service;
+
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingFeedbackRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingQueryRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingRequestDto;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingQueryResponseVo;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingResponseVo;
+
+public interface FastApiService {
+
+    /**
+     * AI打标接口
+     * @param request 打标请求
+     * @return 响应结果
+     */
+    AiTaggingResponseVo tagging(AiTaggingRequestDto request);
+
+    /**
+     * AI打标查询接口
+     * @param request 查询请求
+     * @return 查询结果
+     */
+    AiTaggingQueryResponseVo query(AiTaggingQueryRequestDto request);
+
+    /**
+     * AI打标反馈接口
+     * @param request 反馈请求
+     * @return 响应结果
+     */
+    AiTaggingResponseVo feedback(AiTaggingFeedbackRequestDto request);
+}

+ 29 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AitagTagCategoryServiceImpl.java

@@ -145,4 +145,33 @@ public class AitagTagCategoryServiceImpl implements AitagTagCategoryService {
 
         aiTagCategoryMapper.deleteCategory(id);
     }
+
+    @Override
+    public Page<AitagTagCategoryVo> listEnabledCategories(int page, int size) {
+        int offset = (page - 1) * size;
+        List<AitagTagCategory> records = aiTagCategoryMapper.selectPageEnabledCategories(offset, size);
+        long total = aiTagCategoryMapper.selectCountEnabled();
+
+        // 转换为 VO 并动态计算 tagNum
+        List<AitagTagCategoryVo> voList = records.stream().map(category -> {
+            AitagTagCategoryVo vo = new AitagTagCategoryVo();
+            vo.setId(category.getId());
+            vo.setCategoryCode(category.getCategoryCode());
+            vo.setCategoryNm(category.getCategoryNm());
+            vo.setCategoryDesc(category.getCategoryDesc());
+            vo.setVisibilityLevel(category.getVisibilityLevel());
+            vo.setState(category.getState());
+            vo.setIsDelete(category.getIsDelete());
+
+            // 动态计算 tagNum
+            int tagNum = aiTagCategoryMapper.countTagsByCategoryId(category.getId());
+            vo.setTagNum(tagNum);
+
+            return vo;
+        }).collect(Collectors.toList());
+
+        Page<AitagTagCategoryVo> pageObj = new Page<>(page, size, total);
+        pageObj.setRecords(voList);
+        return pageObj;
+    }
 }

+ 118 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/AuthServiceImpl.java

@@ -0,0 +1,118 @@
+package cn.com.yusys.yusp.service.impl;
+
+import cn.com.yusys.yusp.condition.AuthEnabledCondition;
+import cn.com.yusys.yusp.domain.dto.LoginRequestDto;
+import cn.com.yusys.yusp.domain.entity.AitagUser;
+import cn.com.yusys.yusp.domain.vo.LoginResponseVo;
+import cn.com.yusys.yusp.mapper.AitagUserMapper;
+import cn.com.yusys.yusp.service.AuthService;
+import cn.com.yusys.yusp.util.JwtUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 认证服务实现类
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Slf4j
+@Service
+@Conditional(AuthEnabledCondition.class)
+public class AuthServiceImpl implements AuthService {
+
+    @Autowired
+    private AitagUserMapper userMapper;
+
+    @Autowired
+    private JwtUtil jwtUtil;
+
+    @Value("${jwt.expiration:86400000}")
+    private Long expiration;
+
+    // 存储在线用户的token(简单实现,生产环境建议使用Redis)
+    private final ConcurrentHashMap<String, String> onlineUsers = new ConcurrentHashMap<>();
+
+    @Override
+    public LoginResponseVo login(LoginRequestDto loginRequest) {
+        String userId = loginRequest.getUserId();
+        String password = loginRequest.getPassword();
+
+        log.info("用户尝试登录: {}", userId);
+
+        // 验证用户是否存在
+        AitagUser user = getUserById(userId);
+        if (user == null) {
+            log.warn("用户不存在: {}", userId);
+            throw new RuntimeException("用户名或密码错误");
+        }
+
+        // 验证密码
+        if (!validatePassword(userId, password)) {
+            log.warn("密码错误: {}", userId);
+            throw new RuntimeException("用户名或密码错误");
+        }
+
+        // 生成token
+        String token = jwtUtil.generateToken(userId, user.getRole());
+
+        // 记录在线用户
+        onlineUsers.put(userId, token);
+
+        log.info("用户登录成功: {}, 角色: {}", userId, user.getRole());
+
+        return LoginResponseVo.success(
+                token,
+                user.getUserId(),
+                user.getUserName(),
+                user.getRole(),
+                expiration
+        );
+    }
+
+    @Override
+    public void logout(String userId) {
+        log.info("用户登出: {}", userId);
+        onlineUsers.remove(userId);
+    }
+
+    @Override
+    public AitagUser getUserById(String userId) {
+        return userMapper.selectByUserId(userId);
+    }
+
+    @Override
+    public boolean validatePassword(String userId, String password) {
+        AitagUser user = getUserById(userId);
+        if (user == null) {
+            return false;
+        }
+        // 简单的密码比较,实际项目中应该使用加密存储
+        return user.getPassword() != null && user.getPassword().equals(password);
+    }
+
+    /**
+     * 检查用户是否在线
+     *
+     * @param userId 用户ID
+     * @return 是否在线
+     */
+    public boolean isUserOnline(String userId) {
+        return onlineUsers.containsKey(userId);
+    }
+
+    /**
+     * 获取用户的当前token
+     *
+     * @param userId 用户ID
+     * @return token
+     */
+    public String getUserToken(String userId) {
+        return onlineUsers.get(userId);
+    }
+}

+ 199 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/service/impl/FastApiServiceImpl.java

@@ -0,0 +1,199 @@
+package cn.com.yusys.yusp.service.impl;
+
+import cn.com.yusys.yusp.config.FastApiConfig;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingFeedbackRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingQueryRequestDto;
+import cn.com.yusys.yusp.domain.dto.fastapidto.AiTaggingRequestDto;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingQueryResponseVo;
+import cn.com.yusys.yusp.domain.vo.fastapivo.AiTaggingResponseVo;
+import cn.com.yusys.yusp.service.FastApiService;
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+public class FastApiServiceImpl implements FastApiService {
+
+    @Autowired
+    private FastApiConfig fastApiConfig;
+
+    private static final String TAGGING_PATH = "/aitag/v1/tagging";
+    private static final String QUERY_PATH = "/aitag/v1/query";
+    private static final String FEEDBACK_PATH = "/aitag/v1/feedback";
+
+    @Override
+    public AiTaggingResponseVo tagging(AiTaggingRequestDto request) {
+        String url = fastApiConfig.getUrl() + TAGGING_PATH;
+        log.info("调用AI打标接口: {}", url);
+
+        try (CloseableHttpClient httpClient = createHttpClient()) {
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setHeader("Content-Type", "application/json");
+
+            // 构造请求体
+            String requestBody = JSON.toJSONString(request);
+            httpPost.setEntity(new StringEntity(requestBody, "UTF-8"));
+
+            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+                String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+                log.info("AI打标接口响应: {}", responseBody);
+
+                return JSON.parseObject(responseBody, AiTaggingResponseVo.class);
+            }
+        } catch (Exception e) {
+            log.error("调用AI打标接口失败", e);
+            AiTaggingResponseVo errorResponse = new AiTaggingResponseVo();
+            errorResponse.setCode("500");
+            errorResponse.setMessage("调用AI打标接口失败: " + e.getMessage());
+            return errorResponse;
+        }
+    }
+
+    @Override
+    public AiTaggingQueryResponseVo query(AiTaggingQueryRequestDto request) {
+        StringBuilder urlBuilder = new StringBuilder(fastApiConfig.getUrl()).append(QUERY_PATH);
+
+        // 只添加非空参数
+        boolean hasParams = false;
+
+        if (request.getAppId() != null && !request.getAppId().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("appId=").append(encodeParam(request.getAppId()));
+            hasParams = true;
+        }
+
+        if (request.getTimestamp() != null && !request.getTimestamp().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("timestamp=").append(encodeParam(request.getTimestamp()));
+            hasParams = true;
+        }
+
+        if (request.getSign() != null && !request.getSign().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("sign=").append(encodeParam(request.getSign()));
+            hasParams = true;
+        }
+
+        // businessAttr是必填参数,直接添加
+        urlBuilder.append(hasParams ? "&" : "?").append("businessAttr=").append(encodeParam(request.getBusinessAttr()));
+
+        String url = urlBuilder.toString();
+        log.info("调用AI打标查询接口: {}", url);
+        try (CloseableHttpClient httpClient = createHttpClient()) {
+            HttpGet httpGet = new HttpGet(url);
+
+            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+                String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+                log.info("AI打标查询接口响应: {}", responseBody);
+
+                return JSON.parseObject(responseBody, AiTaggingQueryResponseVo.class);
+            }
+        } catch (Exception e) {
+            log.error("调用AI打标查询接口失败", e);
+            AiTaggingQueryResponseVo errorResponse = new AiTaggingQueryResponseVo();
+            errorResponse.setCode("500");
+            errorResponse.setMessage("调用AI打标查询接口失败: " + e.getMessage());
+            return errorResponse;
+        }
+    }
+
+    @Override
+    public AiTaggingResponseVo feedback(AiTaggingFeedbackRequestDto request) {
+        StringBuilder urlBuilder = new StringBuilder(fastApiConfig.getUrl()).append(FEEDBACK_PATH);
+
+        // 构造GET请求参数
+        boolean hasParams = false;
+
+        if (request.getUserId() != null && !request.getUserId().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("user_id=").append(encodeParam(request.getUserId()));
+            hasParams = true;
+        }
+
+        if (request.getUserNm() != null && !request.getUserNm().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("user_nm=").append(encodeParam(request.getUserNm()));
+            hasParams = true;
+        }
+
+        // feedback是必填参数
+        urlBuilder.append(hasParams ? "&" : "?").append("feedback=").append(encodeParam(request.getFeedback()));
+        hasParams = true;
+
+        if (request.getContractNo() != null && !request.getContractNo().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("contract_no=").append(encodeParam(request.getContractNo()));
+            hasParams = true;
+        }
+
+        if (request.getUserOrg() != null && !request.getUserOrg().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("user_org=").append(encodeParam(request.getUserOrg()));
+            hasParams = true;
+        }
+
+        if (request.getUserEndpoint() != null && !request.getUserEndpoint().isEmpty()) {
+            urlBuilder.append(hasParams ? "&" : "?").append("user_endpoint=").append(encodeParam(request.getUserEndpoint()));
+            hasParams = true;
+        }
+
+        // business_attr是必填参数
+        urlBuilder.append(hasParams ? "&" : "?").append("business_attr=").append(encodeParam(request.getBusinessAttr()));
+
+        String url = urlBuilder.toString();
+        log.info("调用AI打标反馈接口: {}", url);
+
+        try (CloseableHttpClient httpClient = createHttpClient()) {
+            HttpGet httpGet = new HttpGet(url);
+
+            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+                String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
+                log.info("AI打标反馈接口响应: {}", responseBody);
+
+                return JSON.parseObject(responseBody, AiTaggingResponseVo.class);
+            }
+        } catch (Exception e) {
+            log.error("调用AI打标反馈接口失败", e);
+            AiTaggingResponseVo errorResponse = new AiTaggingResponseVo();
+            errorResponse.setCode("500");
+            errorResponse.setMessage("调用AI打标反馈接口失败: " + e.getMessage());
+            return errorResponse;
+        }
+    }
+
+
+    // 辅助方法用于URL编码
+    private String encodeParam(String param) {
+        try {
+            return URLEncoder.encode(param, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            log.warn("URL编码失败,使用原始值: {}", param);
+            return param;
+        }
+    }
+
+    /**
+     * 创建HTTP客户端
+     */
+    private CloseableHttpClient createHttpClient() {
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setConnectTimeout(fastApiConfig.getConnectTimeout())
+                .setSocketTimeout(fastApiConfig.getReadTimeout())
+                .build();
+
+        return HttpClients.custom()
+                .setDefaultRequestConfig(requestConfig)
+                .build();
+    }
+}

+ 81 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/util/AuthContextUtil.java

@@ -0,0 +1,81 @@
+package cn.com.yusys.yusp.util;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 认证上下文工具类
+ * 用于存储当前线程的用户信息
+ *
+ * @author AI助手
+ * @date 2026-02-27
+ */
+@Slf4j
+public class AuthContextUtil {
+
+    private static final ThreadLocal<String> currentUser = new ThreadLocal<>();
+    private static final ThreadLocal<String> currentRole = new ThreadLocal<>();
+    private static final ThreadLocal<String> currentUserName = new ThreadLocal<>(); // 新增:存储用户名
+
+    /**
+     * 设置当前用户
+     *
+     * @param userId 用户ID
+     */
+    public static void setCurrentUser(String userId) {
+        currentUser.set(userId);
+    }
+
+    /**
+     * 获取当前用户
+     *
+     * @return 用户ID
+     */
+    public static String getCurrentUser() {
+        return currentUser.get();
+    }
+
+    /**
+     * 设置当前用户角色
+     *
+     * @param role 用户角色
+     */
+    public static void setCurrentRole(String role) {
+        currentRole.set(role);
+    }
+
+    /**
+     * 获取当前用户角色
+     *
+     * @return 用户角色
+     */
+    public static String getCurrentRole() {
+        return currentRole.get();
+    }
+
+    /**
+     * 设置当前用户名
+     *
+     * @param userName 用户名
+     */
+    public static void setCurrentUserName(String userName) {
+        currentUserName.set(userName);
+    }
+
+    /**
+     * 获取当前用户名
+     *
+     * @return 用户名
+     */
+    public static String getCurrentUserName() {
+        return currentUserName.get();
+    }
+
+    /**
+     * 清除当前线程的用户信息
+     */
+    public static void clear() {
+        currentUser.remove();
+        currentRole.remove();
+        currentUserName.remove(); // 新增:清除用户名
+    }
+}

+ 126 - 0
server/yusp-tagging-core/src/main/java/cn/com/yusys/yusp/util/JwtUtil.java

@@ -0,0 +1,126 @@
+package cn.com.yusys.yusp.util;
+
+import cn.com.yusys.yusp.condition.AuthEnabledCondition;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm; // 0.9.1 使用这个枚举
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JWT工具类 (适配 JJWT 0.9.1)
+ *
+ * @author 2643616413
+ * @date 2026-02-27
+ */
+@Slf4j
+@Component
+@Conditional(AuthEnabledCondition.class)
+public class JwtUtil {
+
+    @Value("${jwt.secret:yusp-tagging-secret-key}")
+    private String secret;
+
+    @Value("${jwt.expiration:86400000}")
+    private Long expiration;
+
+    /**
+     * 生成JWT token
+     */
+    public String generateToken(String userId, String role) {
+        Map<String, Object> claims = new HashMap<>();
+        claims.put("userId", userId);
+        claims.put("role", role);
+        return createToken(claims, userId);
+    }
+
+    /**
+     * 创建token
+     */
+    private String createToken(Map<String, Object> claims, String subject) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + expiration);
+
+        return Jwts.builder()
+                .setClaims(claims)
+                .setSubject(subject)
+                .setIssuedAt(now)
+                .setExpiration(expiryDate)
+                // 0.9.1 的 signWith 签名算法在前,密钥在后,且密钥可以是 String
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    /**
+     * 验证token有效性
+     */
+    public Boolean validateToken(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            Date expiration = claims.getExpiration();
+            return !expiration.before(new Date());
+        } catch (Exception e) {
+            log.warn("JWT token验证失败:{}", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 从token中获取用户ID
+     */
+    public String getUserIdFromToken(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            return claims.get("userId", String.class);
+        } catch (Exception e) {
+            log.warn("从token获取用户ID失败:{}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 从token中获取用户角色
+     */
+    public String getRoleFromToken(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            return claims.get("role", String.class);
+        } catch (Exception e) {
+            log.warn("从token获取用户角色失败:{}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 从token中获取声明 (0.9.1 写法)
+     */
+    private Claims getClaimsFromToken(String token) {
+        // 修改点 3: 0.9.1 没有 parserBuilder(),直接使用 parser()
+        // 修改点 4: setSigningKey 直接接受 String secret
+        return Jwts.parser()
+                .setSigningKey(secret)
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    /**
+     * 刷新token
+     */
+    public String refreshToken(String token) {
+        try {
+            Claims claims = getClaimsFromToken(token);
+            String userId = claims.getSubject();
+            String role = claims.get("role", String.class);
+            return generateToken(userId, role);
+        } catch (Exception e) {
+            log.warn("刷新token失败:{}", e.getMessage());
+            return null;
+        }
+    }
+}

+ 12 - 0
server/yusp-tagging-core/src/main/resources/mapper/AitagTagCategoryMapper.xml

@@ -59,4 +59,16 @@
     <select id="selectCountAll" resultType="long">
         SELECT COUNT(*) FROM aitag_tag_category WHERE is_delete = 0
     </select>
+
+    <!-- 分页查询已启用的标签体系 -->
+    <select id="selectPageEnabledCategories" resultType="cn.com.yusys.yusp.domain.entity.AitagTagCategory">
+        SELECT * FROM aitag_tag_category
+        WHERE is_delete = 0 AND state = 0
+            LIMIT #{offset}, #{limit}
+    </select>
+
+    <!-- 统计已启用标签体系的数量 -->
+    <select id="selectCountEnabled" resultType="long">
+        SELECT COUNT(*) FROM aitag_tag_category WHERE is_delete = 0 AND state = 0
+    </select>
 </mapper>

+ 10 - 0
server/yusp-tagging-core/src/main/resources/mapper/AitagUserMapper.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.com.yusys.yusp.mapper.AitagUserMapper">
+
+    <select id="selectByUserId" resultType="cn.com.yusys.yusp.domain.entity.AitagUser">
+        SELECT * FROM aitag_user
+        WHERE user_id = #{userId}
+    </select>
+
+</mapper>

+ 21 - 0
server/yusp-tagging-starter/src/main/java/cn/com/yusys/yusp/detail/App.java

@@ -18,6 +18,27 @@ import java.net.URL;
 @EnableTransactionManagement
 public class App {
     public static void main(String[] args) {
+        // --- 新增诊断代码开始 ---
+        try {
+            // 尝试强制加载 Claims 类
+            Class.forName("io.jsonwebtoken.Claims");
+            System.out.println(">>> [DIAGNOSTIC] SUCCESS: io.jsonwebtoken.Claims is in classpath!");
+
+            // 打印该类来自哪个 Jar 包
+            String location = Class.forName("io.jsonwebtoken.Claims")
+                    .getProtectionDomain()
+                    .getCodeSource()
+                    .getLocation()
+                    .toString();
+            System.out.println(">>> [DIAGNOSTIC] Loaded from: " + location);
+
+        } catch (ClassNotFoundException e) {
+            System.err.println(">>> [DIAGNOSTIC] FATAL: io.jsonwebtoken.Claims NOT found in classpath!");
+            System.err.println(">>> [DIAGNOSTIC] Current Thread Context ClassLoader: " + Thread.currentThread().getContextClassLoader());
+            e.printStackTrace();
+        }
+        // --- 新增诊断代码结束 ---
+
         SpringApplication.run(App.class, args);
     }
 

+ 15 - 1
server/yusp-tagging-starter/src/main/resources/application.yml

@@ -56,4 +56,18 @@ management:
 
 mybatis-plus:
   configuration:
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 控制台输出
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 控制台输出
+
+fastapi:
+  service:
+    url: http://10.192.72.13:9876  # 你的FastAPI服务地址
+    timeout: 10000  # 请求超时时间
+
+# 认证配置
+auth:
+  enabled: false  # 认证开关,true为开启,false为关闭
+
+# JWT配置
+jwt:
+  secret: yusp-tagging-secret-key-for-jwt-token-generation-and-validation-2026
+  expiration: 86400000  # token过期时间(毫秒),默认24小时