From befbf57e4112d07003bff18102f556a1e5a154de Mon Sep 17 00:00:00 2001
From: zj <1772600164@qq.com>
Date: Wed, 22 Apr 2026 10:53:37 +0800
Subject: [PATCH] 1

---
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java |  391 +++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 331 insertions(+), 60 deletions(-)

diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java
index 9b68706..b38bdae 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java
+++ b/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);
+		}
+	}
+
 }

--
Gitblit v1.9.3