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-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java |  623 +++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 477 insertions(+), 146 deletions(-)

diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java
index bca842c..b8842c0 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java
@@ -2,10 +2,15 @@
 
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yami.trading.bean.contract.domain.ContractOrder;
+import com.yami.trading.bean.item.domain.Item;
 import com.yami.trading.bean.model.User;
+import com.yami.trading.common.domain.BaseEntity;
+import com.yami.trading.bean.trader.FollowCommissionType;
 import com.yami.trading.bean.trader.domain.Trader;
 import com.yami.trading.bean.trader.domain.TraderFollowSetting;
 import com.yami.trading.bean.trader.domain.TraderFollowUser;
+import com.yami.trading.bean.trader.domain.TraderFollowUserOrder;
 import com.yami.trading.bean.trader.domain.TraderInviteLink;
 import com.yami.trading.common.constants.Constants;
 import com.yami.trading.common.exception.BusinessException;
@@ -14,6 +19,7 @@
 import com.yami.trading.common.web.ResultObject;
 import com.yami.trading.security.common.util.SecurityUtils;
 import com.yami.trading.service.contract.ContractOrderService;
+import com.yami.trading.service.item.ItemService;
 import com.yami.trading.service.trader.*;
 import com.yami.trading.service.user.UserService;
 import org.apache.commons.logging.Log;
@@ -25,11 +31,16 @@
 
 import javax.servlet.http.HttpServletRequest;
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
 import java.text.DecimalFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.*;
+import java.util.regex.Pattern;
 
 
 
@@ -55,6 +66,9 @@
 	private TraderFollowUserService traderFollowUserService;
 
 	@Autowired
+	private TraderFollowUserOrderService traderFollowUserOrderService;
+
+	@Autowired
 	private TraderOrderService traderOrderService;
 
 	@Autowired
@@ -62,6 +76,9 @@
 
 	@Autowired
 	private UserService userService;
+
+	@Autowired
+	private ItemService itemService;
 
 	@Autowired
 	private TraderFollowSettingService traderFollowSettingService;
@@ -101,21 +118,25 @@
 				for (int i = 0; i < data.size(); i++) {
 					Map<String, Object> map = data.get(i);
 					String partyId = SecurityUtils.getCurrentUserId();
+					Object commissionTypeForRemaining = map.get("follow_commission_type");
 					if (partyId != null) {
 						TraderFollowUser user = this.traderFollowUserService.findByPartyIdAndTrader_partyId(partyId, map.get("partyId").toString());
 						if (user != null) {
-							/**
-							 * 1跟随 2未跟随
-							 */
-							map.put("follow_state", "1");
+							map.put("follow_state", user.getState());
+							map.put("follow_volume", user.getVolume());
+							map.put("follow_volume_max", user.getVolumeMax());
+							map.put("follow_monthly_remaining_days",
+									computeFollowMonthlyRemainingDays(commissionTypeForRemaining, user));
 							map.remove("partyId");
 						} else {
 							map.put("follow_state", "2");
+							map.put("follow_monthly_remaining_days", null);
 							map.remove("partyId");
 						}
 
 					} else {
 						map.put("follow_state", "2");
+						map.put("follow_monthly_remaining_days", null);
 						map.remove("partyId");
 					}
 				}
