1
zj
yesterday befbf57e4112d07003bff18102f556a1e5a154de
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));