111
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域过滤器 - 支持凭证,最高优先级
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -26,6 +26,7 @@ public class TokenFilter implements Filter {
|
||||
"/api/user/login",
|
||||
"/api/wallet/default",
|
||||
"/admin/login",
|
||||
"/uploads/",
|
||||
"/swagger-resources",
|
||||
"/v2/api-docs",
|
||||
"/webjars/",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 计算月度总盈亏
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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$$
|
||||
|
||||
Reference in New Issue
Block a user