feat: 添加业务分析后端接口

新增 AnalysisController 提供 6 个分析接口:
- /admin/analysis/profit - 盈利分析(交易手续费/充提手续费/资金利差)
- /admin/analysis/cash-flow - 资金流动趋势(按月统计充值/提现/净流入)
- /admin/analysis/trade - 交易分析(买入/卖出统计+趋势)
- /admin/analysis/coin-distribution - 币种交易分布
- /admin/analysis/user-growth - 用户增长分析(新增/活跃用户)
- /admin/analysis/risk - 风险指标(大额交易/异常提现/KYC/冻结账户)
- /admin/analysis/health - 综合健康度评分

更新 Mapper 添加分析查询方法:
- OrderFundMapper: 手续费统计、时间范围查询、大额交易、异常提现
- OrderTradeMapper: 交易金额统计、活跃用户、币种分布

前端 API 对接:
- 新增 6 个分析相关 Query hooks
- 更新 analytics.vue 使用真实数据
- 动态决策建议基于实际数据
This commit is contained in:
2026-03-22 04:50:19 +08:00
parent 0e95890d68
commit c3f196ded4
23 changed files with 3520 additions and 1055 deletions

View File

@@ -0,0 +1,342 @@
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;
import com.it.rattan.monisuo.mapper.OrderTradeMapper;
import com.it.rattan.monisuo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 业务分析接口
*/
@RestController
@RequestMapping("/admin/analysis")
public class AnalysisController {
@Autowired
private OrderFundMapper orderFundMapper;
@Autowired
private OrderTradeMapper orderTradeMapper;
@Autowired
private AccountFundMapper accountFundMapper;
@Autowired
private UserService userService;
/**
* 盈利分析
*/
@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;
int days = getDays(range);
BigDecimal interestRate = new BigDecimal("0.035").divide(new BigDecimal("365"), 10, RoundingMode.HALF_UP);
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);
}
/**
* 资金流动趋势
*/
@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月");
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));
// 充值
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);
result.add(item);
}
return Result.success(result);
}
/**
* 交易分析
*/
@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);
data.put("buyAmount", buyAmount != null ? buyAmount : BigDecimal.ZERO);
data.put("buyCount", buyCount);
// 卖出统计
BigDecimal sellAmount = orderTradeMapper.sumAmountByTypeAndTime(2, startTime);
int sellCount = orderTradeMapper.countByTypeAndTime(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<>();
int days = "week".equals(range) ? 7 : 30;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M-d");
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);
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);
trend.add(item);
}
data.put("trend", trend);
return Result.success(data);
}
/**
* 币种交易分布
*/
@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);
}
/**
* 风险指标
*/
@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次)
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);
}
/**
* 综合健康度评分
*/
@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);
}
// ========== 工具方法 ==========
private LocalDateTime getStartTime(String range) {
switch (range) {
case "day":
return LocalDate.now().atStartOfDay();
case "week":
return LocalDate.now().minusWeeks(1).atStartOfDay();
case "month":
return LocalDate.now().withDayOfMonth(1).atStartOfDay();
case "year":
return LocalDate.now().withDayOfYear(1).atStartOfDay();
default:
return LocalDate.now().withDayOfMonth(1).atStartOfDay();
}
}
private int getDays(String range) {
switch (range) {
case "day":
return 1;
case "week":
return 7;
case "month":
return 30;
case "year":
return 365;
default:
return 30;
}
}
}

View File

@@ -3,8 +3,10 @@ package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.OrderFund;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 充提订单Mapper
@@ -20,4 +22,37 @@ public interface OrderFundMapper extends BaseMapper<OrderFund> {
@Select("SELECT COUNT(*) FROM order_fund WHERE status = 1")
int countPending();
// ========== 分析相关查询 ==========
/**
* 指定时间段内的手续费总额
*/
@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);
/**
* 大额交易数量
*/
@Select("SELECT COUNT(*) FROM order_fund WHERE amount >= #{threshold} AND status = 2")
int countLargeAmount(@Param("threshold") BigDecimal threshold);
/**
* 异常提现用户数(指定时间内提现次数超过阈值)
*/
@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);
}

View File

@@ -3,10 +3,55 @@ package com.it.rattan.monisuo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.it.rattan.monisuo.entity.OrderTrade;
import org.apache.ibatis.annotations.Mapper;
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
*/
@Mapper
public interface OrderTradeMapper extends BaseMapper<OrderTrade> {
// ========== 分析相关查询 ==========
/**
* 指定类型和时间段内的交易金额
*/
@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 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 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(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);
/**
* 指定时间段内的手续费总额假设手续费率为0.1%
*/
@Select("SELECT IFNULL(SUM(amount * 0.001), 0) FROM order_trade WHERE status = 2 AND create_time >= #{startTime}")
BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime);
}