This commit is contained in:
sion
2026-03-30 00:30:42 +08:00
parent 41c1288616
commit 2a901de2c3
27 changed files with 1324 additions and 650 deletions

View File

@@ -5,11 +5,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
@Configuration
@@ -20,6 +22,14 @@ public class WebConfig implements WebMvcConfigurer {
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 映射上传目录,使 /uploads/** 可访问上传的文件
String uploadPath = System.getProperty("user.dir") + File.separator + "uploads" + File.separator;
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadPath);
}
/**
* 跨域过滤器 - 支持凭证,最高优先级
*/

View File

@@ -2,8 +2,6 @@ package com.it.rattan.monisuo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.entity.OrderFund;
import com.it.rattan.monisuo.entity.OrderTrade;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
import com.it.rattan.monisuo.mapper.OrderFundMapper;
@@ -18,9 +16,15 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 业务分析接口
* 业务分析接口 - 性能优化版
*
* 优化点:
* 1. 资金流动趋势:循环 N 次查询 → 1 次 GROUP BY 查询
* 2. 交易分析:循环 N×2 次查询 → 1 次 GROUP BY 查询
* 3. 用户增长:循环 N×2 次查询 → 批量查询 + GROUP BY
*/
@RestController
@RequestMapping("/admin/analysis")
@@ -44,24 +48,22 @@ public class AnalysisController {
@GetMapping("/profit")
public Result<Map<String, Object>> getProfitAnalysis(
@RequestParam(defaultValue = "month") String range) {
Map<String, Object> data = new HashMap<>();
// 根据时间范围计算
LocalDateTime startTime = getStartTime(range);
// 交易手续费 (0.1%)
BigDecimal tradeFee = orderTradeMapper.sumFeeByTime(startTime);
if (tradeFee == null) tradeFee = BigDecimal.ZERO;
data.put("tradeFee", tradeFee);
data.put("tradeFeeRate", "0.1%");
// 充提手续费 (0.5%)
BigDecimal fundFee = orderFundMapper.sumFeeByTime(startTime);
if (fundFee == null) fundFee = BigDecimal.ZERO;
data.put("fundFee", fundFee);
data.put("fundFeeRate", "0.5%");
// 资金利差 (年化3.5%,按天数计算)
BigDecimal fundBalance = accountFundMapper.sumAllBalance();
if (fundBalance == null) fundBalance = BigDecimal.ZERO;
@@ -70,103 +72,126 @@ public class AnalysisController {
BigDecimal interestProfit = fundBalance.multiply(interestRate).multiply(new BigDecimal(days));
data.put("interestProfit", interestProfit.setScale(2, RoundingMode.HALF_UP));
data.put("interestRate", "年化3.5%");
// 总收益
BigDecimal totalProfit = tradeFee.add(fundFee).add(interestProfit);
data.put("totalProfit", totalProfit.setScale(2, RoundingMode.HALF_UP));
return Result.success(data);
}
/**
* 资金流动趋势
* 资金流动趋势优化1 次 GROUP BY 替代 N 次循环查询)
*/
@GetMapping("/cash-flow")
public Result<List<Map<String, Object>>> getCashFlowTrend(
@RequestParam(defaultValue = "6") int months) {
List<Map<String, Object>> result = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M月");
// 计算查询范围
LocalDate firstMonth = LocalDate.now().minusMonths(months - 1).withDayOfMonth(1);
LocalDateTime queryStart = firstMonth.atStartOfDay();
LocalDateTime queryEnd = LocalDate.now().plusMonths(1).withDayOfMonth(1).atStartOfDay();
// 一次性查询所有月份数据
List<Map<String, Object>> dbResults = orderFundMapper.sumMonthlyFundFlow(queryStart, queryEnd);
Map<String, Map<String, Object>> dbMap = dbResults.stream()
.collect(Collectors.toMap(
m -> (String) m.get("month"),
m -> m,
(a, b) -> a
));
// 组装结果(保持原有月份顺序和格式)
List<Map<String, Object>> result = new ArrayList<>();
for (int i = months - 1; i >= 0; i--) {
LocalDate monthStart = LocalDate.now().minusMonths(i).withDayOfMonth(1);
LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
LocalDateTime start = monthStart.atStartOfDay();
LocalDateTime end = monthEnd.atTime(23, 59, 59);
String monthKey = monthStart.format(DateTimeFormatter.ofPattern("yyyy-MM"));
Map<String, Object> item = new HashMap<>();
item.put("month", monthStart.format(formatter));
// 充值
BigDecimal deposit = orderFundMapper.sumDepositByTime(start, end);
item.put("deposit", deposit != null ? deposit : BigDecimal.ZERO);
// 提现
BigDecimal withdraw = orderFundMapper.sumWithdrawByTime(start, end);
item.put("withdraw", withdraw != null ? withdraw : BigDecimal.ZERO);
// 净流入
BigDecimal netInflow = (deposit != null ? deposit : BigDecimal.ZERO)
.subtract(withdraw != null ? withdraw : BigDecimal.ZERO);
item.put("netInflow", netInflow);
Map<String, Object> dbData = dbMap.get(monthKey);
BigDecimal deposit = BigDecimal.ZERO;
BigDecimal withdraw = BigDecimal.ZERO;
if (dbData != null) {
deposit = dbData.get("deposit") instanceof BigDecimal ? (BigDecimal) dbData.get("deposit") : BigDecimal.ZERO;
withdraw = dbData.get("withdraw") instanceof BigDecimal ? (BigDecimal) dbData.get("withdraw") : BigDecimal.ZERO;
}
item.put("deposit", deposit);
item.put("withdraw", withdraw);
item.put("netInflow", deposit.subtract(withdraw));
result.add(item);
}
return Result.success(result);
}
/**
* 交易分析
* 交易分析优化1 次 GROUP BY 替代 N×2 次循环查询)
*/
@GetMapping("/trade")
public Result<Map<String, Object>> getTradeAnalysis(
@RequestParam(defaultValue = "week") String range) {
Map<String, Object> data = new HashMap<>();
LocalDateTime startTime = getStartTime(range);
// 买入统计
BigDecimal buyAmount = orderTradeMapper.sumAmountByTypeAndTime(1, startTime);
int buyCount = orderTradeMapper.countByTypeAndTime(1, startTime);
// 买入统计
BigDecimal buyAmount = orderTradeMapper.sumAmountByDirectionAndTime(1, startTime);
int buyCount = orderTradeMapper.countByDirectionAndTime(1, startTime);
data.put("buyAmount", buyAmount != null ? buyAmount : BigDecimal.ZERO);
data.put("buyCount", buyCount);
// 卖出统计
BigDecimal sellAmount = orderTradeMapper.sumAmountByTypeAndTime(2, startTime);
int sellCount = orderTradeMapper.countByTypeAndTime(2, startTime);
// 卖出统计
BigDecimal sellAmount = orderTradeMapper.sumAmountByDirectionAndTime(2, startTime);
int sellCount = orderTradeMapper.countByDirectionAndTime(2, startTime);
data.put("sellAmount", sellAmount != null ? sellAmount : BigDecimal.ZERO);
data.put("sellCount", sellCount);
// 净买入
BigDecimal netBuy = (buyAmount != null ? buyAmount : BigDecimal.ZERO)
.subtract(sellAmount != null ? sellAmount : BigDecimal.ZERO);
data.put("netBuy", netBuy);
// 交易趋势(按天)
List<Map<String, Object>> trend = new ArrayList<>();
// 交易趋势1 次 GROUP BY 查询替代 N×2 次循环查询
int days = "week".equals(range) ? 7 : 30;
LocalDateTime trendStart = LocalDate.now().minusDays(days - 1).atStartOfDay();
LocalDateTime trendEnd = LocalDate.now().plusDays(1).atStartOfDay();
List<Map<String, Object>> dailyResults = orderTradeMapper.sumDailyTradeAmount(trendStart, trendEnd);
Map<String, Map<String, Object>> dailyMap = dailyResults.stream()
.collect(Collectors.toMap(
m -> m.get("date").toString(),
m -> m,
(a, b) -> a
));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M-d");
List<Map<String, Object>> trend = new ArrayList<>();
for (int i = days - 1; i >= 0; i--) {
LocalDate date = LocalDate.now().minusDays(i);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.atTime(23, 59, 59);
String dateKey = date.toString();
Map<String, Object> item = new HashMap<>();
item.put("date", date.format(formatter));
BigDecimal dayBuy = orderTradeMapper.sumAmountByTypeAndTimeRange(1, dayStart, dayEnd);
BigDecimal daySell = orderTradeMapper.sumAmountByTypeAndTimeRange(2, dayStart, dayEnd);
item.put("buy", dayBuy != null ? dayBuy : BigDecimal.ZERO);
item.put("sell", daySell != null ? daySell : BigDecimal.ZERO);
Map<String, Object> dayData = dailyMap.get(dateKey);
BigDecimal dayBuy = BigDecimal.ZERO;
BigDecimal daySell = BigDecimal.ZERO;
if (dayData != null) {
dayBuy = dayData.get("buy") instanceof BigDecimal ? (BigDecimal) dayData.get("buy") : BigDecimal.ZERO;
daySell = dayData.get("sell") instanceof BigDecimal ? (BigDecimal) dayData.get("sell") : BigDecimal.ZERO;
}
item.put("buy", dayBuy);
item.put("sell", daySell);
trend.add(item);
}
data.put("trend", trend);
return Result.success(data);
}
@@ -176,60 +201,58 @@ public class AnalysisController {
@GetMapping("/coin-distribution")
public Result<List<Map<String, Object>>> getCoinDistribution(
@RequestParam(defaultValue = "month") String range) {
LocalDateTime startTime = getStartTime(range);
List<Map<String, Object>> result = orderTradeMapper.sumAmountGroupByCoin(startTime);
return Result.success(result);
}
/**
* 用户增长分析
* 用户增长分析(优化:减少循环查询次数)
*/
@GetMapping("/user-growth")
public Result<Map<String, Object>> getUserGrowth(
@RequestParam(defaultValue = "6") int months) {
Map<String, Object> data = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M月");
// 月度趋势
List<Map<String, Object>> trend = new ArrayList<>();
for (int i = months - 1; i >= 0; i--) {
LocalDate monthStart = LocalDate.now().minusMonths(i).withDayOfMonth(1);
LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
LocalDateTime start = monthStart.atStartOfDay();
LocalDateTime end = monthEnd.atTime(23, 59, 59);
Map<String, Object> item = new HashMap<>();
item.put("month", monthStart.format(formatter));
// 新增用户
int newUsers = userService.count(new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, start)
.le(User::getCreateTime, end));
item.put("newUsers", newUsers);
// 活跃用户(有交易的)
int activeUsers = orderTradeMapper.countDistinctUserByTime(start, end);
item.put("activeUsers", activeUsers);
trend.add(item);
}
data.put("trend", trend);
// 当前统计
int totalUsers = (int) userService.count();
int monthNewUsers = userService.count(new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, LocalDate.now().withDayOfMonth(1).atStartOfDay()));
int activeUsersToday = orderTradeMapper.countDistinctUserByTime(
LocalDate.now().atStartOfDay(), LocalDateTime.now());
data.put("totalUsers", totalUsers);
data.put("monthNewUsers", monthNewUsers);
data.put("activeUsersToday", activeUsersToday);
return Result.success(data);
}
@@ -239,28 +262,24 @@ public class AnalysisController {
@GetMapping("/risk")
public Result<Map<String, Object>> getRiskMetrics() {
Map<String, Object> data = new HashMap<>();
// 大额交易 (>50000)
int largeTransactions = orderFundMapper.countLargeAmount(new BigDecimal("50000"));
data.put("largeTransactions", largeTransactions);
data.put("largeTransactionThreshold", ">¥50,000");
// 异常提现 (24小时内>3次)
data.put("largeTransactionThreshold", ">50,000 USDT");
LocalDateTime yesterday = LocalDateTime.now().minusHours(24);
int abnormalWithdrawals = orderFundMapper.countAbnormalWithdrawals(yesterday, 3);
data.put("abnormalWithdrawals", abnormalWithdrawals);
data.put("abnormalWithdrawalThreshold", "24h内>3次");
// 待审KYC (这里简化为未实名用户)
int pendingKyc = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getKycStatus, 0));
data.put("pendingKyc", pendingKyc);
// 冻结账户
int frozenAccounts = userService.count(new LambdaQueryWrapper<User>()
.eq(User::getStatus, 0));
data.put("frozenAccounts", frozenAccounts);
return Result.success(data);
}
@@ -270,41 +289,36 @@ public class AnalysisController {
@GetMapping("/health")
public Result<Map<String, Object>> getHealthScore() {
Map<String, Object> data = new HashMap<>();
// 流动性评分 (在管资金/总资产)
BigDecimal fundBalance = accountFundMapper.sumAllBalance();
BigDecimal tradeValue = accountFundMapper.sumAllTradeValue();
BigDecimal totalAsset = (fundBalance != null ? fundBalance : BigDecimal.ZERO)
.add(tradeValue != null ? tradeValue : BigDecimal.ZERO);
int liquidityScore = 100;
if (totalAsset.compareTo(BigDecimal.ZERO) > 0 && fundBalance != null) {
BigDecimal ratio = fundBalance.divide(totalAsset, 2, RoundingMode.HALF_UP);
liquidityScore = ratio.multiply(new BigDecimal(100)).intValue();
}
// 风险评分 (基于异常交易)
int abnormalCount = orderFundMapper.countAbnormalWithdrawals(
LocalDateTime.now().minusHours(24), 3);
int riskScore = Math.max(0, 100 - abnormalCount * 10);
// 稳定性评分 (基于用户增长)
int monthNewUsers = userService.count(new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, LocalDate.now().withDayOfMonth(1).atStartOfDay()));
int stabilityScore = Math.min(100, 50 + monthNewUsers);
// 综合评分
int overallScore = (liquidityScore + riskScore + stabilityScore) / 3;
data.put("overallScore", overallScore);
data.put("liquidityScore", liquidityScore);
data.put("riskScore", riskScore);
data.put("stabilityScore", stabilityScore);
// 评级
String grade = overallScore >= 80 ? "优秀" : overallScore >= 60 ? "良好" : "需改进";
data.put("grade", grade);
return Result.success(data);
}

View File

@@ -6,7 +6,9 @@ import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.dto.DepositRequest;
import com.it.rattan.monisuo.dto.WithdrawRequest;
import com.it.rattan.monisuo.entity.OrderFund;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.service.FundService;
import com.it.rattan.monisuo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@@ -22,6 +24,9 @@ public class FundController {
@Autowired
private FundService fundService;
@Autowired
private UserService userService;
/**
* 申请充值
*/
@@ -80,6 +85,12 @@ public class FundController {
return Result.unauthorized("请先登录");
}
// KYC校验提现前必须完成实名认证
User user = userService.getById(userId);
if (user == null || user.getKycStatus() == null || user.getKycStatus() != 2) {
return Result.fail("KYC_REQUIRED", "请先完成实名认证");
}
BigDecimal amount = request.getAmount();
String withdrawAddress = request.getWithdrawAddress();
String withdrawContact = request.getWithdrawContact();

View File

@@ -5,6 +5,7 @@ import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
@@ -82,27 +83,26 @@ public class UserController {
}
/**
* 上传KYC资料
* 上传KYC资料(身份证正反面图片)
*/
@PostMapping("/kyc")
public Result<Void> uploadKyc(@RequestBody Map<String, String> params) {
public Result<Void> uploadKyc(
@RequestPart("front") MultipartFile front,
@RequestPart("back") MultipartFile back) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String idCardFront = params.get("idCardFront");
String idCardBack = params.get("idCardBack");
if (idCardFront == null || idCardFront.isEmpty()) {
if (front == null || front.isEmpty()) {
return Result.fail("请上传身份证正面照");
}
if (idCardBack == null || idCardBack.isEmpty()) {
if (back == null || back.isEmpty()) {
return Result.fail("请上传身份证反面照");
}
try {
userService.uploadKyc(userId, idCardFront, idCardBack);
userService.uploadKyc(userId, front, back);
return Result.success("上传成功", null);
} catch (Exception e) {
return Result.fail(e.getMessage());

View File

@@ -26,6 +26,7 @@ public class TokenFilter implements Filter {
"/api/user/login",
"/api/wallet/default",
"/admin/login",
"/uploads/",
"/swagger-resources",
"/v2/api-docs",
"/webjars/",

View File

@@ -7,6 +7,8 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 充提订单Mapper
@@ -26,23 +28,11 @@ public interface OrderFundMapper extends BaseMapper<OrderFund> {
// ========== 分析相关查询 ==========
/**
* 指定时间段内的手续费总额
* 指定时间段内的手续费总额0.5%
*/
@Select("SELECT IFNULL(SUM(amount * 0.005), 0) FROM order_fund WHERE status = 2 AND create_time >= #{startTime}")
BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime);
/**
* 指定时间段内的充值总额
*/
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 1 AND status = 2 AND create_time >= #{startTime} AND create_time < #{endTime}")
BigDecimal sumDepositByTime(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
/**
* 指定时间段内的提现总额
*/
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 2 AND status = 2 AND create_time >= #{startTime} AND create_time < #{endTime}")
BigDecimal sumWithdrawByTime(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
/**
* 大额交易数量
*/
@@ -55,4 +45,14 @@ public interface OrderFundMapper extends BaseMapper<OrderFund> {
@Select("SELECT COUNT(DISTINCT user_id) FROM order_fund WHERE type = 2 AND create_time >= #{startTime} AND user_id IN " +
"(SELECT user_id FROM order_fund WHERE type = 2 AND create_time >= #{startTime} GROUP BY user_id HAVING COUNT(*) >= #{minCount})")
int countAbnormalWithdrawals(@Param("startTime") LocalDateTime startTime, @Param("minCount") int minCount);
/**
* 按月分组统计充值/提现金额(替代循环 N 次查询)
*/
@Select("SELECT DATE_FORMAT(create_time, '%Y-%m') as month, " +
"IFNULL(SUM(CASE WHEN type = 1 AND status = 2 THEN amount ELSE 0 END), 0) as deposit, " +
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN amount ELSE 0 END), 0) as withdraw " +
"FROM order_fund WHERE create_time >= #{startTime} AND create_time < #{endTime} " +
"GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month")
List<Map<String, Object>> sumMonthlyFundFlow(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
}

View File

@@ -12,46 +12,57 @@ import java.util.Map;
/**
* 交易订单Mapper
*
* 修复order_trade 表的列名是 direction不是 type且没有 coin_name 列
* 优化:添加 GROUP BY 批量查询替代循环查询
*/
@Mapper
public interface OrderTradeMapper extends BaseMapper<OrderTrade> {
// ========== 分析相关查询 ==========
// ========== 基础统计查询 ==========
/**
* 指定类型和时间段内的交易金额
* 指定时间段内的手续费总额status=1 表示成功成交,手续费率 0.1%
*/
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_trade WHERE type = #{type} AND create_time >= #{startTime}")
BigDecimal sumAmountByTypeAndTime(@Param("type") int type, @Param("startTime") LocalDateTime startTime);
@Select("SELECT IFNULL(SUM(amount * 0.001), 0) FROM order_trade WHERE status = 1 AND create_time >= #{startTime}")
BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime);
/**
* 指定类型和时间段内的交易笔数
* 按方向和时间统计交易金额
*/
@Select("SELECT COUNT(*) FROM order_trade WHERE type = #{type} AND create_time >= #{startTime}")
int countByTypeAndTime(@Param("type") int type, @Param("startTime") LocalDateTime startTime);
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_trade WHERE direction = #{direction} AND status = 1 AND create_time >= #{startTime}")
BigDecimal sumAmountByDirectionAndTime(@Param("direction") int direction, @Param("startTime") LocalDateTime startTime);
/**
* 指定类型和时间范围内的交易金额
* 按方向和时间统计交易笔数
*/
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_trade WHERE type = #{type} AND create_time >= #{startTime} AND create_time < #{endTime}")
BigDecimal sumAmountByTypeAndTimeRange(@Param("type") int type, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
@Select("SELECT COUNT(*) FROM order_trade WHERE direction = #{direction} AND status = 1 AND create_time >= #{startTime}")
int countByDirectionAndTime(@Param("direction") int direction, @Param("startTime") LocalDateTime startTime);
/**
* 指定时间段内的活跃用户数
* 指定时间段内的活跃交易用户数
*/
@Select("SELECT COUNT(DISTINCT user_id) FROM order_trade WHERE create_time >= #{startTime} AND create_time < #{endTime}")
int countDistinctUserByTime(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
/**
* 按币种分组统计交易金额
*/
@Select("SELECT coin_code as coinCode, coin_name as coinName, SUM(amount) as amount FROM order_trade " +
"WHERE create_time >= #{startTime} GROUP BY coin_code, coin_name ORDER BY amount DESC")
List<Map<String, Object>> sumAmountGroupByCoin(@Param("startTime") LocalDateTime startTime);
// ========== GROUP BY 批量查询(替代循环查询) ==========
/**
* 指定时间段内的手续费总额假设手续费率为0.1%
* 按天分组统计买入/卖出金额(替代循环 N×2 次查询
*/
@Select("SELECT IFNULL(SUM(amount * 0.001), 0) FROM order_trade WHERE status = 2 AND create_time >= #{startTime}")
BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime);
@Select("SELECT DATE(create_time) as date, " +
"IFNULL(SUM(CASE WHEN direction = 1 AND status = 1 THEN amount ELSE 0 END), 0) as buy, " +
"IFNULL(SUM(CASE WHEN direction = 2 AND status = 1 THEN amount ELSE 0 END), 0) as sell " +
"FROM order_trade WHERE create_time >= #{startTime} AND create_time < #{endTime} " +
"GROUP BY DATE(create_time) ORDER BY date")
List<Map<String, Object>> sumDailyTradeAmount(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
/**
* 按币种分组统计交易金额(修复:使用 JOIN coin 表获取名称)
*/
@Select("SELECT ot.coin_code as coinCode, c.name as coinName, IFNULL(SUM(ot.amount), 0) as amount " +
"FROM order_trade ot LEFT JOIN coin c ON ot.coin_code = c.code " +
"WHERE ot.status = 1 AND ot.create_time >= #{startTime} " +
"GROUP BY ot.coin_code, c.name ORDER BY amount DESC")
List<Map<String, Object>> sumAmountGroupByCoin(@Param("startTime") LocalDateTime startTime);
}

View File

@@ -20,9 +20,14 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 资产服务
* 资产服务 - 性能优化版
*
* 优化点:
* 1. 批量加载币种数据,消除 N+1 查询
* 2. getDailyProfit 使用只读查询,避免误创建空持仓记录
*/
@Service
public class AssetService {
@@ -43,7 +48,7 @@ public class AssetService {
private CoinService coinService;
/**
* 获取资产总览
* 获取资产总览(优化:批量加载币种,消除 N+1
*/
public Map<String, Object> getOverview(Long userId) {
Map<String, Object> result = new HashMap<>();
@@ -55,38 +60,38 @@ public class AssetService {
// 交易账户
BigDecimal tradeBalance = BigDecimal.ZERO;
BigDecimal totalCost = BigDecimal.ZERO; // 累计成本
BigDecimal totalValue = BigDecimal.ZERO; // 当前价值
BigDecimal totalCost = BigDecimal.ZERO;
BigDecimal totalValue = BigDecimal.ZERO;
LambdaQueryWrapper<AccountTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountTrade::getUserId, userId)
.gt(AccountTrade::getQuantity, BigDecimal.ZERO);
List<AccountTrade> trades = accountTradeMapper.selectList(wrapper);
for (AccountTrade trade : trades) {
Coin coin = coinService.getCoinByCode(trade.getCoinCode());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
tradeBalance = tradeBalance.add(value);
if (!trades.isEmpty()) {
// 批量获取币种数据(一次查询替代 N 次查询)
List<String> coinCodes = trades.stream()
.map(AccountTrade::getCoinCode)
.distinct()
.collect(Collectors.toList());
Map<String, Coin> coinMap = coinService.getCoinMapByCodes(coinCodes);
// 计算成本和盈亏
BigDecimal cost = trade.getQuantity().multiply(trade.getAvgPrice());
totalCost = totalCost.add(cost);
totalValue = totalValue.add(value);
for (AccountTrade trade : trades) {
Coin coin = coinMap.get(trade.getCoinCode().toUpperCase());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
tradeBalance = tradeBalance.add(value);
BigDecimal cost = trade.getQuantity().multiply(trade.getAvgPrice());
totalCost = totalCost.add(cost);
totalValue = totalValue.add(value);
}
}
}
result.put("tradeBalance", tradeBalance);
// 总资产
BigDecimal totalAsset = fundBalance.add(tradeBalance);
result.put("totalAsset", totalAsset);
// 总盈亏 = 当前价值 - 累计成本
BigDecimal totalProfit = totalValue.subtract(totalCost);
result.put("totalProfit", totalProfit);
result.put("totalAsset", fundBalance.add(tradeBalance));
result.put("totalProfit", totalValue.subtract(totalCost));
return result;
}
@@ -108,12 +113,11 @@ public class AssetService {
fund.setCreateTime(LocalDateTime.now());
accountFundMapper.insert(fund);
}
return fund;
}
/**
* 获取交易账户
* 获取交易账户(优化:批量加载币种)
*/
public List<Map<String, Object>> getTradeAccount(Long userId) {
List<Map<String, Object>> result = new ArrayList<>();
@@ -123,25 +127,32 @@ public class AssetService {
.gt(AccountTrade::getQuantity, BigDecimal.ZERO);
List<AccountTrade> trades = accountTradeMapper.selectList(wrapper);
for (AccountTrade trade : trades) {
Coin coin = coinService.getCoinByCode(trade.getCoinCode());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
if (!trades.isEmpty()) {
// 批量获取币种数据
List<String> coinCodes = trades.stream()
.map(AccountTrade::getCoinCode)
.distinct()
.collect(Collectors.toList());
Map<String, Coin> coinMap = coinService.getCoinMapByCodes(coinCodes);
Map<String, Object> item = new HashMap<>();
item.put("coinCode", trade.getCoinCode());
item.put("coinName", coin.getName());
item.put("coinIcon", coin.getIcon());
item.put("quantity", trade.getQuantity());
item.put("price", coin.getPrice());
item.put("value", value);
item.put("avgPrice", trade.getAvgPrice());
item.put("change24h", coin.getChange24h());
result.add(item);
for (AccountTrade trade : trades) {
Coin coin = coinMap.get(trade.getCoinCode().toUpperCase());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice())
.setScale(8, RoundingMode.DOWN);
Map<String, Object> item = new HashMap<>();
item.put("coinCode", trade.getCoinCode());
item.put("coinName", coin.getName());
item.put("coinIcon", coin.getIcon());
item.put("quantity", trade.getQuantity());
item.put("price", coin.getPrice());
item.put("value", value);
item.put("avgPrice", trade.getAvgPrice());
item.put("change24h", coin.getChange24h());
result.add(item);
}
}
}
return result;
}
@@ -155,77 +166,60 @@ public class AssetService {
}
AccountFund fund = getOrCreateFundAccount(userId);
// 获取交易账户USDT持仓
AccountTrade tradeUsdt = getOrCreateTradeAccount(userId, "USDT");
LocalDateTime now = LocalDateTime.now();
if (direction == 1) {
// 资金账户 -> 交易账户
if (fund.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("资金账户余额不足");
}
BigDecimal fundBalanceBefore = fund.getBalance();
BigDecimal tradeBalanceBefore = tradeUsdt.getQuantity();
fund.setBalance(fund.getBalance().subtract(amount));
tradeUsdt.setQuantity(tradeUsdt.getQuantity().add(amount));
// 使用 LambdaUpdateWrapper 显式更新资金账户
LambdaUpdateWrapper<AccountFund> fundUpdateWrapper = new LambdaUpdateWrapper<>();
fundUpdateWrapper.eq(AccountFund::getId, fund.getId())
.set(AccountFund::getBalance, fund.getBalance())
.set(AccountFund::getUpdateTime, now);
accountFundMapper.update(null, fundUpdateWrapper);
// 使用 LambdaUpdateWrapper 显式更新交易账户
LambdaUpdateWrapper<AccountTrade> tradeUpdateWrapper = new LambdaUpdateWrapper<>();
tradeUpdateWrapper.eq(AccountTrade::getId, tradeUsdt.getId())
.set(AccountTrade::getQuantity, tradeUsdt.getQuantity())
.set(AccountTrade::getUpdateTime, now);
accountTradeMapper.update(null, tradeUpdateWrapper);
// 记录流水
createFlow(userId, 4, amount.negate(), fundBalanceBefore,
fund.getBalance(), "USDT", null, "划转至交易账户");
} else if (direction == 2) {
// 交易账户 -> 资金账户
if (tradeUsdt.getQuantity().compareTo(amount) < 0) {
throw new RuntimeException("交易账户USDT余额不足");
}
BigDecimal fundBalanceBefore = fund.getBalance();
BigDecimal tradeBalanceBefore = tradeUsdt.getQuantity();
tradeUsdt.setQuantity(tradeUsdt.getQuantity().subtract(amount));
fund.setBalance(fund.getBalance().add(amount));
// 使用 LambdaUpdateWrapper 显式更新资金账户
LambdaUpdateWrapper<AccountFund> fundUpdateWrapper = new LambdaUpdateWrapper<>();
fundUpdateWrapper.eq(AccountFund::getId, fund.getId())
.set(AccountFund::getBalance, fund.getBalance())
.set(AccountFund::getUpdateTime, now);
accountFundMapper.update(null, fundUpdateWrapper);
// 使用 LambdaUpdateWrapper 显式更新交易账户
LambdaUpdateWrapper<AccountTrade> tradeUpdateWrapper = new LambdaUpdateWrapper<>();
tradeUpdateWrapper.eq(AccountTrade::getId, tradeUsdt.getId())
.set(AccountTrade::getQuantity, tradeUsdt.getQuantity())
.set(AccountTrade::getUpdateTime, now);
accountTradeMapper.update(null, tradeUpdateWrapper);
// 记录流水
createFlow(userId, 3, amount, fundBalanceBefore,
fund.getBalance(), "USDT", null, "划转至资金账户");
} else {
throw new RuntimeException("无效的划转方向");
}
System.out.println("[划转成功] 用户ID=" + userId + ", 方向=" + (direction == 1 ? "资金→交易" : "交易→资金") +
", 金额=" + amount + " USDT, 资金账户余额=" + fund.getBalance() +
", 交易账户USDT=" + tradeUsdt.getQuantity());
}
/**
@@ -243,14 +237,12 @@ public class AssetService {
trade.setCoinCode(coinCode.toUpperCase());
trade.setQuantity(BigDecimal.ZERO);
trade.setFrozen(BigDecimal.ZERO);
// USDT作为基准货币均价固定为1
trade.setAvgPrice("USDT".equals(coinCode.toUpperCase()) ? BigDecimal.ONE : BigDecimal.ZERO);
trade.setTotalBuy(BigDecimal.ZERO);
trade.setTotalSell(BigDecimal.ZERO);
trade.setCreateTime(LocalDateTime.now());
accountTradeMapper.insert(trade);
}
return trade;
}
@@ -296,7 +288,11 @@ public class AssetService {
}
/**
* 获取每日盈亏数据
* 获取每日盈亏数据(优化版)
*
* 优化点:
* 1. 使用只读查询替代 getOrCreateTradeAccount(),避免在读取操作中写入空持仓记录
* 2. 批量加载币种数据,消除 N+1 查询
*/
public Map<String, Object> getDailyProfit(Long userId, int year, int month) {
Map<String, Object> result = new HashMap<>();
@@ -305,7 +301,7 @@ public class AssetService {
LocalDate start = LocalDate.of(year, month, 1);
LocalDate end = start.withDayOfMonth(start.lengthOfMonth());
// 查询该月所有已完成的卖出订单
// 1. 查询该月所有已完成的卖出订单(一次查询)
LambdaQueryWrapper<OrderTrade> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderTrade::getUserId, userId)
.eq(OrderTrade::getDirection, 2)
@@ -314,34 +310,61 @@ public class AssetService {
.lt(OrderTrade::getCreateTime, end.plusDays(1).atStartOfDay());
List<OrderTrade> sellOrders = orderTradeMapper.selectList(wrapper);
// 按日期分组计算每日已实现盈亏
for (OrderTrade order : sellOrders) {
String dateKey = order.getCreateTime().toLocalDate().toString();
AccountTrade account = getOrCreateTradeAccount(userId, order.getCoinCode());
BigDecimal cost = order.getQuantity().multiply(account.getAvgPrice());
BigDecimal profit = order.getAmount().subtract(cost);
dailyMap.merge(dateKey, profit, BigDecimal::add);
if (!sellOrders.isEmpty()) {
// 批量获取涉及的币种持仓信息(只读查询,不创建)
List<String> coinCodes = sellOrders.stream()
.map(OrderTrade::getCoinCode)
.distinct()
.collect(Collectors.toList());
Map<String, AccountTrade> tradeMap = new HashMap<>();
LambdaQueryWrapper<AccountTrade> tradeWrapper = new LambdaQueryWrapper<>();
tradeWrapper.eq(AccountTrade::getUserId, userId)
.in(AccountTrade::getCoinCode, coinCodes);
List<AccountTrade> tradeAccounts = accountTradeMapper.selectList(tradeWrapper);
for (AccountTrade at : tradeAccounts) {
tradeMap.put(at.getCoinCode(), at);
}
// 按日期分组计算每日已实现盈亏
for (OrderTrade order : sellOrders) {
String dateKey = order.getCreateTime().toLocalDate().toString();
AccountTrade account = tradeMap.get(order.getCoinCode());
if (account != null) {
BigDecimal cost = order.getQuantity().multiply(account.getAvgPrice());
BigDecimal profit = order.getAmount().subtract(cost);
dailyMap.merge(dateKey, profit, BigDecimal::add);
}
}
}
// 今日额外加上未实现盈亏
// 2. 今日未实现盈亏(批量加载币种)
LocalDate today = LocalDate.now();
if (!today.isBefore(start) && !today.isAfter(end)) {
BigDecimal unrealized = BigDecimal.ZERO;
LambdaQueryWrapper<AccountTrade> tradeWrapper = new LambdaQueryWrapper<>();
tradeWrapper.eq(AccountTrade::getUserId, userId)
.gt(AccountTrade::getQuantity, BigDecimal.ZERO);
List<AccountTrade> trades = accountTradeMapper.selectList(tradeWrapper);
for (AccountTrade trade : trades) {
Coin coin = coinService.getCoinByCode(trade.getCoinCode());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice());
BigDecimal cost = trade.getQuantity().multiply(trade.getAvgPrice());
unrealized = unrealized.add(value.subtract(cost));
if (!trades.isEmpty()) {
List<String> coinCodes = trades.stream()
.map(AccountTrade::getCoinCode)
.distinct()
.collect(Collectors.toList());
Map<String, Coin> coinMap = coinService.getCoinMapByCodes(coinCodes);
BigDecimal unrealized = BigDecimal.ZERO;
for (AccountTrade trade : trades) {
Coin coin = coinMap.get(trade.getCoinCode().toUpperCase());
if (coin != null) {
BigDecimal value = trade.getQuantity().multiply(coin.getPrice());
BigDecimal cost = trade.getQuantity().multiply(trade.getAvgPrice());
unrealized = unrealized.add(value.subtract(cost));
}
}
String todayKey = today.toString();
dailyMap.merge(todayKey, unrealized, BigDecimal::add);
}
String todayKey = today.toString();
dailyMap.merge(todayKey, unrealized, BigDecimal::add);
}
// 计算月度总盈亏

View File

@@ -7,39 +7,108 @@ import com.it.rattan.monisuo.mapper.CoinMapper;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 币种服务
* 币种服务 - 带内存缓存
*/
@Service
public class CoinService extends ServiceImpl<CoinMapper, Coin> {
/** 缓存过期时间毫秒30秒 */
private static final long CACHE_TTL = 30_000;
/** 币种列表缓存 */
private volatile List<Coin> cachedActiveCoins;
private volatile long activeCoinsCacheTime = 0;
/** 单个币种缓存 */
private final Map<String, Coin> coinCodeCache = new ConcurrentHashMap<>();
private final Map<String, Long> coinCodeCacheTime = new ConcurrentHashMap<>();
/**
* 获取所有上架币种
* 获取所有上架币种(带缓存)
*/
public List<Coin> getActiveCoins() {
long now = System.currentTimeMillis();
if (cachedActiveCoins != null && (now - activeCoinsCacheTime) < CACHE_TTL) {
return cachedActiveCoins;
}
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Coin::getStatus, 1)
.orderByDesc(Coin::getSort);
return list(wrapper);
cachedActiveCoins = list(wrapper);
activeCoinsCacheTime = now;
return cachedActiveCoins;
}
/**
* 根据代码获取币种
* 根据代码获取币种(带缓存)
*/
public Coin getCoinByCode(String code) {
String key = code.toUpperCase();
long now = System.currentTimeMillis();
Long cacheTime = coinCodeCacheTime.get(key);
if (cacheTime != null && (now - cacheTime) < CACHE_TTL) {
return coinCodeCache.get(key);
}
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Coin::getCode, code.toUpperCase())
wrapper.eq(Coin::getCode, key)
.eq(Coin::getStatus, 1);
return getOne(wrapper);
Coin coin = getOne(wrapper);
coinCodeCache.put(key, coin);
coinCodeCacheTime.put(key, now);
return coin;
}
/**
* 更新币种价格
* 注意USDT价格固定为1不允许修改
* 批量获取币种(一次查询,返回 Map<code, Coin>
* 用于解决 N+1 查询问题
*/
public Map<String, Coin> getCoinMapByCodes(List<String> codes) {
if (codes == null || codes.isEmpty()) {
return new java.util.HashMap<>();
}
List<String> upperCodes = codes.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 先从缓存中取
Map<String, Coin> result = new ConcurrentHashMap<>();
List<String> missedCodes = new java.util.ArrayList<>();
long now = System.currentTimeMillis();
for (String code : upperCodes) {
Long cacheTime = coinCodeCacheTime.get(code);
if (cacheTime != null && (now - cacheTime) < CACHE_TTL && coinCodeCache.get(code) != null) {
result.put(code, coinCodeCache.get(code));
} else {
missedCodes.add(code);
}
}
// 批量查询缺失的
if (!missedCodes.isEmpty()) {
LambdaQueryWrapper<Coin> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Coin::getCode, missedCodes)
.eq(Coin::getStatus, 1);
List<Coin> coins = list(wrapper);
for (Coin coin : coins) {
String code = coin.getCode().toUpperCase();
result.put(code, coin);
coinCodeCache.put(code, coin);
coinCodeCacheTime.put(code, now);
}
}
return result;
}
/**
* 更新币种价格(同时清除缓存)
*/
public void updatePrice(String code, BigDecimal price) {
// USDT价格固定为1不允许修改
if ("USDT".equalsIgnoreCase(code)) {
return;
}
@@ -48,6 +117,8 @@ public class CoinService extends ServiceImpl<CoinMapper, Coin> {
coin.setPrice(price);
coin.setUpdateTime(java.time.LocalDateTime.now());
updateById(coin);
// 清除该币种的缓存
clearCache(code);
}
}
@@ -60,4 +131,24 @@ public class CoinService extends ServiceImpl<CoinMapper, Coin> {
.eq(Coin::getStatus, 1);
return list(wrapper);
}
/**
* 清除所有缓存(币种数据变更时调用)
*/
public void clearCache(String code) {
if (code != null) {
String key = code.toUpperCase();
coinCodeCache.remove(key);
coinCodeCacheTime.remove(key);
}
cachedActiveCoins = null;
activeCoinsCacheTime = 0;
}
public void clearAllCache() {
cachedActiveCoins = null;
activeCoinsCacheTime = 0;
coinCodeCache.clear();
coinCodeCacheTime.clear();
}
}

View File

@@ -379,29 +379,14 @@ public class FundService {
throw new RuntimeException("充值审批更新账户余额失败");
}
// 验证更新结果 - 使用新的查询确保从数据库读取
AccountFund verifyFund = accountFundMapper.selectById(fund.getId());
System.out.println(" - 验证更新后余额: " + verifyFund.getBalance());
System.out.println(" - 验证更新后累计充值: " + verifyFund.getTotalDeposit());
if (verifyFund.getBalance() == null || !verifyFund.getBalance().equals(newBalance)) {
System.err.println("[FundService.approve] 严重错误: 账户余额更新验证失败!");
System.err.println(" - 期望余额: " + newBalance);
System.err.println(" - 实际余额: " + verifyFund.getBalance());
throw new RuntimeException("账户余额更新验证失败");
}
System.out.println(" - 余额验证通过 ✓");
// 更新本地对象状态
// 更新本地对象状态(直接信任 update 返回值,避免额外的 selectById 查询)
fund.setBalance(newBalance);
fund.setTotalDeposit(newTotalDeposit);
fund.setUpdateTime(updateTime);
// 记录流水
System.out.println("[FundService.approve] 创建资金流水记录...");
assetService.createFlow(order.getUserId(), 1, order.getAmount(),
balanceBefore, newBalance, "USDT", orderNo, "充值");
System.out.println(" - 流水记录创建成功");
System.out.println("[充值审批成功] 订单号: " + orderNo + ", 用户ID: " + order.getUserId() +
", 充值金额: " + order.getAmount() + " USDT");
@@ -443,21 +428,14 @@ public class FundService {
throw new RuntimeException("提现审批更新账户冻结失败");
}
// 验证更新结果
AccountFund verifyFund = accountFundMapper.selectById(fund.getId());
System.out.println(" - 验证更新后冻结: " + verifyFund.getFrozen());
System.out.println(" - 验证更新后累计提现: " + verifyFund.getTotalWithdraw());
// 更新本地对象状态
fund.setFrozen(newFrozen);
fund.setTotalWithdraw(newTotalWithdraw);
fund.setUpdateTime(updateTime);
// 记录流水 (负数表示支出)
System.out.println("[FundService.approve] 创建资金流水记录...");
assetService.createFlow(order.getUserId(), 2, order.getAmount().negate(),
balanceBefore, balanceBefore, "USDT", orderNo, "提现");
System.out.println(" - 流水记录创建成功");
System.out.println("[提现审批成功] 订单号: " + orderNo + ", 用户ID: " + order.getUserId() +
", 提现金额: " + order.getAmount() + " USDT");
@@ -505,21 +483,14 @@ public class FundService {
throw new RuntimeException("提现驳回更新账户失败");
}
// 验证更新结果
AccountFund verifyFund = accountFundMapper.selectById(fund.getId());
System.out.println(" - 验证更新后余额: " + verifyFund.getBalance());
System.out.println(" - 验证更新后冻结: " + verifyFund.getFrozen());
// 更新本地对象状态
fund.setBalance(newBalance);
fund.setFrozen(newFrozen);
fund.setUpdateTime(updateTime);
// 记录流水
System.out.println("[FundService.approve] 创建资金流水记录...");
assetService.createFlow(order.getUserId(), 2, order.getAmount(),
balanceBefore, newBalance, "USDT", orderNo, "提现驳回退还");
System.out.println(" - 流水记录创建成功");
System.out.println("[提现驳回成功] 订单号: " + orderNo + ", 用户ID: " + order.getUserId() +
", 退还金额: " + order.getAmount() + " USDT");
@@ -552,61 +523,14 @@ public class FundService {
System.out.println(" - UPDATE SQL 将更新: status=" + finalStatus + ", approveAdminId=" + adminId);
int orderUpdateResult = orderFundMapper.update(null, updateWrapper);
System.out.println(" - 订单更新结果: " + orderUpdateResult + " (1=成功, 0=失败)");
// 验证更新是否成功 - 使用新的查询确保从数据库读取
if (orderUpdateResult > 0) {
System.out.println("[FundService.approve] 步骤6: 验证更新结果...");
// 清除可能的缓存,强制从数据库读取
OrderFund verifyOrder = orderFundMapper.selectOne(
new LambdaQueryWrapper<OrderFund>()
.eq(OrderFund::getId, order.getId())
.select(OrderFund::getId, OrderFund::getOrderNo, OrderFund::getStatus,
OrderFund::getApproveAdminId, OrderFund::getApproveTime));
System.out.println(" - 验证查询结果: ID=" + verifyOrder.getId() +
", 订单号=" + verifyOrder.getOrderNo() +
", 状态=" + verifyOrder.getStatus());
if (!verifyOrder.getStatus().equals(finalStatus)) {
System.err.println("[FundService.approve] 严重错误: 订单状态更新后验证失败!");
System.err.println(" - 期望状态: " + finalStatus);
System.err.println(" - 实际状态: " + verifyOrder.getStatus());
throw new RuntimeException("订单状态更新失败,请检查数据库配置");
} else {
System.out.println(" - 状态验证通过 ✓");
}
} else {
System.err.println("[FundService.approve] 订单更新失败! update返回: " + orderUpdateResult);
if (orderUpdateResult <= 0) {
throw new RuntimeException("订单更新失败");
}
// 最终验证:确保账户余额正确更新(仅在审批通过时)
if (status == 2) {
System.out.println("[FundService.approve] 步骤7: 最终验证账户余额...");
AccountFund finalVerifyFund = accountFundMapper.selectById(fund.getId());
System.out.println(" - 最终账户余额: " + finalVerifyFund.getBalance());
System.out.println(" - 最终累计充值: " + finalVerifyFund.getTotalDeposit());
if (order.getType() == 1) {
// 充值订单:验证余额是否正确增加
BigDecimal expectedBalance = fund.getBalance();
if (finalVerifyFund.getBalance() == null ||
finalVerifyFund.getBalance().compareTo(expectedBalance) != 0) {
System.err.println("[FundService.approve] 严重错误: 最终验证发现账户余额不一致!");
System.err.println(" - 期望余额: " + expectedBalance);
System.err.println(" - 实际余额: " + finalVerifyFund.getBalance());
throw new RuntimeException("账户余额最终验证失败,数据可能不一致");
}
System.out.println(" - 账户余额最终验证通过 ✓");
}
}
System.out.println("[审批完成] 订单号: " + orderNo + ", 订单类型: " + (order.getType() == 1 ? "充值" : "提现") +
", 审批结果: " + (status == 2 ? "通过" : "驳回") + ", 最终状态: " + finalStatus +
", 审批人: " + adminName);
System.out.println("[FundService.approve] 处理完成\n");
}
/**

View File

@@ -12,6 +12,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@@ -122,20 +125,52 @@ public class UserService extends ServiceImpl<UserMapper, User> {
}
/**
* 上传KYC资料
* 上传KYC资料(身份证正反面图片)
*/
@Transactional
public void uploadKyc(Long userId, String idCardFront, String idCardBack) {
public void uploadKyc(Long userId, MultipartFile frontFile, MultipartFile backFile) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
user.setIdCardFront(idCardFront);
user.setIdCardBack(idCardBack);
user.setKycStatus(1);
user.setUpdateTime(LocalDateTime.now());
userMapper.updateById(user);
try {
// 上传目录:项目工作目录下的 uploads/kyc/
String uploadDir = System.getProperty("user.dir") + File.separator + "uploads" + File.separator + "kyc";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
String timestamp = String.valueOf(System.currentTimeMillis());
String frontPath = saveFile(frontFile, dir, userId + "_front_" + timestamp);
String backPath = saveFile(backFile, dir, userId + "_back_" + timestamp);
// 存储相对访问路径
user.setIdCardFront("/uploads/kyc/" + frontPath);
user.setIdCardBack("/uploads/kyc/" + backPath);
user.setKycStatus(2); // 虚拟KYC提交即自动通过
user.setUpdateTime(LocalDateTime.now());
userMapper.updateById(user);
} catch (IOException e) {
throw new RuntimeException("文件保存失败: " + e.getMessage());
}
}
/**
* 保存上传文件,返回保存的文件名
*/
private String saveFile(MultipartFile file, File dir, String baseName) throws IOException {
String originalName = file.getOriginalFilename();
String ext = "";
if (originalName != null && originalName.contains(".")) {
ext = originalName.substring(originalName.lastIndexOf("."));
} else {
ext = ".jpg";
}
String fileName = baseName + ext;
file.transferTo(new File(dir, fileName));
return fileName;
}
/**

View File

@@ -2,6 +2,11 @@ server:
port: 5010
spring:
servlet:
multipart:
enabled: true
max-file-size: 5MB
max-request-size: 10MB
datasource:
username: monisuo
password: JPJ8wYicSGC8aRnk

View File

@@ -2,6 +2,11 @@ server:
port: 9010
spring:
servlet:
multipart:
enabled: true
max-file-size: 5MB
max-request-size: 10MB
datasource:
username: root
password: 897admin$$