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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user