Zhangbw 2 месяцев назад
Родитель
Сommit
5219d2b5f3

+ 110 - 0
src/main/java/com/yingpai/gupiao/controller/AuthController.java

@@ -0,0 +1,110 @@
+package com.yingpai.gupiao.controller;
+
+import com.yingpai.gupiao.domain.dto.*;
+import com.yingpai.gupiao.domain.vo.LoginVO;
+import com.yingpai.gupiao.domain.vo.Result;
+import com.yingpai.gupiao.domain.vo.WxLoginCheckVO;
+import com.yingpai.gupiao.service.AuthService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 认证控制器
+ * 处理用户登录、注册相关请求
+ *
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/v1/auth/wx")
+public class AuthController {
+    
+    private final AuthService authService;
+    
+    /**
+     * 第一步:微信静默登录(检查是否为老用户)
+     * 接口路径:POST /v1/auth/wx/silent-login
+     * 
+     * @param dto 包含 loginCode
+     * @return 老用户返回 { isSign: "true", token },新用户返回 { isSign: "false" }
+     */
+    @PostMapping("/silent-login")
+    public Result<WxLoginCheckVO> wxSilentLogin(@RequestBody WxSilentLoginDTO dto) {
+        log.info("【第一步】微信静默登录,loginCode: {}", dto.getLoginCode());
+        
+        if (dto.getLoginCode() == null || dto.getLoginCode().isEmpty()) {
+            return Result.error("登录code不能为空");
+        }
+        
+        try {
+            WxLoginCheckVO result = authService.wxSilentLogin(dto.getLoginCode());
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("微信静默登录失败", e);
+            return Result.error("登录失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 第二步:手机号授权验证(新用户)
+     * 接口路径:POST /v1/auth/wx/phone-verify
+     * 
+     * @param dto 包含 loginCode, encryptedData, iv
+     * @return 已注册返回 { isSign: "true", token },未注册返回 { isSign: "false", openid, unionid, phoneNumber }
+     */
+    @PostMapping("/phone-verify")
+    public Result<WxLoginCheckVO> wxPhoneVerify(@RequestBody WxPhoneLoginDTO dto) {
+        log.info("【第二步】手机号授权验证,loginCode: {}", dto.getLoginCode());
+        
+        if (dto.getLoginCode() == null || dto.getLoginCode().isEmpty()) {
+            return Result.error("登录code不能为空");
+        }
+        
+        if (dto.getEncryptedData() == null || dto.getIv() == null) {
+            return Result.error("手机号加密数据不能为空");
+        }
+        
+        try {
+            WxLoginCheckVO result = authService.wxPhoneCheck(dto);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("手机号授权验证失败", e);
+            return Result.error("验证失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 第三步:完善用户信息(新用户)
+     * 接口路径:POST /v1/auth/wx/register
+     * 
+     * @param dto 包含 openid, unionid, phoneNumber, nickname, avatarUrl
+     * @return 返回 { token }
+     */
+    @PostMapping("/register")
+    public Result<LoginVO> wxRegister(@RequestBody WxCompleteUserInfoDTO dto) {
+        log.info("【第三步】完善用户信息,openid: {}, phone: {}, nickname: {}", 
+                dto.getOpenid(), dto.getPhoneNumber(), dto.getNickname());
+        
+        if (dto.getOpenid() == null || dto.getOpenid().isEmpty()) {
+            return Result.error("openid不能为空");
+        }
+        
+        if (dto.getPhoneNumber() == null || dto.getPhoneNumber().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        
+        if (dto.getNickname() == null || dto.getNickname().isEmpty()) {
+            return Result.error("昵称不能为空");
+        }
+        
+        try {
+            LoginVO result = authService.wxCompleteUserInfo(dto);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("完善用户信息失败", e);
+            return Result.error("注册失败:" + e.getMessage());
+        }
+    }
+
+}

+ 2 - 17
src/main/java/com/yingpai/gupiao/domain/po/StockPool.java

@@ -49,24 +49,9 @@ public class StockPool {
      * 加入日期
      */
     private LocalDate addDate;
-
-    /**
-     * 当日收盘价
-     */
-    private BigDecimal closePrice;
-
-    /**
-     * 隔日最高价
-     */
-    private BigDecimal nextDayHigh;
-
-    /**
-     * 隔日涨幅(%)
-     */
-    private BigDecimal nextDayGain;
-
+    
     /**
-     * 状态:0-已移除,1-历史有效,2-当前有效
+     * 状态:1-有效,0-已移除
      */
     private Integer status;
     

+ 1 - 6
src/main/java/com/yingpai/gupiao/domain/po/User.java

@@ -39,12 +39,7 @@ public class User {
      * 手机号
      */
     private String phone;
-
-    /**
-     * 登录密码(明文存储)
-     */
-    private String password;
-
+    
     /**
      * 用户昵称
      */

+ 7 - 10
src/main/java/com/yingpai/gupiao/domain/vo/WxPayVO.java

@@ -14,28 +14,25 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor
 @AllArgsConstructor
 public class WxPayVO {
-
+    
     /** 订单号 */
     private String orderNo;
-
-    /** 微信AppID(H5支付需要) */
-    private String appId;
-
+    
     /** 时间戳 */
     private String timeStamp;
-
+    
     /** 随机字符串 */
     private String nonceStr;
-
+    
     /** 预支付交易会话标识(prepay_id=xxx) */
     private String packageValue;
-
+    
     /** 签名类型 */
     private String signType;
-
+    
     /** 签名 */
     private String paySign;
-
+    
     /** 支付金额(单位:分) */
     @JsonProperty("total_fee")
     private Integer totalFee;

+ 74 - 0
src/main/java/com/yingpai/gupiao/mapper/StockPoolHistoryMapper.java

@@ -0,0 +1,74 @@
+package com.yingpai.gupiao.mapper;
+
+import com.yingpai.gupiao.domain.vo.StockHistoryVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 股票池历史数据Mapper(小程序端)
+ */
+@Mapper
+public interface StockPoolHistoryMapper {
+    
+    /**
+     * 查询指定日期区间的历史数据(带隔日信息),根据池类型筛选
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param poolType 池类型:1-超短池,2-强势池
+     * @param offset 偏移量
+     * @param limit 每页数量
+     * @return 历史数据列表
+     */
+    List<StockHistoryVO> selectHistoryWithNextDay(
+        @Param("startDate") LocalDate startDate,
+        @Param("endDate") LocalDate endDate,
+        @Param("poolType") Integer poolType,
+        @Param("offset") int offset,
+        @Param("limit") int limit
+    );
+    
+    /**
+     * 统计指定日期区间的记录总数,根据池类型筛选
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param poolType 池类型:1-超短池,2-强势池
+     * @return 记录总数
+     */
+    int countHistory(
+        @Param("startDate") LocalDate startDate,
+        @Param("endDate") LocalDate endDate,
+        @Param("poolType") Integer poolType
+    );
+    
+    /**
+     * 查询统计数据(成功率、平均收益、总交易次数)
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param poolType 池类型:1-超短池,2-强势池
+     * @return 统计数据Map
+     */
+    Map<String, Object> selectHistoryStats(
+        @Param("startDate") LocalDate startDate,
+        @Param("endDate") LocalDate endDate,
+        @Param("poolType") Integer poolType
+    );
+    
+    /**
+     * 根据股票代码或名称模糊查询最新的历史记录
+     * @param keyword 搜索关键词(股票代码或名称)
+     * @return 最新的历史记录
+     */
+    StockHistoryVO selectLatestByKeyword(@Param("keyword") String keyword);
+
+    /**
+     * 根据股票代码或名称和日期查询历史记录
+     * @param keyword 搜索关键词(股票代码或名称)
+     * @param recordDate 记录日期
+     * @return 指定日期的历史记录
+     */
+    StockHistoryVO selectByKeywordAndDate(@Param("keyword") String keyword, @Param("recordDate") LocalDate recordDate);
+}

+ 0 - 56
src/main/java/com/yingpai/gupiao/mapper/StockPoolMapper.java

@@ -3,66 +3,10 @@ package com.yingpai.gupiao.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.yingpai.gupiao.domain.po.StockPool;
 import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Select;
-
-import java.time.LocalDate;
-import java.util.List;
 
 /**
  * 股票池Mapper
  */
 @Mapper
 public interface StockPoolMapper extends BaseMapper<StockPool> {
-
-    /**
-     * 查询指定池类型的最新加入日期
-     * @param poolType 池类型
-     * @return 最新日期
-     */
-    @Select("SELECT MAX(add_date) FROM stock_pool WHERE pool_type = #{poolType} AND status IN (1, 2)")
-    LocalDate selectLatestAddDate(Integer poolType);
-
-    /**
-     * 分页查询历史数据(status为1和2的记录)
-     * @param startDate 开始日期
-     * @param endDate 结束日期
-     * @param poolType 池类型
-     * @param offset 偏移量
-     * @param limit 限制数量
-     * @return 历史数据列表
-     */
-    @Select("SELECT stock_code, stock_name, add_date as recordDate, add_price, close_price as closePrice, " +
-            "next_day_high as nextDayHighPrice, next_day_gain as nextDayHighTrend, status " +
-            "FROM stock_pool " +
-            "WHERE add_date BETWEEN #{startDate} AND #{endDate} " +
-            "AND pool_type = #{poolType} " +
-            "AND status IN (1, 2) " +
-            "ORDER BY add_date DESC, id DESC " +
-            "LIMIT #{limit} OFFSET #{offset}")
-    List<com.yingpai.gupiao.domain.vo.StockHistoryVO> selectPoolHistory(
-        LocalDate startDate, LocalDate endDate, Integer poolType, int offset, int limit);
-
-    /**
-     * 统计历史数据总数
-     * @param startDate 开始日期
-     * @param endDate 结束日期
-     * @param poolType 池类型
-     * @return 总数
-     */
-    @Select("SELECT COUNT(*) FROM stock_pool " +
-            "WHERE add_date BETWEEN #{startDate} AND #{endDate} " +
-            "AND pool_type = #{poolType} " +
-            "AND status IN (1, 2)")
-    int countPoolHistory(LocalDate startDate, LocalDate endDate, Integer poolType);
-
-    /**
-     * 从历史信息表中查询收盘价
-     * @param stockCode 股票代码
-     * @param recordDate 记录日期
-     * @return 收盘价
-     */
-    @Select("SELECT close_price FROM stock_pool_history " +
-            "WHERE stock_code = #{stockCode} AND record_date = #{recordDate} " +
-            "LIMIT 1")
-    java.math.BigDecimal selectClosePriceFromHistory(String stockCode, LocalDate recordDate);
 }

+ 41 - 0
src/main/java/com/yingpai/gupiao/service/AuthService.java

@@ -0,0 +1,41 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.dto.WxCompleteUserInfoDTO;
+import com.yingpai.gupiao.domain.dto.WxPhoneLoginDTO;
+import com.yingpai.gupiao.domain.vo.LoginVO;
+import com.yingpai.gupiao.domain.vo.WxLoginCheckVO;
+
+/**
+ * 认证服务接口
+ * 提供用户登录相关功能
+ * 
+ * 完整登录流程:
+ * 1. wxSilentLogin - 检查是否为老用户
+ * 2. wxPhoneCheck - 验证手机号
+ * 3. wxCompleteUserInfo - 完善用户信息
+ */
+public interface AuthService {
+    
+    /**
+     * 第一步:微信静默登录(检查是否为老用户)
+     * @param loginCode 微信登录code
+     * @return 老用户返回token,新用户返回isSign=false
+     */
+    WxLoginCheckVO wxSilentLogin(String loginCode);
+    
+    /**
+     * 第二步:手机号授权验证
+     * @param dto 包含loginCode和加密phone
+     * @return 已注册返回token,未注册返回用户信息
+     */
+    WxLoginCheckVO wxPhoneCheck(WxPhoneLoginDTO dto);
+    
+    /**
+     * 第三步:完善用户信息(首次登录)
+     * @param dto 包含完整用户信息
+     * @return 返回token
+     */
+    LoginVO wxCompleteUserInfo(WxCompleteUserInfoDTO dto);
+}
+
+

+ 247 - 0
src/main/java/com/yingpai/gupiao/service/impl/AuthServiceImpl.java

@@ -0,0 +1,247 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yingpai.gupiao.domain.dto.WxCompleteUserInfoDTO;
+import com.yingpai.gupiao.domain.dto.WxLoginResponse;
+import com.yingpai.gupiao.domain.dto.WxPhoneLoginDTO;
+import com.yingpai.gupiao.domain.po.User;
+import com.yingpai.gupiao.domain.vo.LoginVO;
+import com.yingpai.gupiao.domain.vo.WxLoginCheckVO;
+import com.yingpai.gupiao.mapper.UserMapper;
+import com.yingpai.gupiao.service.AuthService;
+import com.yingpai.gupiao.util.JwtUtil;
+import com.yingpai.gupiao.util.WxApiUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+/**
+ * 认证服务实现类
+ * 用于处理微信小程序登录认证
+ * 
+ * 完整登录流程:
+ * 1. 静默登录:wx.login() -> POST /v1/auth/wx/silent-login(老用户直接返回token)
+ * 2. 新用户获取头像昵称:前端弹窗选择头像和输入昵称
+ * 3. 手机号授权:getPhoneNumber -> POST /v1/auth/wx/phone-verify(已注册用户返回token,未注册返回openid等信息)
+ * 4. 完善用户信息:头像+昵称+手机号 -> POST /v1/auth/wx/register(创建新用户,返回token)
+ * 5. 获取用户信息:token -> GET /v1/user/info
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AuthServiceImpl implements AuthService {
+    
+    private final UserMapper userMapper;
+    private final JwtUtil jwtUtil;
+    private final WxApiUtil wxApiUtil;
+    
+    /**
+     * 第一步:微信静默登录
+     * 小程序调用 wx.login() 获取 code,发送到后端检查是否为老用户
+     * 
+     * @param loginCode 微信登录code
+     * @return 老用户返回 isSign=true + token,新用户返回 isSign=false
+     */
+    @Override
+    public WxLoginCheckVO wxSilentLogin(String loginCode) {
+        try {
+            log.info("【第一步】微信静默登录,loginCode: {}", loginCode);
+            
+            // 1. 调用微信API获取openid和unionid
+            WxLoginResponse wxResponse = wxApiUtil.getOpenidByCode(loginCode);
+            String openid = wxResponse.getOpenid();
+            String unionid = wxResponse.getUnionid();
+            
+            log.info("获取微信信息成功,openid: {}, unionid: {}", openid, unionid);
+            
+            // 2. 根据openid查询用户
+            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(User::getOpenid, openid);
+            User user = userMapper.selectOne(wrapper);
+            
+            // 3. 判断用户是否存在
+            if (user == null) {
+                // 新用户,返回 isSign=false
+                log.info("新用户,需要授权手机号");
+                return WxLoginCheckVO.builder()
+                        .isSign("false")
+                        .build();
+            }
+            
+            // 4. 检查用户状态
+            if (user.getStatus() != null && user.getStatus() == 1) {
+                // 账号被禁用
+                log.warn("账号被禁用,userId: {}", user.getId());
+                return WxLoginCheckVO.builder()
+                        .code(103)
+                        .message("账号已被禁用")
+                        .build();
+            }
+            
+            // 5. 老用户,生成token并返回
+            String token = jwtUtil.generateToken(user.getId());
+            log.info("老用户登录成功,userId: {}, token已生成", user.getId());
+            
+            return WxLoginCheckVO.builder()
+                    .isSign("true")
+                    .token(token)
+                    .build();
+                    
+        } catch (Exception e) {
+            log.error("微信静默登录失败", e);
+            throw new RuntimeException("微信静默登录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 第二步(新用户):手机号授权验证
+     * 小程序调用 getPhoneNumber 获取加密手机号,发送到后端解密验证
+     * 
+     * @param dto 包含 loginCode、encryptedData、iv
+     * @return 已注册用户返回 isSign=true + token,未注册返回 isSign=false + openid/unionid/phoneNumber
+     */
+    @Override
+    public WxLoginCheckVO wxPhoneCheck(WxPhoneLoginDTO dto) {
+        try {
+            log.info("【第二步】手机号授权验证,loginCode: {}", dto.getLoginCode());
+            
+            // 1. 调用微信API获取openid、unionid和session_key
+            WxLoginResponse wxResponse = wxApiUtil.getOpenidByCode(dto.getLoginCode());
+            String openid = wxResponse.getOpenid();
+            String unionid = wxResponse.getUnionid();
+            String sessionKey = wxResponse.getSessionKey();
+            
+            log.info("获取微信信息成功,openid: {}, unionid: {}, sessionKey已获取", openid, unionid);
+            
+            // 2. 解密手机号(使用encryptedData和iv方式)
+            String phone = null;
+            
+            if (dto.getEncryptedData() != null && dto.getIv() != null && sessionKey != null) {
+                // 使用本地解密方式
+                log.info("使用encryptedData解密手机号");
+                phone = wxApiUtil.decryptPhoneNumber(dto.getEncryptedData(), sessionKey, dto.getIv());
+                log.info("解密手机号成功: {}", phone);
+            } else {
+                log.error("缺少解密参数:encryptedData={}, iv={}, sessionKey={}", 
+                    dto.getEncryptedData() != null, dto.getIv() != null, sessionKey != null);
+                throw new RuntimeException("缺少手机号解密参数");
+            }
+            
+            // 3. 根据手机号查询用户
+            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(User::getPhone, phone);
+            User user = userMapper.selectOne(wrapper);
+            
+            // 4. 判断用户是否存在
+            if (user == null) {
+                // 未注册,返回用户信息供前端完善资料
+                log.info("未注册用户,需要完善信息");
+                return WxLoginCheckVO.builder()
+                        .isSign("false")
+                        .openid(openid)
+                        .unionid(unionid)
+                        .phoneNumber(phone)
+                        .build();
+            }
+            
+            // 5. 已注册用户,更新openid和unionid(如果没有)
+            boolean needUpdate = false;
+            
+            if (user.getOpenid() == null || user.getOpenid().isEmpty()) {
+                user.setOpenid(openid);
+                needUpdate = true;
+            }
+            
+            if (unionid != null && (user.getUnionid() == null || user.getUnionid().isEmpty())) {
+                user.setUnionid(unionid);
+                needUpdate = true;
+            }
+            
+            if (needUpdate) {
+                user.setUpdateTime(LocalDateTime.now());
+                userMapper.updateById(user);
+                log.info("更新用户微信信息,userId: {}", user.getId());
+            }
+            
+            // 6. 生成token并返回
+            String token = jwtUtil.generateToken(user.getId());
+            log.info("已注册用户登录成功,userId: {}, token已生成", user.getId());
+            
+            return WxLoginCheckVO.builder()
+                    .isSign("true")
+                    .token(token)
+                    .build();
+                    
+        } catch (Exception e) {
+            log.error("手机号授权验证失败", e);
+            throw new RuntimeException("手机号授权验证失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 第三步(新用户):完善用户信息并注册
+     * 小程序提交完整的用户信息(头像+昵称+手机号+openid等)完成注册
+     * 
+     * @param dto 包含 openid、unionid、phoneNumber、nickname、avatarUrl
+     * @return 返回 token
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LoginVO wxCompleteUserInfo(WxCompleteUserInfoDTO dto) {
+        try {
+            log.info("【第三步】完善用户信息,openid: {}, phone: {}, nickname: {}", 
+                    dto.getOpenid(), dto.getPhoneNumber(), dto.getNickname());
+            
+            // 1. 检查手机号是否已被注册
+            LambdaQueryWrapper<User> phoneWrapper = new LambdaQueryWrapper<>();
+            phoneWrapper.eq(User::getPhone, dto.getPhoneNumber());
+            User existUser = userMapper.selectOne(phoneWrapper);
+            
+            if (existUser != null) {
+                log.warn("手机号已被注册,phone: {}, userId: {}", dto.getPhoneNumber(), existUser.getId());
+                throw new RuntimeException("该手机号已被注册");
+            }
+            
+            // 2. 检查openid是否已存在
+            LambdaQueryWrapper<User> openidWrapper = new LambdaQueryWrapper<>();
+            openidWrapper.eq(User::getOpenid, dto.getOpenid());
+            existUser = userMapper.selectOne(openidWrapper);
+            
+            if (existUser != null) {
+                log.warn("openid已存在,openid: {}, userId: {}", dto.getOpenid(), existUser.getId());
+                throw new RuntimeException("该微信账号已注册");
+            }
+            
+            // 3. 创建新用户
+            User newUser = User.builder()
+                    .openid(dto.getOpenid())
+                    .unionid(dto.getUnionid())
+                    .phone(dto.getPhoneNumber())
+                    .nickname(dto.getNickname())
+                    .avatar(dto.getAvatarUrl())
+                    .status(0)
+                    .createTime(LocalDateTime.now())
+                    .updateTime(LocalDateTime.now())
+                    .build();
+            
+            userMapper.insert(newUser);
+            log.info("创建新用户成功,userId: {}, openid: {}, phone: {}, nickname: {}", 
+                    newUser.getId(), dto.getOpenid(), dto.getPhoneNumber(), dto.getNickname());
+            
+            // 4. 生成token
+            String token = jwtUtil.generateToken(newUser.getId());
+            
+            // 5. 返回token
+            return LoginVO.builder()
+                    .token(token)
+                    .build();
+            
+        } catch (Exception e) {
+            log.error("完善用户信息失败", e);
+            throw new RuntimeException("注册失败: " + e.getMessage());
+        }
+    }
+}

+ 116 - 69
src/main/java/com/yingpai/gupiao/service/impl/StockHistoryServiceImpl.java

@@ -1,12 +1,14 @@
 package com.yingpai.gupiao.service.impl;
 
 import com.yingpai.gupiao.domain.vo.StockHistoryVO;
-import com.yingpai.gupiao.mapper.StockPoolMapper;
+import com.yingpai.gupiao.mapper.StockPoolHistoryMapper;
 import com.yingpai.gupiao.service.StockHistoryService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.util.HashMap;
 import java.util.List;
@@ -14,43 +16,30 @@ import java.util.Map;
 
 /**
  * 股票历史数据服务实现类(小程序端)
- * 查询stock_pool表中status为1和2的记录
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class StockHistoryServiceImpl implements StockHistoryService {
-
-    private final StockPoolMapper stockPoolMapper;
+    
+    private final StockPoolHistoryMapper stockPoolHistoryMapper;
     
     @Override
     public Map<String, Object> queryHistoryPage(LocalDate startDate, LocalDate endDate, Integer poolType, int pageNum, int pageSize) {
         // 计算偏移量
         int offset = (pageNum - 1) * pageSize;
-
-        // 查询stock_pool表中status为1和2的数据
-        List<StockHistoryVO> list = stockPoolMapper.selectPoolHistory(
+        
+        // 查询数据
+        List<StockHistoryVO> list = stockPoolHistoryMapper.selectHistoryWithNextDay(
             startDate, endDate, poolType, offset, pageSize
         );
-
-        // 计算每条记录的成功/失败状态
-        for (StockHistoryVO vo : list) {
-            if (vo.getNextDayHighTrend() != null) {
-                if (vo.getNextDayHighTrend().compareTo(new java.math.BigDecimal("2")) >= 0) {
-                    vo.setStatus("success");  // 涨幅 >= 2%,成功
-                } else if (vo.getNextDayHighTrend().compareTo(new java.math.BigDecimal("-3")) <= 0) {
-                    vo.setStatus("fail");  // 涨幅 <= -3%,失败
-                }
-                // 否则 status 保持为 null(无状态)
-            }
-        }
-
+        
         // 查询总数
-        int total = stockPoolMapper.countPoolHistory(startDate, endDate, poolType);
-
+        int total = stockPoolHistoryMapper.countHistory(startDate, endDate, poolType);
+        
         // 计算总页数
         int pages = (total + pageSize - 1) / pageSize;
-
+        
         // 构建返回结果
         Map<String, Object> result = new HashMap<>();
         result.put("list", list);
@@ -59,74 +48,132 @@ public class StockHistoryServiceImpl implements StockHistoryService {
         result.put("pageSize", pageSize);
         result.put("pages", pages);
         result.put("hasMore", pageNum < pages);
-
+        
         return result;
     }
     
     @Override
     public Map<String, Object> queryHistoryStats(LocalDate startDate, LocalDate endDate, Integer poolType) {
-        // 查询所有数据(不分页)
-        List<StockHistoryVO> allList = stockPoolMapper.selectPoolHistory(
-            startDate, endDate, poolType, 0, Integer.MAX_VALUE
-        );
-
-        // 计算每条记录的成功/失败状态并统计
-        int successCount = 0;
-        int failCount = 0;
-        java.math.BigDecimal totalTrend = java.math.BigDecimal.ZERO;
-        int trendCount = 0;
-
-        for (StockHistoryVO vo : allList) {
-            if (vo.getNextDayHighTrend() != null) {
-                // 累加涨幅用于计算平均值
-                totalTrend = totalTrend.add(vo.getNextDayHighTrend());
-                trendCount++;
-
-                if (vo.getNextDayHighTrend().compareTo(new java.math.BigDecimal("2")) >= 0) {
-                    successCount++;
-                } else if (vo.getNextDayHighTrend().compareTo(new java.math.BigDecimal("-3")) <= 0) {
-                    failCount++;
-                }
-            }
+        Map<String, Object> stats = stockPoolHistoryMapper.selectHistoryStats(startDate, endDate, poolType);
+        
+        Map<String, Object> result = new HashMap<>();
+        
+        if (stats == null || stats.get("totalCount") == null) {
+            result.put("totalCount", 0);
+            result.put("successCount", 0);
+            result.put("failCount", 0);
+            result.put("successRate", "0%");
+            result.put("avgTrend", "0%");
+            return result;
         }
-
-        int total = allList.size();
-        String successRate = total > 0 ? String.format("%.1f%%", (successCount * 100.0 / total)) : "0%";
-
-        // 计算平均收益率
-        String avgTrend = "0%";
-        if (trendCount > 0) {
-            java.math.BigDecimal avg = totalTrend.divide(
-                new java.math.BigDecimal(trendCount),
-                2,
-                java.math.RoundingMode.HALF_UP
-            );
-            avgTrend = String.format("%+.2f%%", avg);
+        
+        // 获取统计数据
+        long totalCount = ((Number) stats.get("totalCount")).longValue();
+        long successCount = stats.get("successCount") != null ? ((Number) stats.get("successCount")).longValue() : 0;
+        long failCount = stats.get("failCount") != null ? ((Number) stats.get("failCount")).longValue() : 0;
+        BigDecimal avgTrend = stats.get("avgTrend") != null ? new BigDecimal(stats.get("avgTrend").toString()) : BigDecimal.ZERO;
+        
+        // 计算成功率
+        String successRate = "0%";
+        if (totalCount > 0) {
+            BigDecimal rate = new BigDecimal(successCount * 100).divide(new BigDecimal(totalCount), 1, RoundingMode.HALF_UP);
+            successRate = rate.stripTrailingZeros().toPlainString() + "%";
         }
-
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalCount", total);
+        
+        // 格式化平均收益
+        String avgTrendStr = (avgTrend.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "") + 
+                            avgTrend.setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
+        
+        result.put("totalCount", totalCount);
         result.put("successCount", successCount);
         result.put("failCount", failCount);
         result.put("successRate", successRate);
-        result.put("avgTrend", avgTrend);
-
+        result.put("avgTrend", avgTrendStr);
+        
         return result;
     }
     
     @Override
     public Map<String, Object> queryLatestHistoryByKeyword(String keyword) {
         Map<String, Object> result = new HashMap<>();
-        result.put("found", false);
-        result.put("message", "该功能暂不支持");
+
+        if (keyword == null || keyword.trim().isEmpty()) {
+            result.put("found", false);
+            result.put("message", "请输入搜索关键词");
+            return result;
+        }
+
+        StockHistoryVO history = stockPoolHistoryMapper.selectLatestByKeyword(keyword.trim());
+
+        if (history == null) {
+            result.put("found", false);
+            result.put("message", "未找到该股票的历史数据");
+            return result;
+        }
+
+        result.put("found", true);
+        result.put("stockCode", history.getStockCode());
+        result.put("stockName", history.getStockName());
+        result.put("recordDate", history.getRecordDate());
+        result.put("closePrice", history.getClosePrice());
+        result.put("changePercent", history.getChangePercent());
+        result.put("strengthScore", history.getStrengthScore());
+        result.put("totalAmount", history.getTotalAmount());
+        result.put("circulationMarketValue", history.getCirculationMarketValue());
+        result.put("mainRisePeriod", history.getMainRisePeriod());
+        result.put("recentRiseHand", history.getRecentRiseHand());
+        result.put("recentLimitUp", history.getRecentLimitUp());
+        result.put("dayHighestPrice", history.getDayHighestPrice());
+        result.put("dayLowestPrice", history.getDayLowestPrice());
+        result.put("dayAvgPrice", history.getDayAvgPrice());
+        result.put("dayClosePrice", history.getDayClosePrice());
+        result.put("highTrend", history.getHighTrend());
+
         return result;
     }
 
     @Override
     public Map<String, Object> queryHistoryByKeywordAndDate(String keyword, LocalDate recordDate) {
         Map<String, Object> result = new HashMap<>();
-        result.put("found", false);
-        result.put("message", "该功能暂不支持");
+
+        if (keyword == null || keyword.trim().isEmpty()) {
+            result.put("found", false);
+            result.put("message", "请输入搜索关键词");
+            return result;
+        }
+
+        if (recordDate == null) {
+            result.put("found", false);
+            result.put("message", "请输入查询日期");
+            return result;
+        }
+
+        StockHistoryVO history = stockPoolHistoryMapper.selectByKeywordAndDate(keyword.trim(), recordDate);
+
+        if (history == null) {
+            result.put("found", false);
+            result.put("message", "未找到该股票在指定日期的历史数据");
+            return result;
+        }
+
+        result.put("found", true);
+        result.put("stockCode", history.getStockCode());
+        result.put("stockName", history.getStockName());
+        result.put("recordDate", history.getRecordDate());
+        result.put("closePrice", history.getClosePrice());
+        result.put("changePercent", history.getChangePercent());
+        result.put("strengthScore", history.getStrengthScore());
+        result.put("totalAmount", history.getTotalAmount());
+        result.put("circulationMarketValue", history.getCirculationMarketValue());
+        result.put("mainRisePeriod", history.getMainRisePeriod());
+        result.put("recentRiseHand", history.getRecentRiseHand());
+        result.put("recentLimitUp", history.getRecentLimitUp());
+        result.put("dayHighestPrice", history.getDayHighestPrice());
+        result.put("dayLowestPrice", history.getDayLowestPrice());
+        result.put("dayAvgPrice", history.getDayAvgPrice());
+        result.put("dayClosePrice", history.getDayClosePrice());
+        result.put("highTrend", history.getHighTrend());
+
         return result;
     }
 }

+ 16 - 46
src/main/java/com/yingpai/gupiao/service/impl/StockPoolServiceImpl.java

@@ -43,23 +43,12 @@ public class StockPoolServiceImpl implements StockPoolService {
     @Override
     public List<StockPoolVO> getPoolStocks(Integer poolType) {
         log.info("[获取股票池] poolType={}", poolType);
-
-        // 查询最新日期
-        LocalDate latestDate = stockPoolMapper.selectLatestAddDate(poolType);
-        if (latestDate == null) {
-            log.info("[获取股票池] 没有找到任何记录");
-            return new ArrayList<>();
-        }
-
-        log.info("[获取股票池] 最新日期: {}", latestDate);
-
-        // 根据最新日期查询股票池
+        
         LambdaQueryWrapper<StockPool> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StockPool::getPoolType, poolType)
-               .eq(StockPool::getAddDate, latestDate)
-               .eq(StockPool::getStatus, 2)  // 只查询当前有效的
-               .orderByAsc(StockPool::getStockCode);
-
+               .eq(StockPool::getStatus, 1)  // 只查询有效的
+               .orderByDesc(StockPool::getAddDate);
+        
         List<StockPool> stocks = stockPoolMapper.selectList(wrapper);
         log.info("[获取股票池] 查询到 {} 条记录", stocks.size());
         
@@ -167,16 +156,9 @@ public class StockPoolServiceImpl implements StockPoolService {
     
     @Override
     public List<StockPoolVO> getPoolList(Integer poolType) {
-        LocalDate today = LocalDate.now();
-
         LambdaQueryWrapper<StockPool> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StockPool::getPoolType, poolType)
-               .and(w -> w
-                   // 日期在当日之前的状态为1和2的记录
-                   .or(w1 -> w1.lt(StockPool::getAddDate, today).in(StockPool::getStatus, 1, 2))
-                   // 日期在当天的状态为2的记录
-                   .or(w2 -> w2.eq(StockPool::getAddDate, today).eq(StockPool::getStatus, 2))
-               )
+               .eq(StockPool::getStatus, 1)
                .orderByDesc(StockPool::getAddDate);
         List<StockPool> list = stockPoolMapper.selectList(wrapper);
         
@@ -221,7 +203,7 @@ public class StockPoolServiceImpl implements StockPoolService {
         LambdaQueryWrapper<StockPool> existWrapper = new LambdaQueryWrapper<>();
         existWrapper.eq(StockPool::getStockCode, stockCode)
                    .eq(StockPool::getPoolType, poolType)
-                   .eq(StockPool::getStatus, 2);  // 检查是否在当前池中
+                   .eq(StockPool::getStatus, 1);
         if (stockPoolMapper.selectCount(existWrapper) > 0) {
             log.warn("[添加股票到池] 股票已存在: {}", stockCode);
             throw new RuntimeException("该股票已在池中");
@@ -229,24 +211,19 @@ public class StockPoolServiceImpl implements StockPoolService {
         
         // 获取实时价格
         BigDecimal currentPrice = fetchCurrentPrice(stockCode);
-
-        // 从历史信息中查询当天的收盘价
-        LocalDate today = LocalDate.now();
-        BigDecimal closePrice = fetchClosePriceFromHistory(stockCode, today);
-
+        
         // 添加到池
         StockPool pool = new StockPool();
         pool.setStockCode(stockCode);
         pool.setStockName(stockInfo.getStockName());
         pool.setPoolType(poolType);
         pool.setAddPrice(currentPrice);
-        pool.setClosePrice(closePrice);  // 设置收盘价
-        pool.setAddDate(today);
-        pool.setStatus(2);  // 新增时设置为当前有效
+        pool.setAddDate(LocalDate.now());
+        pool.setStatus(1);
         pool.setAdminId(adminId);
         pool.setCreateTime(LocalDateTime.now());
         pool.setUpdateTime(LocalDateTime.now());
-
+        
         return stockPoolMapper.insert(pool) > 0;
     }
     
@@ -288,27 +265,20 @@ public class StockPoolServiceImpl implements StockPoolService {
     @Override
     public boolean deleteStock(String stockCode, Integer poolType) {
         log.info("[从池中删除股票] stockCode={}, poolType={}", stockCode, poolType);
-
+        
         LambdaQueryWrapper<StockPool> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StockPool::getStockCode, stockCode)
                .eq(StockPool::getPoolType, poolType)
-               .eq(StockPool::getStatus, 2);  // 只查询当前有效的
-
+               .eq(StockPool::getStatus, 1);
+        
         StockPool pool = stockPoolMapper.selectOne(wrapper);
         if (pool == null) {
             throw new RuntimeException("记录不存在");
         }
-
-        // 管理员删除:改为历史有效
-        pool.setStatus(1);
+        
+        // 软删除
+        pool.setStatus(0);
         pool.setUpdateTime(LocalDateTime.now());
         return stockPoolMapper.updateById(pool) > 0;
     }
-
-    /**
-     * 从历史信息中查询收盘价
-     */
-    private BigDecimal fetchClosePriceFromHistory(String stockCode, LocalDate recordDate) {
-        return stockPoolMapper.selectClosePriceFromHistory(stockCode, recordDate);
-    }
 }

+ 1 - 66
src/main/java/com/yingpai/gupiao/service/impl/UserStockServiceImpl.java

@@ -2,8 +2,6 @@ package com.yingpai.gupiao.service.impl;
 
 import cn.hutool.http.HttpUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yingpai.gupiao.domain.dto.AddUserStockDTO;
 import com.yingpai.gupiao.domain.po.UserStock;
 import com.yingpai.gupiao.domain.vo.UserStockVO;
@@ -36,9 +34,6 @@ public class UserStockServiceImpl implements UserStockService {
 
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final String BASE_URL = "http://qt.gtimg.cn/q=";
-    private static final String TREND_URL = "https://web.ifzq.gtimg.cn/appstock/app/minute/query";
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
 
     @Override
     public List<UserStockVO> getUserStocks(Long userId) {
@@ -90,10 +85,6 @@ public class UserStockServiceImpl implements UserStockService {
                 }
             }
 
-            // 获取分时数据
-            List<BigDecimal> trendData = fetchTrendData(stock.getStockCode());
-            builder.trendData(trendData);
-
             result.add(builder.build());
         }
 
@@ -239,63 +230,7 @@ public class UserStockServiceImpl implements UserStockService {
         wrapper.eq(UserStock::getUserId, userId)
                .eq(UserStock::getStockCode, stockCode)
                .eq(UserStock::getPoolType, poolType);
-
+        
         return userStockMapper.selectCount(wrapper) > 0;
     }
-
-    /**
-     * 获取股票分时数据
-     */
-    private List<BigDecimal> fetchTrendData(String stockCode) {
-        List<BigDecimal> trendData = new ArrayList<>();
-
-        try {
-            String marketPrefix = StockUtils.getMarketPrefix(stockCode);
-            if (marketPrefix == null) {
-                return trendData;
-            }
-
-            String fullUrl = TREND_URL + "?code=" + marketPrefix + stockCode;
-
-            String body = HttpUtil.get(fullUrl);
-
-            if (body != null && !body.isEmpty()) {
-                JsonNode root = objectMapper.readTree(body);
-                JsonNode data = root.path("data").path(marketPrefix + stockCode).path("data").path("data");
-
-                if (data != null && data.isArray()) {
-                    List<BigDecimal> allPrices = new ArrayList<>();
-                    for (JsonNode item : data) {
-                        String itemStr = item.asText();
-                        String[] parts = itemStr.split(" ");
-                        if (parts.length >= 2) {
-                            try {
-                                BigDecimal price = new BigDecimal(parts[1]);
-                                allPrices.add(price);
-                            } catch (NumberFormatException e) {
-                                log.debug("[趋势数据] 解析价格失败: {}", parts[1]);
-                            }
-                        }
-                    }
-
-                    int targetPoints = 30;
-                    if (allPrices.size() <= targetPoints) {
-                        trendData = allPrices;
-                    } else {
-                        double step = (double) (allPrices.size() - 1) / (targetPoints - 1);
-                        for (int i = 0; i < targetPoints; i++) {
-                            int index = (int) Math.round(i * step);
-                            if (index < allPrices.size()) {
-                                trendData.add(allPrices.get(index));
-                            }
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.error("[趋势数据] 获取失败: {}", e.getMessage());
-        }
-
-        return trendData;
-    }
 }

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

@@ -168,10 +168,9 @@ public class WxPayServiceImpl implements WxPayService {
         PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
         
         log.info("预支付成功,orderNo: {}", orderNo);
-
+        
         return WxPayVO.builder()
                 .orderNo(orderNo)
-                .appId(wxConfig.getAppid())
                 .timeStamp(response.getTimeStamp())
                 .nonceStr(response.getNonceStr())
                 .packageValue(response.getPackageVal())

+ 11 - 2
src/main/resources/application.yml

@@ -3,8 +3,17 @@ server:
 
 # 微信配置
 wx:
-  appid: wx3a2e1ce6a8cf342d
-  secret: cdd169a5575237cee52725529411e6a3
+  # 小程序配置
+  miniapp:
+    appid: wx9d7e6e3592830447
+    secret: f79f4bf879df37aff9e12fc81f3ff901
+  # H5公众号配置
+  h5:
+    appid: wx3a2e1ce6a8cf342d
+    secret: cdd169a5575237cee52725529411e6a3
+  # 默认使用小程序配置(向后兼容)
+  appid: wx9d7e6e3592830447
+  secret: f79f4bf879df37aff9e12fc81f3ff901
 
 # 文件存储配置
 file: