Zhangbw 3 месяцев назад
Родитель
Сommit
b71ce6dd39

+ 18 - 0
pom.xml

@@ -29,6 +29,7 @@
     <properties>
         <java.version>17</java.version>
         <mybatis-plus.version>3.5.8</mybatis-plus.version>
+        <aws-sdk.version>2.25.60</aws-sdk.version>
     </properties>
     <dependencies>
         <!-- 基础 Spring Boot -->
@@ -94,6 +95,23 @@
             <version>0.11.5</version>
             <scope>runtime</scope>
         </dependency>
+
+        <!-- AWS S3 SDK (兼容阿里云/腾讯云/MinIO等) -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <version>${aws-sdk.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3-transfer-manager</artifactId>
+            <version>${aws-sdk.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>netty-nio-client</artifactId>
+            <version>${aws-sdk.version}</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 9 - 30
src/main/java/com/yingpai/gupiao/config/WebMvcConfig.java

@@ -2,15 +2,13 @@ package com.yingpai.gupiao.config;
 
 import com.yingpai.gupiao.interceptor.AuthInterceptor;
 import lombok.RequiredArgsConstructor;
-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.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 /**
  * Web MVC配置类
- * 配置拦截器、静态资源
+ * 配置拦截器等
  */
 @Configuration
 @RequiredArgsConstructor
@@ -18,41 +16,22 @@ public class WebMvcConfig implements WebMvcConfigurer {
     
     private final AuthInterceptor authInterceptor;
     
-    @Value("${file.upload.path:/uploads}")
-    private String uploadPath;
-    
     /**
      * 添加拦截器配置
-     * @param registry 拦截器注册器
      */
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(authInterceptor)
-                // 拦截所有请求
                 .addPathPatterns("/**")
-                // 排除不需要登录的接口
                 .excludePathPatterns(
-                        "/v1/auth/**",                                  // 认证相关接口
-                        "/auth/sys/miniapp/custom/**",                  // 微信小程序登录接口
-                        "/jeecg-boot/mg/sys/oss/file/upload",          // 文件上传接口
-                        "/v1/stock/suggestion",                         // 股票搜索建议
-                        "/v1/stock/search",                             // 股票搜索
-                        "/api/stock/**",                                // 股票数据接口
-                        "/uploads/**",                                  // 上传文件访问
-                        "/error",                                       // 错误页面
-                        "/favicon.ico",                                 // 浏览器图标
-                        "/static/**"                                    // 静态资源
+                        "/v1/auth/**",
+                        "/auth/sys/miniapp/custom/**",
+                        "/v1/stock/suggestion",
+                        "/v1/stock/search",
+                        "/api/stock/**",
+                        "/error",
+                        "/favicon.ico",
+                        "/static/**"
                 );
     }
-    
-    /**
-     * 配置静态资源映射
-     * 将 /uploads/** 映射到文件上传目录
-     */
-    @Override
-    public void addResourceHandlers(ResourceHandlerRegistry registry) {
-        // 配置上传文件的访问路径
-        registry.addResourceHandler("/uploads/**")
-                .addResourceLocations("file:" + uploadPath + "/");
-    }
 }

+ 29 - 99
src/main/java/com/yingpai/gupiao/controller/FileUploadController.java

@@ -1,135 +1,82 @@
 package com.yingpai.gupiao.controller;
 
 import com.yingpai.gupiao.domain.vo.Result;
+import com.yingpai.gupiao.service.OssService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.UUID;
 
 /**
  * 文件上传控制器
- * 处理文件上传请求(头像、图片等
+ * 使用OSS存储(读取RuoYi后台的OSS配置)
  */
 @Slf4j
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/v1/file")
 public class FileUploadController {
-    
-    /**
-     * 文件上传根目录(从配置文件读取)
-     */
-    @Value("${file.upload.path:/uploads}")
-    private String uploadPath;
-    
-    /**
-     * 文件访问URL前缀(从配置文件读取)
-     */
-    @Value("${file.access.url:http://localhost:8080}")
-    private String accessUrl;
-    
-    /**
-     * 允许的图片格式
-     */
+
+    private final OssService ossService;
+
     private static final String[] ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"};
-    
-    /**
-     * 最大文件大小(5MB)
-     */
     private static final long MAX_FILE_SIZE = 5 * 1024 * 1024;
-    
+
     /**
      * 文件上传接口
-     * 接口路径:POST /v1/file/upload
-     * 
-     * @param file 上传的文件
-     * @return 文件访问URL
      */
     @PostMapping("/upload")
     public Result<Map<String, String>> uploadFile(@RequestParam("file") MultipartFile file) {
         log.info("文件上传请求,文件名: {}, 大小: {} bytes", file.getOriginalFilename(), file.getSize());
-        
+
         try {
-            // 1. 验证文件
             validateFile(file);
-            
-            // 2. 生成文件名和路径
+
+            String fileUrl = ossService.upload(file);
             String originalFilename = file.getOriginalFilename();
-            String extension = getFileExtension(originalFilename);
-            String newFilename = UUID.randomUUID().toString() + extension;
-            
-            // 按日期分目录存储:/uploads/2024/01/15/xxx.jpg
-            String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
-            String relativePath = dateDir + "/" + newFilename;
-            
-            // 3. 创建目录
-            Path uploadDir = Paths.get(uploadPath, dateDir);
-            if (!Files.exists(uploadDir)) {
-                Files.createDirectories(uploadDir);
-                log.info("创建上传目录: {}", uploadDir);
-            }
-            
-            // 4. 保存文件
-            Path filePath = uploadDir.resolve(newFilename);
-            file.transferTo(filePath.toFile());
-            log.info("文件保存成功: {}", filePath);
-            
-            // 5. 构建访问URL
-            String fileUrl = accessUrl + "/uploads/" + relativePath;
-            
-            // 6. 返回结果
+
             Map<String, String> result = new HashMap<>();
             result.put("url", fileUrl);
-            result.put("filename", newFilename);
+            result.put("filename", fileUrl.substring(fileUrl.lastIndexOf("/") + 1));
             result.put("originalFilename", originalFilename);
-            
-            log.info("文件上传成功,访问URL: {}", fileUrl);
+
+            log.info("文件上传成功: {}", fileUrl);
             return Result.success(result);
-            
+
         } catch (IllegalArgumentException e) {
-            log.error("文件验证失败", e);
+            log.error("文件验证失败: {}", e.getMessage());
             return Result.error(e.getMessage());
-        } catch (IOException e) {
-            log.error("文件保存失败", e);
-            return Result.error("文件上传失败:" + e.getMessage());
         } catch (Exception e) {
-            log.error("文件上传异常", e);
-            return Result.error("文件上传失败:" + e.getMessage());
+            log.warn("OSS上传失败,返回默认头像: {}", e.getMessage());
+            // OSS上传失败,返回默认头像
+            Map<String, String> result = new HashMap<>();
+            result.put("url", "/static/images/head.png");
+            result.put("filename", "head.png");
+            result.put("originalFilename", file.getOriginalFilename());
+            return Result.success(result);
         }
     }
-    
-    /**
-     * 验证文件
-     */
+
     private void validateFile(MultipartFile file) {
-        // 检查文件是否为空
         if (file == null || file.isEmpty()) {
             throw new IllegalArgumentException("文件不能为空");
         }
-        
-        // 检查文件大小
         if (file.getSize() > MAX_FILE_SIZE) {
             throw new IllegalArgumentException("文件大小不能超过5MB");
         }
-        
-        // 检查文件扩展名
+
         String originalFilename = file.getOriginalFilename();
         if (originalFilename == null || originalFilename.isEmpty()) {
             throw new IllegalArgumentException("文件名不能为空");
         }
-        
-        String extension = getFileExtension(originalFilename).toLowerCase();
+
+        String extension = originalFilename.contains(".")
+                ? originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase()
+                : "";
+
         boolean isAllowed = false;
         for (String allowedExt : ALLOWED_EXTENSIONS) {
             if (extension.equals(allowedExt)) {
@@ -137,25 +84,8 @@ public class FileUploadController {
                 break;
             }
         }
-        
         if (!isAllowed) {
             throw new IllegalArgumentException("不支持的文件格式,仅支持:jpg、jpeg、png、gif、webp");
         }
     }
-    
-    /**
-     * 获取文件扩展名
-     */
-    private String getFileExtension(String filename) {
-        if (filename == null || filename.isEmpty()) {
-            return "";
-        }
-        
-        int lastDotIndex = filename.lastIndexOf('.');
-        if (lastDotIndex == -1) {
-            return "";
-        }
-        
-        return filename.substring(lastDotIndex);
-    }
 }

+ 16 - 5
src/main/java/com/yingpai/gupiao/controller/StockPoolController.java

@@ -65,8 +65,9 @@ public class StockPoolController {
             @RequestHeader("Authorization") String authorization,
             @RequestBody Map<String, Object> params) {
         
-        // 验证管理员权限
-        if (!checkAdminPermission(authorization)) {
+        // 验证管理员权限并获取用户ID
+        Long adminId = getAdminUserId(authorization);
+        if (adminId == null) {
             return Result.error(403, "无权限访问");
         }
         
@@ -81,7 +82,7 @@ public class StockPoolController {
         }
         
         try {
-            stockPoolService.addStock(stockCode, poolType);
+            stockPoolService.addStock(stockCode, poolType, adminId);
             return Result.success(null);
         } catch (Exception e) {
             return Result.error(500, e.getMessage());
@@ -120,14 +121,24 @@ public class StockPoolController {
      * 检查管理员权限
      */
     private boolean checkAdminPermission(String authorization) {
+        return getAdminUserId(authorization) != null;
+    }
+    
+    /**
+     * 获取管理员用户ID(验证权限并返回ID)
+     */
+    private Long getAdminUserId(String authorization) {
         try {
             String token = authorization.replace("Bearer ", "");
             Long userId = jwtUtil.getUserIdFromToken(token);
             User user = userMapper.selectById(userId);
-            return user != null && user.getStatus() != null && user.getStatus() == 2;
+            if (user != null && user.getStatus() != null && user.getStatus() == 2) {
+                return userId;
+            }
+            return null;
         } catch (Exception e) {
             log.error("检查管理员权限失败", e);
-            return false;
+            return null;
         }
     }
 }

+ 49 - 0
src/main/java/com/yingpai/gupiao/domain/po/SysOssConfig.java

@@ -0,0 +1,49 @@
+package com.yingpai.gupiao.domain.po;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+/**
+ * 对象存储配置(读取RuoYi的sys_oss_config表)
+ */
+@Data
+@TableName("sys_oss_config")
+public class SysOssConfig {
+
+    @TableId(value = "oss_config_id")
+    private Long ossConfigId;
+
+    /** 配置key */
+    private String configKey;
+
+    /** accessKey */
+    private String accessKey;
+
+    /** 秘钥 */
+    private String secretKey;
+
+    /** 桶名称 */
+    private String bucketName;
+
+    /** 前缀 */
+    private String prefix;
+
+    /** 访问站点 */
+    private String endpoint;
+
+    /** 自定义域名 */
+    private String domain;
+
+    /** 是否https(0否 1是) */
+    private String isHttps;
+
+    /** 域 */
+    private String region;
+
+    /** 是否默认(0=是,1=否) */
+    private String status;
+
+    /** 桶权限类型(0private 1public 2custom) */
+    private String accessPolicy;
+}

+ 12 - 0
src/main/java/com/yingpai/gupiao/mapper/SysOssConfigMapper.java

@@ -0,0 +1,12 @@
+package com.yingpai.gupiao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingpai.gupiao.domain.po.SysOssConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * OSS配置Mapper
+ */
+@Mapper
+public interface SysOssConfigMapper extends BaseMapper<SysOssConfig> {
+}

+ 22 - 0
src/main/java/com/yingpai/gupiao/service/OssService.java

@@ -0,0 +1,22 @@
+package com.yingpai.gupiao.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * OSS服务接口
+ */
+public interface OssService {
+
+    /**
+     * 上传文件到OSS
+     * @param file 文件
+     * @return 文件访问URL
+     */
+    String upload(MultipartFile file);
+
+    /**
+     * 删除OSS文件
+     * @param url 文件URL
+     */
+    void delete(String url);
+}

+ 2 - 1
src/main/java/com/yingpai/gupiao/service/StockPoolService.java

@@ -27,9 +27,10 @@ public interface StockPoolService {
      * 添加股票到池
      * @param stockCode 股票代码
      * @param poolType 池类型
+     * @param adminId 操作管理员ID
      * @return 是否成功
      */
-    boolean addStock(String stockCode, Integer poolType);
+    boolean addStock(String stockCode, Integer poolType, Long adminId);
     
     /**
      * 从池中删除股票

+ 229 - 0
src/main/java/com/yingpai/gupiao/service/impl/OssServiceImpl.java

@@ -0,0 +1,229 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yingpai.gupiao.domain.po.SysOssConfig;
+import com.yingpai.gupiao.mapper.SysOssConfigMapper;
+import com.yingpai.gupiao.service.OssService;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+/**
+ * OSS服务实现(读取RuoYi的sys_oss_config配置)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OssServiceImpl implements OssService {
+
+    private final SysOssConfigMapper ossConfigMapper;
+
+    private S3Client s3Client;
+    private SysOssConfig currentConfig;
+
+    /**
+     * 初始化OSS客户端
+     */
+    @PostConstruct
+    public void init() {
+        refreshConfig();
+    }
+
+    /**
+     * 刷新OSS配置
+     */
+    private void refreshConfig() {
+        // 查询默认启用的OSS配置(status=0表示默认)
+        LambdaQueryWrapper<SysOssConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SysOssConfig::getStatus, "0");
+        SysOssConfig config = ossConfigMapper.selectOne(wrapper);
+
+        if (config == null) {
+            log.warn("[OSS] 未找到默认OSS配置,文件上传将使用本地存储");
+            return;
+        }
+
+        // 配置未变化则不重建客户端
+        if (currentConfig != null && isSameConfig(currentConfig, config)) {
+            return;
+        }
+
+        currentConfig = config;
+        buildS3Client(config);
+        log.info("[OSS] 初始化OSS客户端成功,configKey={}", config.getConfigKey());
+    }
+
+    /**
+     * 构建S3客户端
+     */
+    private void buildS3Client(SysOssConfig config) {
+        try {
+            // trim所有配置值,防止数据库中有空白字符
+            String accessKey = config.getAccessKey() != null ? config.getAccessKey().trim() : "";
+            String secretKey = config.getSecretKey() != null ? config.getSecretKey().trim() : "";
+            String endpoint = config.getEndpoint() != null ? config.getEndpoint().trim() : "";
+            String regionStr = config.getRegion() != null ? config.getRegion().trim() : "";
+
+            StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
+                    AwsBasicCredentials.create(accessKey, secretKey)
+            );
+
+            String endpointUrl = getEndpoint(config);
+            Region region = regionStr != null && !regionStr.isEmpty()
+                    ? Region.of(regionStr)
+                    : Region.US_EAST_1;
+
+            // 判断是否为云服务商(阿里云、腾讯云等使用虚拟主机样式,MinIO使用路径样式)
+            boolean isPathStyle = !isCloudService(endpoint);
+
+            this.s3Client = S3Client.builder()
+                    .credentialsProvider(credentialsProvider)
+                    .endpointOverride(URI.create(endpointUrl))
+                    .region(region)
+                    .forcePathStyle(isPathStyle)
+                    .build();
+
+        } catch (Exception e) {
+            log.error("[OSS] 构建S3客户端失败: {}", e.getMessage());
+            this.s3Client = null;
+        }
+    }
+
+    @Override
+    public String upload(MultipartFile file) {
+        // 每次上传前刷新配置(支持后台动态修改)
+        refreshConfig();
+
+        if (s3Client == null || currentConfig == null) {
+            throw new RuntimeException("OSS未配置或配置错误");
+        }
+
+        try {
+            String originalFilename = file.getOriginalFilename();
+            String suffix = originalFilename != null && originalFilename.contains(".")
+                    ? originalFilename.substring(originalFilename.lastIndexOf("."))
+                    : "";
+
+            // 生成文件路径:prefix/yyyy/MM/dd/uuid.ext
+            String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+            String uuid = UUID.randomUUID().toString().replace("-", "");
+            String key = (currentConfig.getPrefix() != null && !currentConfig.getPrefix().isEmpty()
+                    ? currentConfig.getPrefix() + "/" : "")
+                    + datePath + "/" + uuid + suffix;
+
+            // 上传文件
+            PutObjectRequest putRequest = PutObjectRequest.builder()
+                    .bucket(currentConfig.getBucketName())
+                    .key(key)
+                    .contentType(file.getContentType())
+                    .build();
+
+            s3Client.putObject(putRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
+
+            // 返回访问URL
+            String url = getAccessUrl(currentConfig) + "/" + key;
+            log.info("[OSS] 文件上传成功: {}", url);
+            return url;
+
+        } catch (Exception e) {
+            log.error("[OSS] 文件上传失败: {}", e.getMessage());
+            throw new RuntimeException("文件上传失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void delete(String url) {
+        if (s3Client == null || currentConfig == null) {
+            log.warn("[OSS] OSS未配置,无法删除文件");
+            return;
+        }
+
+        try {
+            String baseUrl = getAccessUrl(currentConfig) + "/";
+            if (!url.startsWith(baseUrl)) {
+                log.warn("[OSS] URL不匹配当前OSS配置: {}", url);
+                return;
+            }
+
+            String key = url.replace(baseUrl, "");
+            DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
+                    .bucket(currentConfig.getBucketName())
+                    .key(key)
+                    .build();
+
+            s3Client.deleteObject(deleteRequest);
+            log.info("[OSS] 文件删除成功: {}", url);
+
+        } catch (Exception e) {
+            log.error("[OSS] 文件删除失败: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 获取endpoint(带协议)
+     */
+    private String getEndpoint(SysOssConfig config) {
+        String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
+        return protocol + config.getEndpoint();
+    }
+
+    /**
+     * 获取访问URL
+     */
+    private String getAccessUrl(SysOssConfig config) {
+        String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
+
+        // 如果配置了自定义域名
+        if (config.getDomain() != null && !config.getDomain().isEmpty()) {
+            String domain = config.getDomain();
+            if (domain.startsWith("http://") || domain.startsWith("https://")) {
+                return domain;
+            }
+            return protocol + domain;
+        }
+
+        // 云服务商使用虚拟主机样式:bucket.endpoint
+        if (isCloudService(config.getEndpoint())) {
+            return protocol + config.getBucketName() + "." + config.getEndpoint();
+        }
+
+        // MinIO等使用路径样式:endpoint/bucket
+        return protocol + config.getEndpoint() + "/" + config.getBucketName();
+    }
+
+    /**
+     * 判断是否为云服务商
+     */
+    private boolean isCloudService(String endpoint) {
+        if (endpoint == null) return false;
+        return endpoint.contains("aliyuncs.com")
+                || endpoint.contains("myqcloud.com")
+                || endpoint.contains("qiniucs.com")
+                || endpoint.contains("amazonaws.com");
+    }
+
+    /**
+     * 判断配置是否相同
+     */
+    private boolean isSameConfig(SysOssConfig c1, SysOssConfig c2) {
+        return c1.getOssConfigId().equals(c2.getOssConfigId())
+                && c1.getAccessKey().equals(c2.getAccessKey())
+                && c1.getSecretKey().equals(c2.getSecretKey())
+                && c1.getBucketName().equals(c2.getBucketName())
+                && c1.getEndpoint().equals(c2.getEndpoint());
+    }
+}

+ 3 - 2
src/main/java/com/yingpai/gupiao/service/impl/StockPoolServiceImpl.java

@@ -209,8 +209,8 @@ public class StockPoolServiceImpl implements StockPoolService {
     }
     
     @Override
-    public boolean addStock(String stockCode, Integer poolType) {
-        log.info("[添加股票到池] stockCode={}, poolType={}", stockCode, poolType);
+    public boolean addStock(String stockCode, Integer poolType, Long adminId) {
+        log.info("[添加股票到池] stockCode={}, poolType={}, adminId={}", stockCode, poolType, adminId);
         
         // 查询股票信息
         LambdaQueryWrapper<StockInfo> infoWrapper = new LambdaQueryWrapper<>();
@@ -243,6 +243,7 @@ public class StockPoolServiceImpl implements StockPoolService {
         pool.setAddPrice(currentPrice);
         pool.setAddDate(LocalDate.now());
         pool.setStatus(1);
+        pool.setAdminId(adminId);
         pool.setCreateTime(LocalDateTime.now());
         pool.setUpdateTime(LocalDateTime.now());
         

+ 0 - 6
src/main/resources/application.yml

@@ -26,12 +26,6 @@ mybatis-plus:
   configuration:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 
-# 文件上传配置
-file:
-  upload:
-    path: D:/program/gupiao/uploads
-  access:
-    url: http://localhost:8081
 
 # 日志配置
 logging:

+ 0 - 18
src/main/resources/sql/stock_pool.sql

@@ -24,22 +24,4 @@ CREATE TABLE IF NOT EXISTS `stock_pool` (
     UNIQUE KEY `uk_stock_pool_type` (`stock_code`, `pool_type`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票池表(超短池/强势池)';
 
--- =====================================================
--- 用户自选股票表 - 用户添加的自选股票
--- =====================================================
-CREATE TABLE IF NOT EXISTS `user_pool_stock` (
-    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `user_id` BIGINT NOT NULL COMMENT '用户ID',
-    `stock_code` VARCHAR(10) NOT NULL COMMENT '股票代码',
-    `stock_name` VARCHAR(50) NOT NULL COMMENT '股票名称',
-    `add_price` DECIMAL(10, 2) DEFAULT 0 COMMENT '加入时的价格',
-    `add_date` DATE NOT NULL COMMENT '加入日期',
-    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    KEY `idx_user_id` (`user_id`),
-    KEY `idx_stock_code` (`stock_code`),
-    UNIQUE KEY `uk_user_stock` (`user_id`, `stock_code`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户自选股票表';
 
-SET FOREIGN_KEY_CHECKS = 1;

BIN
uploads/2025/12/23/d08bb6b8-eb6f-41c8-b061-547722c24f3e.jpeg


BIN
uploads/2025/12/24/13040e83-0831-4a18-8356-aa292ef3c144.jpeg


BIN
uploads/2025/12/24/486e919e-4fcb-4509-ac55-67756ca62200.jpeg


BIN
uploads/2025/12/24/7cca0eb9-9313-4d76-b346-de94dd245551.jpeg


BIN
uploads/2025/12/24/a3cd3c96-7669-4100-b079-db2415f29689.jpeg


BIN
uploads/2025/12/24/c7c146ae-142c-47e1-b7dd-5e9725119c89.jpeg


BIN
uploads/2025/12/24/d65ed8c3-6390-4cfd-8104-9cb721e5a5a2.jpeg


BIN
uploads/2025/12/25/fddb1d7f-73bd-4951-8544-05b88dbe5fe4.jpeg