1
zj
yesterday befbf57e4112d07003bff18102f556a1e5a154de
trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java
@@ -1,24 +1,45 @@
package com.yami.trading.service.trader.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yami.trading.bean.trader.domain.Trader;
import com.yami.trading.bean.trader.domain.TraderFollowUser;
import com.yami.trading.bean.trader.domain.TraderFollowUserOrder;
import com.yami.trading.common.constants.Constants;
import com.yami.trading.common.exception.BusinessException;
import com.yami.trading.common.util.Arith;
import com.yami.trading.common.util.StringUtils;
import com.yami.trading.dao.trader.TraderFollowUserMapper;
import com.yami.trading.dao.trader.TraderFollowUserOrderMapper;
import com.yami.trading.service.FollowWalletService;
import com.yami.trading.service.WalletService;
import com.yami.trading.service.trader.FollowCommissionService;
import com.yami.trading.service.trader.TraderFollowUserOrderService;
import com.yami.trading.service.trader.TraderFollowUserService;
import com.yami.trading.service.trader.TraderService;
import com.yami.trading.service.trader.TraderUserService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Instant;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class TraderFollowUserServiceImpl implements TraderFollowUserService {
   private static final ExecutorService STOP_FOLLOW_EXECUTOR = Executors.newFixedThreadPool(4);
   private final Set<String> stoppingTasks = ConcurrentHashMap.newKeySet();
   @Resource
   private TraderService traderService;
   @Resource
@@ -27,25 +48,26 @@
   @Resource
   private TraderFollowUserMapper traderFollowUserMapper;
   @Resource
   private TraderFollowUserOrderMapper traderFollowUserOrderMapper;
   @Resource
   private FollowWalletService followWalletService;
   @Resource
   private WalletService walletService;
   @Resource
   private FollowCommissionService followCommissionService;
   @Lazy
   @Resource
   private TraderFollowUserOrderService traderFollowUserOrderService;
   public List<Map<String, Object>> getPaged(Page pageparam, String partyId, String profit) {
//      StringBuffer queryString = new StringBuffer("");
//      queryString.append(" SELECT * FROM ");
//      queryString.append(" T_TRADER_FOLLOW_USER ");
//      queryString.append(" where 1=1 ");
//
//      Map<String, Object> parameters = new HashMap();
//
//      queryString.append(" and TRADER_PARTY_ID = :partyId");
//      parameters.put("partyId", partyId);
//      if (!StringUtils.isNullOrEmpty(profit)) {
//         queryString.append(" and PROFIT >= 0 ");
//      }
//
//      queryString.append(" order by PROFIT desc ");
      Page page = traderFollowUserMapper.selectPage(pageparam, Wrappers.<TraderFollowUser>lambdaQuery().eq(TraderFollowUser::getTraderPartyId, partyId).ge(TraderFollowUser::getProfit, 0).orderByDesc(TraderFollowUser::getProfit));
//      Page page = this.pagedQueryDao.pagedQuerySQL(pageNo, pageSize, queryString.toString(), parameters);
      Page<TraderFollowUser> page = traderFollowUserMapper.selectPage(pageparam,
            Wrappers.<TraderFollowUser>lambdaQuery()
                  .eq(TraderFollowUser::getTraderPartyId, partyId)
                  .orderByDesc(TraderFollowUser::getCreateTime)
                  .orderByDesc(TraderFollowUser::getUuid));
      List<Map<String, Object>> data = this.bulidData(page.getRecords());
      return data;
   }
@@ -54,15 +76,30 @@
      List<Map<String, Object>> result_traders = new ArrayList();
      DecimalFormat df2 = new DecimalFormat("#.##");
      df2.setRoundingMode(RoundingMode.FLOOR);// 向下取整
      SimpleDateFormat tsFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      if (traderFollowUsers == null) {
         return result_traders;
      }
      for (int i = 0; i < traderFollowUsers.size(); i++) {
         Map<String, Object> map = new HashMap<String, Object>();
         TraderFollowUser entity = traderFollowUsers.get(i);
         map.put("id", entity.getUuid());
         map.put("name", entity.getUsername());
         map.put("profit", df2.format(entity.getProfit()));
         map.put("amount_sum", df2.format(entity.getAmountSum()));
         map.put("followState", entity.getState());
         map.put("symbol", entity.getSymbol());
         map.put("volume", df2.format(entity.getVolume()));
         map.put("volumeMax", df2.format(entity.getVolumeMax()));
         map.put("lever_rate", entity.getLeverRate() > 0 ? df2.format(entity.getLeverRate()) : "");
         map.put("followFailReason", entity.getFailReason() != null ? entity.getFailReason() : "");
         if (entity.getCreateTime() != null) {
            map.put("follow_start_time", tsFmt.format(entity.getCreateTime()));
         } else {
            map.put("follow_start_time", "");
         }
         map.put("follow_stop_time", formatEpochSecond(entity.getStopFinishTime(), tsFmt));
         map.put("follow_fail_time", formatEpochSecond(entity.getLastFailTime(), tsFmt));
         result_traders.add(map);
      }
@@ -71,17 +108,17 @@
   }
   private static String formatEpochSecond(Long sec, SimpleDateFormat tsFmt) {
      if (sec == null || sec <= 0L) {
         return "";
      }
      return tsFmt.format(new Date(sec * 1000L));
   }
   @Override
   @Transactional(rollbackFor = Exception.class)
   public void save(TraderFollowUser entity, String trader_id) {
      if (entity.getVolume() % 1 != 0 || entity.getVolume() <= 0 || entity.getVolumeMax() % 1 != 0) {
         throw new BusinessException(1, "跟单参数输入错误");
      }
      if (entity.getFollowType() == "1" && (entity.getVolume() > 3000 || entity.getVolume() < 1)) {
         throw new BusinessException(1, "跟单参数输入错误");
      }
      if (entity.getFollowType() == "2" && (entity.getVolume() > 5 || entity.getVolume() < 1)) {
         throw new BusinessException(1, "跟单倍数输入错误");
      }
      validateFollowConfig(entity);
      Trader trader = this.traderService.findById(trader_id);
      if (trader == null) {
         throw new BusinessException(1, "交易员不存在");
@@ -89,68 +126,104 @@
      if ("0".equals(trader.getState())) {
         throw new BusinessException(1, "交易员未开启带单");
      }
      if (findByStateAndPartyId(entity.getPartyId(), trader.getPartyId(), "1") != null) {
         throw new BusinessException(1, "用户已跟随交易员");
      }
      if (Arith.sub(trader.getFollowerMax(), trader.getFollowerNow()) < 1) {
         throw new BusinessException(1, "交易员跟随人数已满");
      if (trader.getChecked() != 1) {
         throw new BusinessException(1, "交易员审核未通过");
      }
      if (entity.getPartyId().equals(trader.getPartyId())) {
         throw new BusinessException(1, "交易员不能跟随自己");
      }
      validateFollowSymbol(entity.getSymbol(), trader.getSymbols());
      Trader trader_user = this.traderService.findByPartyId(entity.getPartyId());
      if (trader_user != null) {
      if (trader_user != null && trader_user.getChecked() == 1) {
         throw new BusinessException(1, "交易员无法跟随另一个交易员");
      }
      // 跟单固定张数/固定比例---选择 1,固定张数,2,固定比例
      if (trader.getFollowVolumnMin() > 0) {
         switch (entity.getFollowType()) {
         case "1":
            if (entity.getVolume() < trader.getFollowVolumnMin()) {
               throw new BusinessException(1, "跟单参数输入错误");
            }
            if (entity.getVolumeMax() < trader.getFollowVolumnMin()) {
               throw new BusinessException(1, "跟单参数输入错误");
            }
            break;
         case "2":
            throw new BusinessException(1, "交易员已设置最小下单数,无法通过固定比例跟单");
         default:
            break;
      BigDecimal followMin = trader.getFollowVolumnMin();
      if (followMin != null && followMin.compareTo(BigDecimal.ZERO) > 0) {
         double minVol = followMin.doubleValue();
         if (entity.getVolume() < minVol || entity.getVolumeMax() < minVol) {
            throw new BusinessException(1, "跟单币数量不能低于交易员设置的最小值");
         }
      }
      TraderFollowUser latest = findByPartyIdAndTrader_partyId(entity.getPartyId(), trader.getPartyId());
      if (latest != null
            && (TraderFollowUser.STATE_FOLLOWING.equals(latest.getState())
            || TraderFollowUser.STATE_STOPPING.equals(latest.getState()))) {
         throw new BusinessException(1, "用户已跟随交易员");
      }
      try {
         followCommissionService.applyMonthlyFeeIfNeeded(trader, latest, entity);
      } catch (BusinessException e) {
         if (isInsufficientBalanceError(e)) {
            recordFollowFailed(entity, trader, e.getMessage(), latest);
         }
         throw e;
      }
      entity.setTraderPartyId(trader.getPartyId());
      entity.setCreateTime(new Date());
      entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
      entity.setState(TraderFollowUser.STATE_FOLLOWING);
      entity.setInvestAmount(BigDecimal.valueOf(entity.getVolume()));
      entity.setStopRequestTime(null);
      entity.setStopFinishTime(null);
      entity.setFailReason(null);
      entity.setLastFailTime(null);
      long priorSessions = traderFollowUserMapper.selectCount(Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getTraderPartyId, trader.getPartyId())
            .eq(TraderFollowUser::getPartyId, entity.getPartyId())
            .in(TraderFollowUser::getState, TraderFollowUser.STATE_FOLLOWING, TraderFollowUser.STATE_STOPPING,
                  TraderFollowUser.STATE_STOPPED));
      if (priorSessions == 0L) {
         trader.setFollowerSum((int) Arith.add(trader.getFollowerSum(), 1));
      }
      trader.setFollowerNow((int) Arith.add(trader.getFollowerNow(), 1));
      trader.setFollowerSum((int) Arith.add(trader.getFollowerSum(), 1));
      traderService.update(trader);
      /**
       * 创建累计用户跟随累计表
       */
      traderUserService.saveTraderUserByPartyId(entity.getPartyId());
//      ApplicationUtil.executeSaveOrUpdate(entity);
      if (latest != null) {
         entity.setProfit(latest.getProfit());
         entity.setAmountSum(latest.getAmountSum());
         if (entity.getMonthlyFeePaidPeriod() == null) {
            entity.setMonthlyFeePaidPeriod(latest.getMonthlyFeePaidPeriod());
         }
      }
      entity.setUuid(null);
      traderFollowUserMapper.insert(entity);
      /**
       * 纠正历史脏数据:手动停止跟单时跟单方 saveClose 未同步 T_TRADER_FOLLOW_USER_ORDER 状态,
       * 再次跟单前应把「合约已平仓仍为 submitted」的映射置为 created,否则会占满 volumeMax。
       */
      traderFollowUserOrderService.reconcileStaleSubmittedMappings(entity.getPartyId(), entity.getTraderPartyId());
   }
   @Override
   public void save(TraderFollowUser entity) {
      if (entity.getFollowType() == null) {
         entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
      }
      if (entity.getState() == null) {
         entity.setState(TraderFollowUser.STATE_FOLLOWING);
      }
      traderFollowUserMapper.insert(entity);
   }
   @Override
   public void update(TraderFollowUser entity) {
      if (entity.getVolume() % 1 != 0 || entity.getVolume() <= 0 || entity.getVolumeMax() % 1 != 0) {
         throw new BusinessException(1, "跟单参数输入错误");
      }
      if (entity.getFollowType() == "1" && (entity.getVolume() > 3000 || entity.getVolume() < 1)) {
         throw new BusinessException(1, "跟单参数输入错误");
      }
      if (entity.getFollowType() == "2" && (entity.getVolume() > 5 || entity.getVolume() < 1)) {
         throw new BusinessException(1, "跟单倍数输入错误");
      validateFollowConfig(entity);
      entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
      entity.setInvestAmount(BigDecimal.valueOf(entity.getVolume()));
      TraderFollowUser old = traderFollowUserMapper.selectById(entity.getUuid());
      if (old != null) {
         Trader trader = this.traderService.findByPartyId(old.getTraderPartyId());
         if (trader != null) {
            validateFollowSymbol(entity.getSymbol(), trader.getSymbols());
         }
      }
//      ApplicationUtil.executeUpdate(entity);
@@ -160,6 +233,9 @@
   @Override
   public void deleteCancel(String id) {
      TraderFollowUser entity = findById(id);
      if (entity == null) {
         return;
      }
      /**
       * 将旧的交易员跟随用户-1
       */
@@ -169,9 +245,51 @@
      if (entity != null) {
//         ApplicationUtil.executeDelete(entity);
         traderFollowUserMapper.deleteById(entity);
         traderFollowUserMapper.deleteById(entity.getUuid());
      }
   }
   @Override
   public void cancelFollowAsync(String id, com.yami.trading.service.contract.ContractOrderService contractOrderService) {
      TraderFollowUser entity = findById(id);
      if (entity == null || TraderFollowUser.STATE_STOPPED.equals(entity.getState())) {
         return;
      }
      if (TraderFollowUser.STATE_STOPPING.equals(entity.getState()) || !stoppingTasks.add(entity.getUuid())) {
         return;
      }
      entity.setState(TraderFollowUser.STATE_STOPPING);
      entity.setStopRequestTime(Instant.now().getEpochSecond());
      traderFollowUserMapper.updateById(entity);
      STOP_FOLLOW_EXECUTOR.submit(() -> {
         try {
            List<TraderFollowUserOrder> openOrders = traderFollowUserOrderMapper.selectList(
                  Wrappers.<TraderFollowUserOrder>lambdaQuery()
                        .eq(TraderFollowUserOrder::getPartyId, entity.getPartyId())
                        .eq(TraderFollowUserOrder::getTraderPartyId, entity.getTraderPartyId())
                        .eq(TraderFollowUserOrder::getState, TraderFollowUserOrder.STATE_SUBMITTED));
            if (openOrders != null) {
               for (TraderFollowUserOrder openOrder : openOrders) {
                  contractOrderService.saveClose(entity.getPartyId(), openOrder.getUserOrderNo());
               }
            }
            TraderFollowUser latest = findById(id);
            if (latest != null) {
               latest.setState(TraderFollowUser.STATE_STOPPED);
               latest.setStopFinishTime(Instant.now().getEpochSecond());
               traderFollowUserMapper.updateById(latest);
               refundFollowWalletToMainWallet(latest.getPartyId());
               Trader trader = this.traderService.findByPartyId(latest.getTraderPartyId());
               if (trader != null && trader.getFollowerNow() > 0) {
                  trader.setFollowerNow((int) Arith.sub(trader.getFollowerNow(), 1));
                  this.traderService.update(trader);
               }
            }
         } finally {
            stoppingTasks.remove(entity.getUuid());
         }
      });
   }
   public List<TraderFollowUser> findByStateAndPartyId(String partyId, String trader_partyId, String state) {
@@ -193,6 +311,17 @@
      return null;
   }
   @Override
   public List<TraderFollowUser> findActiveByTraderPartyId(String trader_partyId) {
      List<TraderFollowUser> list = traderFollowUserMapper.selectList(Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getTraderPartyId, trader_partyId)
            .eq(TraderFollowUser::getState, TraderFollowUser.STATE_FOLLOWING));
      if (list.size() > 0) {
         return list;
      }
      return null;
   }
   public List<TraderFollowUser> findByPartyId(String partyId) {
      List<TraderFollowUser> list = traderFollowUserMapper.selectList(Wrappers.<TraderFollowUser>lambdaQuery().eq(TraderFollowUser::getPartyId, partyId));
//      List<TraderFollowUser> list = ApplicationUtil.executeSelect(TraderFollowUser.class, " WHERE PARTY_ID = ? ",
@@ -202,8 +331,33 @@
      return null;
   }
   @Override
   public long countByPartyId(String partyId) {
      if (StringUtils.isNullOrEmpty(partyId)) {
         return 0L;
      }
      Long c = traderFollowUserMapper.selectCount(Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getPartyId, partyId));
      return c == null ? 0L : c.longValue();
   }
   @Override
   public IPage<TraderFollowUser> pageByPartyId(Page<TraderFollowUser> page, String partyId) {
      if (page == null || StringUtils.isNullOrEmpty(partyId)) {
         return page;
      }
      return traderFollowUserMapper.selectPage(page, Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getPartyId, partyId)
            .orderByDesc(TraderFollowUser::getUpdateTime)
            .orderByDesc(TraderFollowUser::getCreateTime));
   }
   public TraderFollowUser findByPartyIdAndTrader_partyId(String partyId, String trader_partyId) {
      List<TraderFollowUser> list = traderFollowUserMapper.selectList(Wrappers.<TraderFollowUser>lambdaQuery().eq(TraderFollowUser::getPartyId, partyId).eq(TraderFollowUser::getTraderPartyId, trader_partyId));
      List<TraderFollowUser> list = traderFollowUserMapper.selectList(Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getPartyId, partyId)
            .eq(TraderFollowUser::getTraderPartyId, trader_partyId)
            .orderByDesc(TraderFollowUser::getCreateTime)
            .orderByDesc(TraderFollowUser::getUuid));
//      List<TraderFollowUser> list = ApplicationUtil.executeSelect(TraderFollowUser.class,
//            " WHERE PARTY_ID= ? and TRADER_PARTY_ID = ? ",
//            new Object[] { partyId, trader_partyId });
@@ -212,10 +366,127 @@
      return null;
   }
   private boolean isInsufficientBalanceError(BusinessException e) {
      if (e == null || e.getMessage() == null) {
         return false;
      }
      return e.getMessage().contains("余额不足");
   }
   private void recordFollowFailed(TraderFollowUser entity, Trader trader, String reason, TraderFollowUser latest) {
      TraderFollowUser failed = new TraderFollowUser();
      failed.setPartyId(entity.getPartyId());
      failed.setUsername(entity.getUsername());
      failed.setTraderPartyId(trader.getPartyId());
      failed.setSymbol(entity.getSymbol());
      failed.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
      failed.setVolume(entity.getVolume());
      failed.setVolumeMax(entity.getVolumeMax());
      failed.setInvestAmount(BigDecimal.valueOf(entity.getVolume()));
      failed.setStopLoss(0D);
      failed.setStopProfit(0D);
      failed.setState(TraderFollowUser.STATE_FAILED);
      failed.setFailReason(reason);
      failed.setLastFailTime(Instant.now().getEpochSecond());
      failed.setCreateTime(new Date());
      if (latest != null && TraderFollowUser.STATE_FAILED.equals(latest.getState())) {
         failed.setUuid(latest.getUuid());
         failed.setProfit(latest.getProfit());
         failed.setAmountSum(latest.getAmountSum());
         failed.setMonthlyFeePaidPeriod(latest.getMonthlyFeePaidPeriod());
         traderFollowUserMapper.updateById(failed);
         return;
      }
      traderFollowUserMapper.insert(failed);
   }
   public TraderFollowUser findById(String id) {
//      return ApplicationUtil.executeGet(id, TraderFollowUser.class);
      TraderFollowUser traderFollowUser = traderFollowUserMapper.selectById(id);
      return traderFollowUser;
   }
   private void validateFollowConfig(TraderFollowUser entity) {
      entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
      if (entity.getVolume() <= 0 || entity.getVolumeMax() <= 0) {
         throw new BusinessException(1, "跟单参数输入错误");
      }
      if (entity.getVolumeMax() < entity.getVolume()) {
         throw new BusinessException(1, "最大跟单币数量不能小于最小跟单币数量");
      }
      if (entity.getStopLoss() < 0 || entity.getStopProfit() < 0) {
         throw new BusinessException(1, "止盈止损参数输入错误");
      }
      if (entity.getLeverRate() <= 0) {
         throw new BusinessException(1, "杠杆倍数必须大于0");
      }
   }
   private void validateFollowSymbol(String followSymbol, String traderSymbolsRaw) {
      if (followSymbol == null || followSymbol.trim().isEmpty()) {
         throw new BusinessException(1, "请选择跟单币种");
      }
      String follow = followSymbol.trim();
      String raw = traderSymbolsRaw == null ? "" : traderSymbolsRaw.trim();
      if (raw.isEmpty()) {
         throw new BusinessException(1, "交易员未配置带单币种");
      }
      String[] arr = raw.split("[;;,,]+");
      for (String one : arr) {
         if (follow.equalsIgnoreCase(one == null ? "" : one.trim())) {
            return;
         }
      }
      throw new BusinessException(1, "只能选择交易员带单币种中的一种进行跟单");
   }
   private void refundFollowWalletToMainWallet(String partyId) {
      if (partyId == null || partyId.trim().isEmpty()) {
         return;
      }
      com.yami.trading.bean.model.FollowWallet followWallet = followWalletService.saveWalletByPartyId(partyId);
      if (followWallet == null || followWallet.getMoney() == null || followWallet.getMoney().compareTo(BigDecimal.ZERO) <= 0) {
         return;
      }
      BigDecimal refund = followWallet.getMoney();
      walletService.updateMoney("USDT", partyId, refund, BigDecimal.ZERO,
            Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE,
            "停止跟单返还独立跟单账户资金");
      followWalletService.updateMoney("USDT", partyId, refund.negate(), BigDecimal.ZERO,
            Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE,
            "停止跟单划转资金到主钱包");
   }
   @Override
   public void markFollowOpenFailed(String partyId, String traderPartyId, String reason) {
      if (partyId == null || partyId.trim().isEmpty() || traderPartyId == null || traderPartyId.trim().isEmpty()) {
         return;
      }
      List<TraderFollowUser> list = traderFollowUserMapper.selectList(Wrappers.<TraderFollowUser>lambdaQuery()
            .eq(TraderFollowUser::getPartyId, partyId)
            .eq(TraderFollowUser::getTraderPartyId, traderPartyId)
            .eq(TraderFollowUser::getState, TraderFollowUser.STATE_FOLLOWING)
            .orderByDesc(TraderFollowUser::getCreateTime)
            .last("LIMIT 1"));
      if (list == null || list.isEmpty()) {
         return;
      }
      TraderFollowUser u = list.get(0);
      String msg = reason == null ? "" : reason.trim();
      if (msg.length() > 900) {
         msg = msg.substring(0, 900) + "…";
      }
      long nowSec = Instant.now().getEpochSecond();
      u.setState(TraderFollowUser.STATE_FAILED);
      u.setFailReason(msg);
      u.setLastFailTime(nowSec);
      u.setStopFinishTime(nowSec);
      traderFollowUserMapper.updateById(u);
      Trader trader = this.traderService.findByPartyId(traderPartyId);
      if (trader != null && trader.getFollowerNow() > 0) {
         trader.setFollowerNow((int) Arith.sub(trader.getFollowerNow(), 1));
         this.traderService.update(trader);
      }
   }
}