@@ -148,27 +169,97 @@
 	@RequestMapping(action + "istrader.action")
 	public Object istrader(HttpServletRequest request) {
 		ResultObject resultObject = new ResultObject();
-		String id = request.getParameter("id");
+		try {
+			String id = request.getParameter("id");
 
-		Trader data = null;
-		if(StringUtils.isNotEmpty(id)) {
-			data = traderService.findById(id);
-		} else{
-			String partyId = SecurityUtils.getCurrentUserId();
-			data = traderService.findByPartyId(partyId);
-		}
-		if(null == data) {
+			Trader data = null;
+			if (StringUtils.isNotEmpty(id)) {
+				data = traderService.findById(id);
+			} else {
+				String partyId = SecurityUtils.getCurrentUserId();
+				if (StringUtils.isEmptyString(partyId)) {
+					resultObject.setCode("1");
+					resultObject.setMsg("用户未登录");
+					Map<String, Object> empty = new HashMap<>();
+					empty.put("exists", false);
+					empty.put("checked", null);
+					empty.put("can_reapply", false);
+					empty.put("reject_reason", "");
+					empty.put("name", "");
+					empty.put("symbols", "");
+					empty.put("follow_volumn_min", 0d);
+					empty.put("follow_commission_type", FollowCommissionType.LEGACY);
+					empty.put("follow_commission_monthly_amount", "0");
+					empty.put("follow_commission_daily_pct", 0d);
+					empty.put("profit_share_ratio", 0d);
+					empty.put("id", "");
+					empty.put("img", "");
+					empty.put("img_path", "");
+					resultObject.setData(empty);
+					return resultObject;
+				}
+				data = traderService.findByPartyId(partyId);
+			}
+			Map<String, Object> ret = new HashMap<>();
+			ret.put("exists", data != null);
+			ret.put("checked", data == null ? null : data.getChecked());
+			ret.put("can_reapply", data != null && data.getChecked() == -1);
+			ret.put("reject_reason", data != null && data.getChecked() == -1 ? data.getRemarks() : "");
+			ret.put("name", data == null ? "" : data.getName());
+			ret.put("symbols", data == null ? "" : data.getSymbols());
+			ret.put("follow_volumn_min", data == null || data.getFollowVolumnMin() == null ? 0d : data.getFollowVolumnMin().doubleValue());
+			if (data != null) {
+				ret.put("follow_commission_type", FollowCommissionType.normalizeOrLegacy(data.getFollowCommissionType()));
+				ret.put("follow_commission_monthly_amount",
+						data.getFollowCommissionMonthlyAmount() == null ? "0"
+								: data.getFollowCommissionMonthlyAmount().stripTrailingZeros().toPlainString());
+				ret.put("follow_commission_daily_pct", Arith.mul(data.getFollowCommissionDailyPct(), 100));
+				ret.put("profit_share_ratio", Arith.mul(data.getProfitShareRatio(), 100));
+			} else {
+				ret.put("follow_commission_type", FollowCommissionType.LEGACY);
+				ret.put("follow_commission_monthly_amount", "0");
+				ret.put("follow_commission_daily_pct", 0d);
+				ret.put("profit_share_ratio", 0d);
+			}
+			ret.put("id", data == null ? "" : data.getUuid());
+			if (data != null && !StringUtils.isEmptyString(data.getImg())) {
+				ret.put("img", Constants.WEB_URL + "/public/showimg!showImg.action?imagePath=" + data.getImg());
+				ret.put("img_path", data.getImg());
+			} else {
+				ret.put("img", "");
+				ret.put("img_path", "");
+			}
+			resultObject.setData(ret);
+			resultObject.setCode("0");
+			return resultObject;
+		} catch (Exception e) {
+			logger.error("istrader error", e);
 			resultObject.setCode("1");
-			resultObject.setData(false);
+			resultObject.setMsg(e.getMessage() != null ? e.getMessage() : "程序错误");
+			Map<String, Object> err = new HashMap<>();
+			err.put("exists", false);
+			err.put("checked", null);
+			err.put("can_reapply", false);
+			err.put("reject_reason", "");
+			err.put("name", "");
+			err.put("symbols", "");
+			err.put("follow_volumn_min", 0d);
+			err.put("follow_commission_type", FollowCommissionType.LEGACY);
+			err.put("follow_commission_monthly_amount", "0");
+			err.put("follow_commission_daily_pct", 0d);
+			err.put("profit_share_ratio", 0d);
+			err.put("id", "");
+			err.put("img", "");
+			err.put("img_path", "");
+			resultObject.setData(err);
 			return resultObject;
 		}
-
-		resultObject.setData(true);
-		resultObject.setCode("0");
-		return resultObject;
 	}
 
-	@RequestMapping(action + "get.action")
+	/**
+	 * 交易员详情(本人或指定 id)。同时注册 {@code /api/trader/get},避免网关对 {@code !} 路径返回 404。
+	 */
+	@RequestMapping(value = { action + "get.action", "/api/trader/get" })
 	public Object get(HttpServletRequest request) {
 		ResultObject resultObject = new ResultObject();
 		String id = request.getParameter("id");
@@ -187,16 +278,27 @@
 			}
 			Page<Trader> page = new Page<>(1, 1000000);
 			Trader data = null;
-			if(StringUtils.isNotEmpty(id)) {
+			if (StringUtils.isNotEmpty(id)) {
 				data = traderService.findById(id);
-			} else{
+				if (null == data) {
+					resultObject.setCode("1");
+					resultObject.setMsg("交易员不存在");
+					return resultObject;
+				}
+			} else {
 				String partyId = SecurityUtils.getCurrentUserId();
+				if (StringUtils.isEmptyString(partyId)) {
+					resultObject.setCode("1");
+					resultObject.setMsg("用户未登录");
+					return resultObject;
+				}
 				data = traderService.findByPartyId(partyId);
-			}
-			if(null == data) {
-				resultObject.setCode("1");
-				resultObject.setMsg("交易员不存在");
-				return resultObject;
+				if (null == data) {
+					resultObject.setCode("1");
+					/** 未传 id 时按当前登录用户查 T_TRADER;无记录表示从未申请或数据未写入,与「传错 uuid」区分 */
+					resultObject.setMsg("当前账号暂无交易员记录,请先提交申请或确认登录账号是否正确");
+					return resultObject;
+				}
 			}
 
 			Map<String, Object> retData = bulidData(data, type, symbol, page);
@@ -345,21 +447,86 @@
 		return remain;
 	}
 
+	/** 带单品种分隔:分号、逗号(含全角) */
+	private static final Pattern TRADER_SYMBOL_SPLIT = Pattern.compile("[;;,,]+");
+
+	/**
+	 * 校验带单品种是否在系统合约品种(Item)中存在;支持多品种(与 T_TRADER.SYMBOLS 约定一致,用分号拼接规范 symbol)。
+	 * 匹配顺序:symbol(含缓存/remarks 映射)→ 小写 symbol → 展示名 name(兼容旧数据)。
+	 */
+	private String validateAndNormalizeTraderSymbols(String raw) throws BusinessException {
+		if (StringUtils.isEmptyString(raw)) {
+			throw new BusinessException("请输入带单品种");
+		}
+		String trimmed = raw.trim();
+		String[] tokens = TRADER_SYMBOL_SPLIT.split(trimmed);
+		LinkedHashSet<String> seen = new LinkedHashSet<>();
+		List<String> normalized = new ArrayList<>();
+		for (String token : tokens) {
+			if (token == null) {
+				continue;
+			}
+			String part = token.trim();
+			if (part.isEmpty()) {
+				continue;
+			}
+			Item item = itemService.findBySymbol(part);
+			if (item == null) {
+				item = itemService.findBySymbol(part.toLowerCase(Locale.ROOT));
+			}
+			if (item == null) {
+				item = itemService.lambdaQuery()
+						.eq(Item::getName, part)
+						.eq(BaseEntity::getDelFlag, 0)
+						.last("LIMIT 1")
+						.one();
+			}
+			if (item == null || StringUtils.isEmptyString(item.getSymbol())) {
+				throw new BusinessException("带单品种无效或不支持:" + part);
+			}
+			String canon = item.getSymbol();
+			if (seen.add(canon)) {
+				normalized.add(canon);
+			}
+		}
+		if (normalized.isEmpty()) {
+			throw new BusinessException("请输入带单品种");
+		}
+		return String.join(";", normalized);
+	}
+
 	@RequestMapping(action + "apply.action")
 	public Object apply(HttpServletRequest request) {
 		ResultObject resultObject = new ResultObject();
 		String partyId = SecurityUtils.getCurrentUserId();
 
 		String symbols = request.getParameter("symbols");
+		if (symbols != null) {
+			symbols = symbols.trim();
+		}
 		String name = request.getParameter("name");
 
 		String follow_volumn_min_param = request.getParameter("follow_volumn_min");
-		int follow_volumn_min = StringUtils.isEmptyString(follow_volumn_min_param)?0:Integer.parseInt(follow_volumn_min_param);
-
 		String state = "1";
 		String img = request.getParameter("img");
+		String remarks = request.getParameter("remarks");
 
 		try {
+			BigDecimal follow_volumn_min = BigDecimal.ZERO;
+			if (!StringUtils.isEmptyString(follow_volumn_min_param)) {
+				try {
+					follow_volumn_min = new BigDecimal(follow_volumn_min_param.trim());
+				} catch (NumberFormatException e) {
+					throw new BusinessException("最小跟单币数量格式不正确");
+				}
+			}
+			if (follow_volumn_min.signum() < 0) {
+				throw new BusinessException("最小跟单币数量不能小于0");
+			}
+			if (StringUtils.isEmptyString(remarks)) {
+				throw new BusinessException("交易员简介不能为空");
+			}
+
 			User party = userService.findByUserId(partyId);
 
 			if (party == null) {
@@ -368,24 +535,87 @@
 			if (Constants.SECURITY_ROLE_TEST.equals(party.getRoleName())) {
 				throw new BusinessException("试用用户无法添加!");
 			}
+			symbols = validateAndNormalizeTraderSymbols(symbols);
 			Trader exist = traderService.findByPartyId(partyId);
-			if (exist != null) {
-				throw new BusinessException("交易员已存在!");
-
+			if (exist != null && exist.getChecked() != -1) {
+				throw new BusinessException("Trader application already exists!");
 			}
-			Trader trader = new Trader();
-			trader.setUuid(ApplicationUtil.getCurrentTimeUUID());
-			trader.setPartyId(partyId);
+			Trader trader = exist == null ? new Trader() : exist;
+			if (exist == null) {
+				trader.setUuid(ApplicationUtil.getCurrentTimeUUID());
+				trader.setPartyId(partyId);
+				trader.setCreateTime(new Date());
+			}
 			trader.setName(StringUtils.isEmptyString(name)?party.getUserName():name);
 			trader.setSymbols(symbols);
-
 			trader.setState(state);
-			trader.setCreateTime(new Date());
 			trader.setImg(img);
-			trader.setFollowVolumnMin(follow_volumn_min);
+			trader.setRemarks(remarks);
+			trader.setFollowVolumnMin(follow_volumn_min.stripTrailingZeros());
 			trader.setChecked(0);
 
-			traderService.save(trader);
+			String fcType = request.getParameter("follow_commission_type");
+			String fcMonthly = request.getParameter("follow_commission_monthly_amount");
+			String fcDaily = request.getParameter("follow_commission_daily_pct");
+			String normalizedType = FollowCommissionType.normalizeOrLegacy(fcType);
+			trader.setFollowCommissionType(normalizedType);
+			if (FollowCommissionType.isMonthlyFixed(normalizedType)) {
+				BigDecimal m = BigDecimal.ZERO;
+				if (!StringUtils.isEmptyString(fcMonthly)) {
+					try {
+						m = new BigDecimal(fcMonthly.trim());
+					} catch (NumberFormatException e) {
+						throw new BusinessException("月跟单费金额格式不正确");
+					}
+				}
+				if (m.compareTo(BigDecimal.ZERO) <= 0) {
+					throw new BusinessException("按月跟单模式须填写大于 0 的月费用");
+				}
+				trader.setFollowCommissionMonthlyAmount(m);
+				trader.setFollowCommissionDailyPct(0D);
+			} else if (FollowCommissionType.isDailyProfitPct(normalizedType)) {
+				double pct = 0D;
+				if (!StringUtils.isEmptyString(fcDaily)) {
+					try {
+						pct = Double.parseDouble(fcDaily.trim());
+					} catch (NumberFormatException e) {
+						throw new BusinessException("按日利润提成比例格式不正确");
+					}
+				}
+				if (pct <= 0 || pct > 100) {
+					throw new BusinessException("按日利润提成比例须在 0~100 之间");
+				}
+				trader.setFollowCommissionDailyPct(Arith.div(pct, 100));
+				trader.setFollowCommissionMonthlyAmount(BigDecimal.ZERO);
+			} else {
+				trader.setFollowCommissionMonthlyAmount(BigDecimal.ZERO);
+				trader.setFollowCommissionDailyPct(0D);
+			}
+
+			String profitShareParam = request.getParameter("profit_share_ratio");
+			if (FollowCommissionType.isLegacy(normalizedType)) {
+				if (StringUtils.isEmptyString(profitShareParam)) {
+					throw new BusinessException("单笔盈利分成须填写利润分成比例(0~100,单位:%)");
+				}
+				double psPct;
+				try {
+					psPct = Double.parseDouble(profitShareParam.trim());
+				} catch (NumberFormatException e) {
+					throw new BusinessException("利润分成比例格式不正确");
+				}
+				if (psPct <= 0 || psPct > 100) {
+					throw new BusinessException("利润分成比例须在 0~100 之间(单位:%)");
+				}
+				trader.setProfitShareRatio(Arith.div(psPct, 100));
+			} else {
+				trader.setProfitShareRatio(0D);
+			}
+
+			if (exist == null) {
+				traderService.save(trader);
+			} else {
+				traderService.update(trader);
+			}
 
 
 
@@ -394,6 +624,9 @@
 		} catch (BusinessException e) {
 			resultObject.setCode("1");
 			resultObject.setMsg(e.getMessage());
+		} catch (NumberFormatException e) {
+			resultObject.setCode("1");
+			resultObject.setMsg("最小跟单币数量格式不正确");
 		} catch (Throwable t) {
 			logger.error("UserAction.register error ", t);
 			resultObject.setCode("1");
@@ -411,13 +644,9 @@
 	@RequestMapping("showfollowsetting.action")
 	public Object show_follow_setting(HttpServletRequest request){
 		ResultObject resultObject = new ResultObject();
-		String partyId = SecurityUtils.getCurrentUserId();
-
-		TraderFollowSetting traderFollowSetting = traderFollowSettingService.findByPartyId(partyId);
-
-		resultObject.setCode("0");
-		resultObject.setMsg("设置成功");
-		resultObject.setData(traderFollowSetting);
+		resultObject.setCode("1");
+		resultObject.setMsg("跟单利息设置功能已下线");
+		resultObject.setData(null);
 		return resultObject;
 	}
 
@@ -429,46 +658,8 @@
 	@RequestMapping("followsetting.action")
 	public Object follow_setting(HttpServletRequest request){
 		ResultObject resultObject = new ResultObject();
-		String partyId = SecurityUtils.getCurrentUserId();
-
-		User user = userService.findByUserId(partyId);
-
-		String days_setting= request.getParameter("days_setting");
-
-		if(StringUtils.isEmptyString(days_setting)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("借款天数不能为空");
-			return resultObject;
-		}
-
-		String rate = request.getParameter("rate");
-
-		if(StringUtils.isEmptyString(rate)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("带单佣金比例不能为空");
-			return resultObject;
-		}
-
-		if(!StringUtils.isDouble(rate)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("带单佣金比例格式不正确");
-			return resultObject;
-		}
-
-		if(null != traderFollowSettingService.findByPartyId(partyId)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("带单设置已存在");
-			return resultObject;
-		}
-
-		TraderFollowSetting traderFollowSetting = new TraderFollowSetting();
-		traderFollowSetting.setPartyId(partyId);
-		traderFollowSetting.setUsername(user.getUserName());
-		traderFollowSetting.setDaysSetting(days_setting);
-		traderFollowSetting.setRate(Double.parseDouble(rate));
-
-		resultObject.setCode("0");
-		resultObject.setMsg("设置成功");
+		resultObject.setCode("1");
+		resultObject.setMsg("跟单利息设置功能已下线");
 		return resultObject;
 	}
 
@@ -480,54 +671,8 @@
 	@RequestMapping("updatefollowsetting.action")
 	public Object update_follow_setting(HttpServletRequest request){
 		ResultObject resultObject = new ResultObject();
-		String partyId = SecurityUtils.getCurrentUserId();
-
-		String id = request.getParameter("id");
-
-		String days_setting= request.getParameter("days_setting");
-
-		if(StringUtils.isEmptyString(id)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("更改记录ID不能为空");
-			return resultObject;
-		}
-
-		if(StringUtils.isEmptyString(days_setting)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("借款天数不能为空");
-			return resultObject;
-		}
-
-		String rate = request.getParameter("rate");
-
-		if(StringUtils.isEmptyString(rate)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("带单佣金比例不能为空");
-			return resultObject;
-		}
-
-		if(!StringUtils.isDouble(rate)) {
-			resultObject.setCode("1");
-			resultObject.setMsg("带单佣金比例格式不正确");
-			return resultObject;
-		}
-
-		TraderFollowSetting traderFollowSetting = traderFollowSettingService.findById(id);
-
-		if(null == traderFollowSetting) {
-			resultObject.setCode("1");
-			resultObject.setMsg("记录不存在");
-			return resultObject;
-		}
-
-
-		traderFollowSetting.setDaysSetting(days_setting);
-		traderFollowSetting.setRate(Double.parseDouble(rate));
-
-		traderFollowSettingService.update(traderFollowSetting);
-
-		resultObject.setCode("0");
-		resultObject.setMsg("设置成功");
+		resultObject.setCode("1");
+		resultObject.setMsg("跟单利息设置功能已下线");
 		return resultObject;
 	}
 
@@ -570,10 +715,6 @@
 			return "当前跟随人数加偏差值不能小于0";
 		if (profit_share_ratio < 0.0D)
 			return "利润分成比例不能小于0";
-		if (follower_max <= 0)
-			return "此次跟单最多跟随人数不能小于等于0";
-		if (follower_max < Arith.add(follower_now, deviation_follower_now))
-			return "此次跟单最多跟随人数不能小于当前跟随人数加偏差值";
 		if (follow_volumn_min < 0)
 			return "最小跟单张数不能小于0";
 		return null;
@@ -621,7 +762,8 @@
 			/**
 			 * 查询类型 orders 当前委托 ,hisorders 历史委托 ,user 跟随者
 			 */
-			trader_order = this.contractOrderService.getPaged(Long.valueOf(page.getCurrent()).intValue(), Long.valueOf(page.getSize()).intValue(), entity.getPartyId(), symbol, type, null);
+			trader_order = this.contractOrderService.buildDataFromOrders(
+					this.contractOrderService.findSubmittedTraderOwn(entity.getPartyId(), symbol));
 		} else if("hisorders".equals(type)) {
 			trader_order = this.traderOrderService.getPaged(page, entity.getPartyId());
 		}
@@ -666,13 +808,20 @@
 
 		map.put("name", entity.getName());
 		map.put("remarks", entity.getRemarks());
+		/** 带单品种(与 T_TRADER.SYMBOLS 一致;交易员广场列表用 symbol_name,此处双字段避免前端遗漏) */
+		String symbolsVal = StringUtils.isEmptyString(entity.getSymbols()) ? "" : entity.getSymbols();
+		map.put("symbols", symbolsVal);
+		map.put("symbol_name", symbolsVal);
+		/** 带单开关:0 停止 1 开启 2 禁止(与后台交易员管理一致) */
+		map.put("state", StringUtils.isEmptyString(entity.getState()) ? "" : entity.getState());
 		/**
 		 * 累计金额order_amount
 		 */
 		map.put("order_amount", df2.format(Arith.add(entity.getOrderAmount(), entity.getDeviationOrderAmount())));
 
 //		map.put("symbol_name", "BTC/USDT;ETH/USDT");
-		map.put("profit", df2.format(Arith.add(entity.getProfit(), entity.getDeviationProfit())));
+		double historyProfitTotal = contractOrderService.historyProfitForTraderTotalYield(entity);
+		map.put("profit", df2.format(historyProfitTotal));
 
 		map.put("order_profit", (int) Arith.add(entity.getOrderProfit(), entity.getDeviationOrderProfit()));
 
@@ -688,8 +837,41 @@
 		map.put("profit_ratio", df2.format(Arith.add(Arith.mul(entity.getDeviationProfitRatio(), 100),
 				Arith.mul(entity.getProfitRatio(), 100))));
 
+		// 与广场 list 一致:历史=合约表已平仓全品种盈亏+偏差;分母优先已平仓保证金合计(与当前持仓 deposit 一致)
+		double historyAmountTotal = contractOrderService.historyAmountBasisForTraderTotalYield(entity);
+		double openProfitAgg = 0D;
+		double openDepositAgg = 0D;
+		List<ContractOrder> openTraderOwn = contractOrderService.findSubmittedTraderOwn(entity.getPartyId(), "");
+		if (openTraderOwn != null) {
+			for (ContractOrder one : openTraderOwn) {
+				if (one == null) {
+					continue;
+				}
+				contractOrderService.wrapProfit(one);
+				openProfitAgg = Arith.add(openProfitAgg, one.getProfit() == null ? 0D : one.getProfit().doubleValue());
+				openDepositAgg = Arith.add(openDepositAgg, one.getDeposit() == null ? 0D : one.getDeposit().doubleValue());
+			}
+		}
+		double totalProfitAgg = Arith.add(historyProfitTotal, openProfitAgg);
+		double totalAmountAgg = Arith.add(historyAmountTotal, openDepositAgg);
+		double totalProfitRatioAgg = 0D;
+		if (totalAmountAgg > 0D) {
+			totalProfitRatioAgg = Arith.mul(Arith.div(totalProfitAgg, totalAmountAgg), 100);
+		}
+		map.put("total_profit", df2.format(totalProfitAgg));
+		map.put("total_profit_ratio", df2.format(totalProfitRatioAgg));
+
 		map.put("profit_share_ratio", Arith.mul(entity.getProfitShareRatio(), 100));
+		map.put("follow_commission_type", FollowCommissionType.normalizeOrLegacy(entity.getFollowCommissionType()));
+		map.put("follow_commission_monthly_amount",
+				entity.getFollowCommissionMonthlyAmount() == null ? "0" : entity.getFollowCommissionMonthlyAmount().stripTrailingZeros().toPlainString());
+		map.put("follow_commission_daily_pct", Arith.mul(entity.getFollowCommissionDailyPct(), 100));
 		map.put("follower_max", entity.getFollowerMax());
+		if (entity.getCreateTime() != null) {
+			map.put("create_time", DateUtils.format(entity.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
+		} else {
+			map.put("create_time", "");
+		}
 		Date date_now = new Date();// 取时单
 		int days = daysBetween(entity.getCreateTime(), date_now);
 		if (days < 0) {
@@ -714,28 +896,177 @@
 			TraderFollowUser user = this.traderFollowUserService
 					.findByPartyIdAndTrader_partyId(partyId, entity.getPartyId());
 			if (user != null) {
-
+				map.put("follow_relation_exists", true);
 				map.put("follow_volume", user.getVolume());
 				map.put("follow_volume_max", user.getVolumeMax());
-				/**
-				 * 跟单固定张数/固定比例---选择 1,固定张数,固定比例
-				 */
 				map.put("follow_type", user.getFollowType());
-				map.put("follow_state", "1");
-			} else {
+				map.put("follow_state", user.getState());
+				map.put("follow_symbol", StringUtils.isEmptyString(user.getSymbol()) ? "" : user.getSymbol());
+				map.put("follow_lever_rate", user.getLeverRate());
+				String followFailReason = StringUtils.isEmptyString(user.getFailReason()) ? "" : user.getFailReason();
+				map.put("follow_fail_reason", followFailReason);
+				map.put("follow_fail_reason_key", followFailReasonKeyI18n(followFailReason));
+				map.put("follow_last_fail_time", user.getLastFailTime());
+				map.put("follow_user_cumulative_profit", df2.format(user.getProfit()));
+				double cumAmt = user.getAmountSum();
+				double cumYieldPct = 0D;
+				if (cumAmt > 0D) {
+					cumYieldPct = Arith.mul(Arith.div(user.getProfit(), cumAmt), 100);
+				}
+				map.put("follow_user_cumulative_yield_pct", df2.format(cumYieldPct));
 
+				// 是否已有成功开仓(仅当前 submitted 持仓算成功) + 当前跟单持仓浮盈亏合计
+				boolean followOpenSuccess = false;
+				String followOpenDirection = "";
+				String followOpenTime = "";
+				String followOpenOrderNo = "";
+				double followMyOpenProfit = 0D;
+				double followMyOpenDeposit = 0D;
+				List<TraderFollowUserOrder> userFollowOrders = traderFollowUserOrderService
+						.findByPartyIdAndTraderPartyIdAndState(partyId, entity.getPartyId(), ContractOrder.STATE_SUBMITTED);
+				if (userFollowOrders != null && !userFollowOrders.isEmpty()) {
+					TraderFollowUserOrder latest = userFollowOrders.get(0);
+					for (TraderFollowUserOrder one : userFollowOrders) {
+						if (one != null && !StringUtils.isEmptyString(one.getUserOrderNo())) {
+							ContractOrder co = contractOrderService.findByOrderNo(one.getUserOrderNo());
+							if (co != null && ContractOrder.STATE_SUBMITTED.equals(co.getState())) {
+								contractOrderService.wrapProfit(co);
+								followMyOpenProfit = Arith.add(followMyOpenProfit,
+										co.getProfit() == null ? 0D : co.getProfit().doubleValue());
+								followMyOpenDeposit = Arith.add(followMyOpenDeposit,
+										co.getDeposit() == null ? 0D : co.getDeposit().doubleValue());
+							}
+						}
+						if (one != null && one.getCreateTime() != null && latest != null
+								&& latest.getCreateTime() != null && one.getCreateTime().after(latest.getCreateTime())) {
+							latest = one;
+						}
+					}
+					if (latest != null && !StringUtils.isEmptyString(latest.getUserOrderNo())) {
+						ContractOrder latestUserOrder = contractOrderService.findByOrderNo(latest.getUserOrderNo());
+						if (latestUserOrder != null && ContractOrder.STATE_SUBMITTED.equals(latestUserOrder.getState())) {
+							followOpenSuccess = true;
+							followOpenDirection = StringUtils.isEmptyString(latestUserOrder.getDirection()) ? ""
+									: latestUserOrder.getDirection();
+							if (latestUserOrder.getCreateTime() != null) {
+								followOpenTime = DateUtils.format(latestUserOrder.getCreateTime(), "yyyy-MM-dd HH:mm:ss");
+							}
+							followOpenOrderNo = latestUserOrder.getOrderNo();
+						}
+					}
+				}
+				map.put("follow_open_success", followOpenSuccess);
+				map.put("follow_open_direction", followOpenDirection);
+				map.put("follow_open_time", followOpenTime);
+				map.put("follow_open_order_no", followOpenOrderNo);
+				map.put("follow_my_open_profit", df2.format(followMyOpenProfit));
+				map.put("follow_my_open_deposit", df2.format(followMyOpenDeposit));
+				double followMyOpenYieldPct = 0D;
+				if (followMyOpenDeposit > 0D) {
+					followMyOpenYieldPct = Arith.mul(Arith.div(followMyOpenProfit, followMyOpenDeposit), 100);
+				}
+				map.put("follow_my_open_yield_pct", df2.format(followMyOpenYieldPct));
+				map.put("follow_monthly_remaining_days",
+						computeFollowMonthlyRemainingDays(entity.getFollowCommissionType(), user));
+			} else {
+				map.put("follow_relation_exists", false);
 				map.put("follow_state", "2");
+				map.put("follow_fail_reason", "");
+				map.put("follow_fail_reason_key", "");
+				map.put("follow_last_fail_time", null);
+				map.put("follow_open_success", false);
+				map.put("follow_open_direction", "");
+				map.put("follow_open_time", "");
+				map.put("follow_open_order_no", "");
+				map.put("follow_user_cumulative_profit", df2.format(0D));
+				map.put("follow_user_cumulative_yield_pct", df2.format(0D));
+				map.put("follow_my_open_profit", df2.format(0D));
+				map.put("follow_my_open_deposit", df2.format(0D));
+				map.put("follow_my_open_yield_pct", df2.format(0D));
+				map.put("follow_monthly_remaining_days", null);
 			}
 
 		} else {
 			map.put("follow_state", "2");
+			map.put("follow_relation_exists", false);
+			map.put("follow_fail_reason", "");
+			map.put("follow_fail_reason_key", "");
+			map.put("follow_user_cumulative_profit", df2.format(0D));
+			map.put("follow_user_cumulative_yield_pct", df2.format(0D));
+			map.put("follow_my_open_profit", df2.format(0D));
+			map.put("follow_my_open_deposit", df2.format(0D));
+			map.put("follow_my_open_yield_pct", df2.format(0D));
+			map.put("follow_monthly_remaining_days", null);
 			map.remove("partyId");
 		}
-		map.put("follow_volumn_min", entity.getFollowVolumnMin());
+		map.put("follow_volumn_min", entity.getFollowVolumnMin() == null ? 0d : entity.getFollowVolumnMin().doubleValue());
+		/** 审核状态:0 待审、1 通过、-1 驳回(供前端在 istrader 异常时从 get 兜底) */
+		map.put("checked", entity.getChecked());
 		return map;
 
 	}
 
+	/** 与 ApiTraderUserController 一致:供 H5/PC i18n 映射跟单失败原因 */
+	private static String followFailReasonKeyI18n(String reason) {
+		if (reason == null) {
+			return "";
+		}
+		String r = reason.trim();
+		if (r.isEmpty()) {
+			return "";
+		}
+		if (r.contains("余额不足")) {
+			return "INSUFFICIENT_BALANCE";
+		}
+		if (r.contains("只能选择交易员带单币种")) {
+			return "SYMBOL_NOT_IN_TRADER_LIST";
+		}
+		if (r.contains("跟单参数输入错误")) {
+			return "INVALID_FOLLOW_PARAMS";
+		}
+		if (r.contains("Insufficient balance") || r.contains("insufficient balance")) {
+			return "INSUFFICIENT_BALANCE";
+		}
+		return "";
+	}
+
+	/**
+	 * 月固定跟单费:当前用户对带单员已缴本月费用时,返回本月剩余自然日(含当天);否则 null。
+	 */
+	private Integer computeFollowMonthlyRemainingDays(Object traderCommissionTypeObj, TraderFollowUser user) {
+		if (user == null) {
+			return null;
+		}
+		String typeNorm = traderCommissionTypeObj == null ? FollowCommissionType.LEGACY
+				: FollowCommissionType.normalizeOrLegacy(traderCommissionTypeObj.toString());
+		if (!FollowCommissionType.isMonthlyFixed(typeNorm)) {
+			return null;
+		}
+		String st = user.getState();
+		if (!(TraderFollowUser.STATE_FOLLOWING.equals(st) || TraderFollowUser.STATE_STOPPING.equals(st)
+				|| TraderFollowUser.STATE_FAILED.equals(st))) {
+			return null;
+		}
+		String paid = user.getMonthlyFeePaidPeriod();
+		if (StringUtils.isEmptyString(paid)) {
+			return null;
+		}
+		try {
+			ZoneId z = ZoneId.systemDefault();
+			YearMonth paidYm = YearMonth.parse(paid);
+			YearMonth nowYm = YearMonth.now(z);
+			if (!paidYm.equals(nowYm)) {
+				return null;
+			}
+			LocalDate today = LocalDate.now(z);
+			LocalDate end = nowYm.atEndOfMonth();
+			long days = ChronoUnit.DAYS.between(today, end) + 1L;
+			return (int) Math.max(0L, days);
+		} catch (Exception e) {
+			return null;
+		}
+	}
+
 	public static int daysBetween(Date smdate, Date bdate) throws ParseException {
 		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
 		smdate = sdf.parse(sdf.format(smdate));

--
Gitblit v1.9.3