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-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowDailyPnl.java                    |   38 
 trading-order-bean/src/main/java/com/yami/trading/bean/user/dto/MoneyLogDto.java                                  |    3 
 trading-order-admin/src/main/java/com/yami/trading/admin/controller/contract/ContractOrderController.java         |    3 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java       |  120 +
 trading-order-service/src/main/resources/mapper/contract/ContractOrderMapper.xml                                  |   14 
 trading-order-service/src/main/java/com/yami/trading/service/notify/WalletTransactionMailNotificationService.java |   28 
 trading-order-bean/src/main/java/com/yami/trading/bean/contract/dto/TraderOwnClosedAggDTO.java                    |   18 
 docs/db/V4__follow_rebuild_schema.sql                                                                             |    8 
 trading-order-admin/src/main/java/com/yami/trading/admin/controller/user/FollowMoneyLogController.java            |    4 
 trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowDailyPnlMapper.java                   |    9 
 docs/db/V5__follow_leverage_support.sql                                                                           |    2 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiContractOrderController.java                 |   13 
 trading-order-admin/src/main/java/com/yami/trading/admin/controller/MoneyLogController.java                       |    4 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java                      |   11 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderUserController.java             |  139 ++
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java    |  428 +++++---
 trading-order-service/src/main/java/com/yami/trading/service/trader/FollowCommissionService.java                  |   31 
 docs/db/V7__follow_failure_record.sql                                                                             |    3 
 trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java                   |  153 +-
 trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java                        |   86 +
 trading-order-service/src/main/java/com/yami/trading/service/user/impl/QRGenerateServiceImpl.java                 |   37 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java                 |  623 +++++++++--
 trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java              |   54 
 trading-order-service/src/main/resources/mapper/trader/TraderFollowUserMapper.xml                                 |    1 
 trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java                   |   14 
 trading-order-admin/src/main/java/com/yami/trading/admin/task/FollowDailyPnlSettleJob.java                        |   33 
 trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserOrderService.java             |   17 
 trading-order-service/src/main/java/com/yami/trading/dao/contract/ContractOrderMapper.java                        |    6 
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java                   |   84 +
 trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserService.java                  |   21 
 docs/db/V3__trader_follow_volumn_min_decimal.sql                                                                  |    2 
 trading-order-common/src/main/java/com/yami/trading/common/constants/Constants.java                               |    4 
 .cursor/plans/跟单功能重构_d09cf09a.plan.md                                                                             |  143 ++
 trading-order-service/src/main/resources/mapper/trader/TraderOrderMapper.xml                                      |    4 
 trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml                                           |    7 
 trading-order-service/src/main/resources/mapper/trader/TraderFollowUserOrderMapper.xml                            |   18 
 trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java             |  123 ++
 trading-order-service/src/main/java/com/yami/trading/service/impl/InternalEmailSenderServiceImpl.java             |   31 
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/FollowCommissionServiceImpl.java         |  203 +++
 trading-order-admin/src/main/java/com/yami/trading/admin/model/trader/TraderModel.java                            |   14 
 docs/db/V6__follow_commission.sql                                                                                 |   26 
 trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowUserOrderMapper.java                  |    2 
 trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiFollowWalletController.java           |   12 
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/AdminTraderServiceImpl.java              |    7 
 trading-order-bean/src/main/java/com/yami/trading/bean/trader/FollowCommissionType.java                           |   39 
 trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/Trader.java                                  |   27 
 trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserServiceImpl.java         |  391 ++++++-
 47 files changed, 2,470 insertions(+), 588 deletions(-)

diff --git "a/.cursor/plans/\350\267\237\345\215\225\345\212\237\350\203\275\351\207\215\346\236\204_d09cf09a.plan.md" "b/.cursor/plans/\350\267\237\345\215\225\345\212\237\350\203\275\351\207\215\346\236\204_d09cf09a.plan.md"
new file mode 100644
index 0000000..8e86328
--- /dev/null
+++ "b/.cursor/plans/\350\267\237\345\215\225\345\212\237\350\203\275\351\207\215\346\236\204_d09cf09a.plan.md"
@@ -0,0 +1,143 @@
+---
+name: 跟单功能重构
+overview: 重构跟单功能以对齐普通合约交易口径:按币数量下单、开平仓完整计入手续费与资金费、引入跟单员申请审核流程、补齐实时持仓与跟单关系展示,并将停止跟单设计为异步全平以支持高并发场景。
+todos:
+  - id: align-follow-qty
+    content: 把跟单配置与执行口径统一为币数量,完成接口校验和旧字段兼容设计
+    status: pending
+  - id: refactor-follow-executor
+    content: 重构跟单开平仓派生执行器,按普通合约流程复用费用与结算,并加入幂等与批量异步编排
+    status: pending
+  - id: async-stop-follow
+    content: 实现停止跟单异步全平、状态流转、失败重试与结果查询
+    status: pending
+  - id: trader-audit-square
+    content: 完善跟单员申请审核与广场可见性过滤,只展示审核通过的跟单员
+    status: pending
+  - id: positions-and-views
+    content: 补齐跟单用户与跟单员两侧的实时持仓和跟单关系查询接口
+    status: pending
+  - id: verify-regressions
+    content: 验证费用结算、钱包扣退、审核过滤、停止跟单和高并发跟单场景
+    status: pending
+isProject: false
+---
+
+# 跟单功能重构计划
+
+## 目标
+
+- 跟单下单与普通合约完全对齐:使用币数量口径,走同一套保证金、手续费、资金费、持仓结算规则。
+- 跟单员必须先申请、后台审核通过后,才能在跟单广场展示并触发带单逻辑。
+- 跟单用户和跟单员都能看到实时持仓与跟单关系;停止跟单采用异步全平,避免大量用户同时平仓阻塞接口。
+- 在高并发跟单场景下,重构执行编排,避免当前串行循环造成堆积或重复问题。
+
+## 现状结论
+
+- 普通合约下单接口 `OpenAction.amount` 已按“币数量”口径接收,`ApiContractApplyOrderController.open` 直接写入 `ContractApplyOrder.volume`。
+- 当前跟单配置存的是 `TraderFollowUser.volume/volumeMax`,语义仍偏“张数/比例”,需要改成“币数量最小值/最大值”并在跟单创建、修改、执行时统一校验。
+- 跟单开仓/平仓目前是在 `ContractOrderService` 成交后触发 `TraderFollowUserOrderServiceImpl.traderOpen/traderClose`,但实现偏逐个用户串行生成委托。
+- 跟单员申请/审核基础已存在:`ApiTraderController.apply` 写 `Trader.checked=0`,`AdminTraderController.check` 审核;但广场查询侧仍需强制只展示 `checked=1`。
+
+## 改造范围
+
+- 交易主链路
+  - [trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java](trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java)
+  - [trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java](trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java)
+  - [trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java](trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java)
+- 跟单配置与关系
+  - [trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java](trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java)
+  - [trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java](trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java)
+  - [trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java](trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java)
+- 跟单员申请审核与广场
+  - [trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java](trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderController.java)
+  - [trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java](trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java)
+  - [trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java](trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java)
+  - [trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml](trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml)
+
+## 方案设计
+
+### 1. 统一跟单数量口径为币数量
+
+- 将 `TraderFollowUser.volume`、`volumeMax` 的业务语义从“张数/比例”调整为“最小跟单币数量/最大跟单币数量”。
+- API `save.action`、`changeFollow.action` 增加区间校验:最小值 > 0,最大值 >= 最小值。
+- 跟单执行时不再基于“固定张数/比例”推导,而是读取该跟随者配置的币数量直接生成 `ContractApplyOrder.volume` / `volumeOpen`,和普通合约 `open.action` 保持一致。
+- 对已有 `followType` 做兼容处理:如果前端和数据库已大量依赖,可先保留字段但废弃“比例跟单”逻辑,仅支持固定币数量模式;旧数据迁移为固定数量。
+
+### 2. 跟单开平仓对齐普通合约费用与结算
+
+- 跟单开仓继续复用 `ContractApplyOrderService.saveCreate/open`,保证金、手续费、资金费、平仓结算统一走普通合约规则。
+- 重点核对并修正跟单相关分支,确保:
+  - 开仓从跟单钱包扣保证金和手续费。
+  - 平仓时释放保证金、计入平仓手续费和资金费,并回写跟单钱包。
+  - 跟单撤单或停止跟单引发的待成交单取消,也走跟单钱包退款,不混用普通钱包。
+- 对 `TraderFollowUserOrder` 增加足够字段,确保能追踪“跟单关系 + 跟随委托单/持仓单 + 状态 + 停止来源”,支持后续异步全平和重试。
+
+### 3. 跟单员申请审核与广场展示
+
+- 复用现有 `Trader.checked` 状态,不另起新审核框架。
+- 用户申请成为跟单员时写入申请资料并置 `checked=0`;后台审核后更新 `checked`。
+- 跟单广场和所有可跟随交易员查询统一只返回 `checked=1` 且 `state=1` 的交易员。
+- 带单触发处继续使用 `findByPartyIdAndChecked(...,1)` 兜底,避免未审核用户即使手工构造数据也能触发跟单。
+
+### 4. 实时持仓与关系展示
+
+- 跟单用户侧:提供“我当前跟随的交易员 + 当前跟单持仓 + 跟单状态(跟随中/停止中/已停止)”查询。
+- 跟单员侧:提供“我下面有多少跟单用户、每个用户当前持仓、汇总跟单人数/持仓币数量”查询。
+- 查询尽量基于现有 `ContractOrder.follow`、`TraderFollowUser`、`TraderFollowUserOrder` 组合装配,避免新建重复持仓表;确有缺口时只补必要索引或状态字段。
+
+### 5. 停止跟单改为异步全平
+
+- `cancelFollow.action` 改为:
+  - 先把 `TraderFollowUser.state` 置为“停止中”。
+  - 收集该用户在该交易员下的未平跟单持仓,异步投递平仓任务。
+  - 任务逐笔创建平仓委托,复用现有合约平仓链路。
+  - 全部完成后置为“已停止”;失败记录明细并支持重试。
+- 接口返回立即成功,并让前端通过状态查询看到“处理中”。
+
+### 6. 高并发保护与执行编排
+
+- 当前 `TraderFollowUserOrderServiceImpl.traderOpen/traderClose` 为逐用户串行处理,需要重构为“批量拉取跟单关系 + 分片生成任务 + 统一入异步队列”。
+- 保持真正的订单撮合与持仓结算仍走现有合约链路,但将“跟单派生任务”从同步循环改为异步批量编排,降低交易员成交线程阻塞。
+- 增加以下保护:
+  - 跟单开平仓任务幂等键:`traderOrderNo + followerPartyId + actionType`
+  - 跟单停止任务幂等键:`followRelationId + stopBatchNo`
+  - 必要的状态流转:跟随中 -> 停止中 -> 已停止
+  - 对高频同用户任务增加更细粒度锁,避免重复创建委托
+
+## 建议数据流
+
+```mermaid
+flowchart TD
+    traderFilled[TraderFilled] --> dispatchFollow[DispatchFollowTasks]
+    dispatchFollow --> followTaskQueue[FollowTaskQueue]
+    followTaskQueue --> buildApplyOrder[BuildFollowerApplyOrder]
+    buildApplyOrder --> contractCreate[ContractApplyOrderService.saveCreate]
+    contractCreate --> contractQueue[NEW_CONTRACT_APPLY_ORDERS]
+    contractQueue --> contractHandle[ContractApplyOrderHandleJob]
+    contractHandle --> saveOpenClose[ContractOrderService.saveOpenOrSaveClose]
+
+    userStopFollow[UserStopFollow] --> markStopping[MarkFollowStopping]
+    markStopping --> stopTaskQueue[StopFollowTaskQueue]
+    stopTaskQueue --> closeFollowerOrders[CreateFollowerCloseOrders]
+    closeFollowerOrders --> contractQueue
+    closeFollowerOrders --> markStopped[MarkFollowStopped]
+```
+
+
+
+## 实施顺序
+
+1. 先重构跟单配置模型和接口校验,把“币数量最小/最大值”口径固定下来。
+2. 再重构跟单开仓/平仓执行器,使其按币数量直接生成跟单委托,并补齐幂等与异步编排。
+3. 然后补停止跟单异步全平链路和状态流转。
+4. 最后收口跟单员申请审核、广场展示过滤和持仓/跟单关系查询接口。
+5. 补充针对关键交易链路的回归测试或最小可验证用例,重点覆盖费用结算、停止跟单、审核过滤和批量跟单任务幂等。
+
+## 风险与验证重点
+
+- 跟单数量口径切换后,历史 `TraderFollowUser` 数据需兼容;需要明确旧“比例跟单”是否全部废弃并做一次迁移。
+- 跟单撤单、停止跟单、任务失败重试时,必须确认退款/回滚全部走跟单钱包。
+- 当前普通合约处理队列吞吐较低,若跟单用户量大,仅重构跟单编排仍可能受下游单线程消费限制;必要时需要同步评估 `ContractApplyOrderHandleJob` 的消费能力。
+- 广场查询要同时修复 `TraderServiceImpl.getPaged` 过滤条件未生效的问题,避免审核状态和展示结果不一致。
+
diff --git a/docs/db/V3__trader_follow_volumn_min_decimal.sql b/docs/db/V3__trader_follow_volumn_min_decimal.sql
new file mode 100644
index 0000000..abb9d3e
--- /dev/null
+++ b/docs/db/V3__trader_follow_volumn_min_decimal.sql
@@ -0,0 +1,2 @@
+-- 交易员「最小跟单币数量」支持小数(MySQL;其他库请按需改写类型)
+ALTER TABLE T_TRADER MODIFY COLUMN FOLLOW_VOLUMN_MIN DECIMAL(32, 16) NOT NULL DEFAULT 0 COMMENT '跟单最小币数量';
diff --git a/docs/db/V4__follow_rebuild_schema.sql b/docs/db/V4__follow_rebuild_schema.sql
new file mode 100644
index 0000000..4a0d9aa
--- /dev/null
+++ b/docs/db/V4__follow_rebuild_schema.sql
@@ -0,0 +1,8 @@
+ALTER TABLE T_TRADER_FOLLOW_USER
+    ADD COLUMN INVEST_AMOUNT DECIMAL(32, 16) NOT NULL DEFAULT 0 COMMENT '跟单投入币数量',
+    ADD COLUMN STOP_REQUEST_TIME BIGINT NULL COMMENT '请求停止跟单时间戳(秒)',
+    ADD COLUMN STOP_FINISH_TIME BIGINT NULL COMMENT '停止跟单完成时间戳(秒)';
+
+ALTER TABLE T_TRADER_FOLLOW_USER
+    MODIFY COLUMN STOP_PFOFIT DOUBLE NOT NULL DEFAULT 0 COMMENT '已废弃:跟单止盈百分比',
+    MODIFY COLUMN STOP_LOSS DOUBLE NOT NULL DEFAULT 0 COMMENT '已废弃:跟单止损百分比';
diff --git a/docs/db/V5__follow_leverage_support.sql b/docs/db/V5__follow_leverage_support.sql
new file mode 100644
index 0000000..274a51d
--- /dev/null
+++ b/docs/db/V5__follow_leverage_support.sql
@@ -0,0 +1,2 @@
+ALTER TABLE T_TRADER_FOLLOW_USER
+    ADD COLUMN LEVER_RATE DECIMAL(32, 16) NOT NULL DEFAULT 1 COMMENT '跟随者自定义杠杆倍数';
diff --git a/docs/db/V6__follow_commission.sql b/docs/db/V6__follow_commission.sql
new file mode 100644
index 0000000..2870237
--- /dev/null
+++ b/docs/db/V6__follow_commission.sql
@@ -0,0 +1,26 @@
+-- 跟单佣金:LEGACY 逐笔盈利分成 | MONTHLY_FIXED 月固定 | DAILY_PROFIT_PCT 按自然日汇总已实现盈亏后抽成
+ALTER TABLE T_TRADER
+    ADD COLUMN FOLLOW_COMMISSION_TYPE VARCHAR(32) NOT NULL DEFAULT 'LEGACY' COMMENT '跟单佣金类型',
+    ADD COLUMN FOLLOW_COMMISSION_MONTHLY_AMOUNT DECIMAL(32, 16) NOT NULL DEFAULT 0 COMMENT '月固定跟单费(USDT)',
+    ADD COLUMN FOLLOW_COMMISSION_DAILY_PCT DOUBLE NOT NULL DEFAULT 0 COMMENT '按日总盈利提成比例 0~1';
+
+ALTER TABLE T_TRADER_FOLLOW_USER
+    ADD COLUMN MONTHLY_FEE_PAID_PERIOD VARCHAR(7) NULL COMMENT '已缴月费周期 yyyy-MM';
+
+CREATE TABLE IF NOT EXISTS T_TRADER_FOLLOW_DAILY_PNL (
+    UUID VARCHAR(64) NOT NULL PRIMARY KEY COMMENT '主键',
+    PARTY_ID VARCHAR(64) NOT NULL COMMENT '跟随者',
+    TRADER_PARTY_ID VARCHAR(64) NOT NULL COMMENT '带单员',
+    PNL_DATE DATE NOT NULL COMMENT '盈亏归属自然日(平仓时间, 服务器默认时区)',
+    REALIZED_PROFIT_SUM DECIMAL(32, 16) NOT NULL DEFAULT 0 COMMENT '当日已实现盈亏合计(可负)',
+    SETTLED TINYINT NOT NULL DEFAULT 0 COMMENT '0未结算 1已结算',
+    COMMISSION_AMOUNT DECIMAL(32, 16) NOT NULL DEFAULT 0 COMMENT '实际抽取给带单员的金额',
+    CREATE_TIME DATETIME NULL,
+    UPDATE_TIME DATETIME NULL,
+    CREATE_TIME_TS BIGINT NULL,
+    UPDATE_TIME_TS BIGINT NULL,
+    CREATE_BY VARCHAR(64) NULL,
+    UPDATE_BY VARCHAR(64) NULL,
+    DEL_FLAG TINYINT NOT NULL DEFAULT 0,
+    UNIQUE KEY UK_FOLLOW_DAILY (PARTY_ID, TRADER_PARTY_ID, PNL_DATE)
+) COMMENT='跟单按日已实现盈亏汇总(用于日提成)';
diff --git a/docs/db/V7__follow_failure_record.sql b/docs/db/V7__follow_failure_record.sql
new file mode 100644
index 0000000..1adf238
--- /dev/null
+++ b/docs/db/V7__follow_failure_record.sql
@@ -0,0 +1,3 @@
+ALTER TABLE T_TRADER_FOLLOW_USER
+    ADD COLUMN FAIL_REASON VARCHAR(255) NULL COMMENT '最近一次跟单失败原因',
+    ADD COLUMN LAST_FAIL_TIME BIGINT NULL COMMENT '最近一次跟单失败时间戳(秒)';
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/MoneyLogController.java b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/MoneyLogController.java
index bd96204..200cb6d 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/MoneyLogController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/MoneyLogController.java
@@ -53,6 +53,10 @@
         page.getRecords().forEach(moneyLogDto -> {
             moneyLogDto.setCategoryText(Constants.MONEYLOG_CATEGORY.get(moneyLogDto.getCategory()));
             moneyLogDto.setRoleNameText(Constants.ROLE_MAP.get(moneyLogDto.getRoleName()));
+            if (!StrUtil.isEmpty(moneyLogDto.getContentType())) {
+                String ct = Constants.MONEYLOG_CONTENT.get(moneyLogDto.getContentType());
+                moneyLogDto.setContentTypeText(StrUtil.isEmpty(ct) ? moneyLogDto.getContentType() : ct);
+            }
             if (!StrUtil.isEmpty(moneyLogDto.getSymbol())){
                 moneyLogDto.setSymbol(moneyLogDto.getSymbol().toUpperCase());
             }
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/contract/ContractOrderController.java b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/contract/ContractOrderController.java
index a93231f..5d29b45 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/contract/ContractOrderController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/contract/ContractOrderController.java
@@ -35,7 +35,6 @@
 import com.yami.trading.bean.contract.domain.ContractOrder;
 
 import com.yami.trading.bean.contract.dto.ContractOrderDTO;
-import com.yami.trading.bean.contract.mapstruct.ContractOrderWrapper;
 import com.yami.trading.service.contract.ContractOrderService;
 import com.yami.trading.bean.contract.query.ContractOrderQuery;
 
@@ -63,8 +62,6 @@
     @Autowired
     private ContractOrderService contractOrderService;
 
-    @Autowired
-    private ContractOrderWrapper contractOrderWrapper;
     @Autowired(required = false)
     @Qualifier("dataService")
     private DataService dataService;
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java
index 3ccb69d..e4cfd50 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/trader/AdminTraderController.java
@@ -5,6 +5,7 @@
 import com.yami.trading.admin.model.trader.TraderModel;
 import com.yami.trading.bean.contract.domain.ContractOrder;
 import com.yami.trading.bean.model.User;
+import com.yami.trading.bean.trader.FollowCommissionType;
 import com.yami.trading.bean.trader.domain.Trader;
 import com.yami.trading.common.constants.Constants;
 import com.yami.trading.common.domain.Result;
@@ -22,6 +23,7 @@
 
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
+import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
@@ -55,6 +57,11 @@
 					Double.parseDouble(data.get("deviation_profit_ratio").toString())), 100));
 			data.put("profit_share_ratio",
 					Arith.mul(Double.parseDouble(data.get("profit_share_ratio").toString()), 100));
+			Object dailyPct = data.get("follow_commission_daily_pct");
+			if (dailyPct != null && !"".equals(String.valueOf(dailyPct))) {
+				data.put("follow_commission_daily_pct",
+						Arith.mul(Double.parseDouble(dailyPct.toString()), 100));
+			}
 			data.put("follower_now", Arith.add(Double.parseDouble(data.get("follower_now").toString()),
 					Double.parseDouble(data.get("deviation_follower_now").toString())));
 			data.put("follower_sum", Arith.add(Double.parseDouble(data.get("follower_sum").toString()),
@@ -125,7 +132,7 @@
 
 		int follower_max = model.getFollowerMax();
 
-		int follow_volumn_min = model.getFollowVolumnMin();
+		double follow_volumn_min = model.getFollowVolumnMin();
 
 		double week_3_profit = model.getWeek3Profit();
 
@@ -209,12 +216,21 @@
 			trader.setDeviationFollowerSum(deviation_follower_sum);
 			trader.setDeviationFollowerNow(deviation_follower_now);
 
-			trader.setProfitShareRatio(Arith.div(profit_share_ratio, 100));
 			trader.setState(state);
 			trader.setFollowerMax(follower_max);
 			trader.setCreateTime(sdf.parse(create_time));
 			trader.setImg(img);
-			trader.setFollowVolumnMin(follow_volumn_min);
+			trader.setFollowVolumnMin(BigDecimal.valueOf(follow_volumn_min));
+			bindTraderFollowCommissionFromModel(trader, model);
+			if (FollowCommissionType.isLegacy(FollowCommissionType.normalizeOrLegacy(trader.getFollowCommissionType()))) {
+				trader.setProfitShareRatio(Arith.div(profit_share_ratio, 100));
+			} else {
+				trader.setProfitShareRatio(0D);
+			}
+			String fcErr = validateFollowCommission(trader);
+			if (!StringUtils.isNullOrEmpty(fcErr)) {
+				throw new BusinessException(fcErr);
+			}
 			trader.setChecked(1);
 
 			adminTraderService.save(trader);
@@ -296,6 +312,7 @@
 		}
 		trader.setProfitRatio(Arith.mul(trader.getProfitRatio(), 100));
 		trader.setProfitShareRatio(Arith.mul(trader.getProfitShareRatio(), 100));
+		trader.setFollowCommissionDailyPct(Arith.mul(trader.getFollowCommissionDailyPct(), 100));
 		trader.setWeek3ProfitRatio(Arith.mul(trader.getWeek3ProfitRatio(), 100));
 		trader.setDeviationProfitRatio(Arith.mul(trader.getDeviationWeek3ProfitRatio(), 100));
 		trader.setDeviationProfitRatio(Arith.mul(trader.getDeviationProfitRatio(), 100));
@@ -394,7 +411,7 @@
 
 		int follower_max = model.getFollowerMax();
 
-		int follow_volumn_min = model.getFollowVolumnMin();
+		double follow_volumn_min = model.getFollowVolumnMin();
 
 		double week_3_profit = model.getWeek3Profit();
 
@@ -466,10 +483,20 @@
 
 			trader.setState(state);
 			trader.setFollowerMax(follower_max);
-			trader.setProfitShareRatio(Arith.div(profit_share_ratio, 100));
 			trader.setCreateTime(sdf.parse(create_time));
 			trader.setImg(img);
-			trader.setFollowVolumnMin(follow_volumn_min);
+			trader.setFollowVolumnMin(BigDecimal.valueOf(follow_volumn_min));
+
+			bindTraderFollowCommissionFromModel(trader, model);
+			if (FollowCommissionType.isLegacy(FollowCommissionType.normalizeOrLegacy(trader.getFollowCommissionType()))) {
+				trader.setProfitShareRatio(Arith.div(profit_share_ratio, 100));
+			} else {
+				trader.setProfitShareRatio(0D);
+			}
+			String fcErrUpdate = validateFollowCommission(trader);
+			if (!StringUtils.isNullOrEmpty(fcErrUpdate)) {
+				throw new BusinessException(fcErrUpdate);
+			}
 
 			adminTraderService.update(trader);
 			return Result.succeed(null,"更新成功!");
@@ -489,6 +516,7 @@
 	public Result check(HttpServletRequest request) {
 		String id = request.getParameter("uuid");
 		String check = request.getParameter("check");
+		String reason = request.getParameter("reason");
 		try {
 			if(StringUtils.isEmptyString(check)) {
 				return Result.failed("1","审核参数不能为空");
@@ -500,6 +528,12 @@
 			if(Integer.parseInt(check) == trader.getChecked()) {
 				return Result.failed("1","该记录审核状跟提交状态一致");
 			}
+			if(Integer.parseInt(check) == -1) {
+				if(StringUtils.isEmptyString(reason)) {
+					return Result.failed("1","驳回原因不能为空");
+				}
+				trader.setRemarks(reason);
+			}
             trader.setChecked(Integer.parseInt(check));
 			adminTraderService.update(trader);
 			return Result.succeed(null,"审核成功!");
@@ -507,6 +541,33 @@
 			return Result.failed("1", e.getMessage());
 		} catch (Throwable t) {
 			logger.error("update error ", t);
+			return Result.failed("1", t.getMessage());
+		}
+	}
+
+	@RequestMapping(action + "updateState.action")
+	public Result updateState(HttpServletRequest request) {
+		String id = request.getParameter("uuid");
+		String state = request.getParameter("state");
+		try {
+			if (StringUtils.isEmptyString(id)) {
+				return Result.failed("1", "交易员ID不能为空");
+			}
+			if (StringUtils.isEmptyString(state)) {
+				return Result.failed("1", "带单状态不能为空");
+			}
+			if (!"0".equals(state) && !"1".equals(state) && !"2".equals(state)) {
+				return Result.failed("1", "带单状态非法");
+			}
+			Trader trader = adminTraderService.findById(id);
+			if (trader == null) {
+				return Result.failed("1", "该记录不存在");
+			}
+			trader.setState(state);
+			adminTraderService.update(trader);
+			return Result.succeed(null, "带单状态更新成功");
+		} catch (Throwable t) {
+			logger.error("updateState error", t);
 			return Result.failed("1", t.getMessage());
 		}
 	}
@@ -533,7 +594,7 @@
 	private String verification(String name, String img, String create_time, String symbols, int order_profit, int deviation_order_profit, int order_loss
 			, int deviation_order_loss, double week_3_order_amount, double deviation_week_3_order_amount, double week_3_order_profit, double deviation_week_3_order_profit,
 			int week_3_order_sum, int deviation_week_3_order_sum, double order_amount, double deviation_order_amount, int follower_sum, int deviation_follower_sum,
-			int follower_now, int deviation_follower_now, double profit_share_ratio, int follower_max, int follow_volumn_min) {
+			int follower_now, int deviation_follower_now, double profit_share_ratio, int follower_max, double follow_volumn_min) {
 		
 		
 		if (StringUtils.isEmptyString(name))
@@ -572,14 +633,52 @@
 			return "当前跟随人数加偏差值不能小于0";
 		if (profit_share_ratio < 0.0D)
 			return "利润分成比例不能小于0";
-		if (follower_max <= 0)
-			return "此次跟单最多跟随人数不能小于等于0";
 		if (StringUtils.isEmptyString(img))
 			return "请上传头像";
-		if (follower_max < Arith.add(follower_now, deviation_follower_now))
-			return "此次跟单最多跟随人数不能小于当前跟随人数加偏差值";
 		if (follow_volumn_min < 0)
-			return "最小跟单张数不能小于0";
+			return "最小跟单币数量不能小于0";
+		return null;
+	}
+
+	private void bindTraderFollowCommissionFromModel(Trader trader, TraderModel model) {
+		if (model.getFollowCommissionType() != null && !model.getFollowCommissionType().trim().isEmpty()) {
+			trader.setFollowCommissionType(FollowCommissionType.normalizeOrLegacy(model.getFollowCommissionType()));
+		} else if (trader.getFollowCommissionType() == null || trader.getFollowCommissionType().trim().isEmpty()) {
+			trader.setFollowCommissionType(FollowCommissionType.LEGACY);
+		}
+		if (model.getFollowCommissionMonthlyAmount() != null) {
+			trader.setFollowCommissionMonthlyAmount(model.getFollowCommissionMonthlyAmount());
+		}
+		if (model.getFollowCommissionDailyPct() != null) {
+			trader.setFollowCommissionDailyPct(Arith.div(model.getFollowCommissionDailyPct(), 100));
+		}
+		if (FollowCommissionType.isLegacy(trader.getFollowCommissionType())) {
+			trader.setFollowCommissionMonthlyAmount(BigDecimal.ZERO);
+			trader.setFollowCommissionDailyPct(0);
+		} else if (FollowCommissionType.isMonthlyFixed(trader.getFollowCommissionType())) {
+			trader.setFollowCommissionDailyPct(0);
+		} else if (FollowCommissionType.isDailyProfitPct(trader.getFollowCommissionType())) {
+			trader.setFollowCommissionMonthlyAmount(BigDecimal.ZERO);
+		}
+	}
+
+	private String validateFollowCommission(Trader t) {
+		String ct = FollowCommissionType.normalizeOrLegacy(t.getFollowCommissionType());
+		t.setFollowCommissionType(ct);
+		if (FollowCommissionType.isMonthlyFixed(ct)) {
+			if (t.getFollowCommissionMonthlyAmount() == null
+					|| t.getFollowCommissionMonthlyAmount().compareTo(BigDecimal.ZERO) <= 0) {
+				return "月固定跟单费须大于0";
+			}
+		} else if (FollowCommissionType.isDailyProfitPct(ct)) {
+			if (t.getFollowCommissionDailyPct() <= 0 || t.getFollowCommissionDailyPct() > 1) {
+				return "按日总盈利提成比例须在0到100之间(按百分比填写)";
+			}
+		} else if (FollowCommissionType.isLegacy(ct)) {
+			if (t.getProfitShareRatio() <= 0 || t.getProfitShareRatio() > 1) {
+				return "单笔盈利分成比例须在0到100之间(表单按百分比填写)";
+			}
+		}
 		return null;
 	}
 
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/user/FollowMoneyLogController.java b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/user/FollowMoneyLogController.java
index 0162160..8144496 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/admin/controller/user/FollowMoneyLogController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/controller/user/FollowMoneyLogController.java
@@ -53,6 +53,10 @@
         page.getRecords().forEach(moneyLogDto -> {
             moneyLogDto.setCategoryText(Constants.MONEYLOG_CATEGORY.get(moneyLogDto.getCategory()));
             moneyLogDto.setRoleNameText(Constants.ROLE_MAP.get(moneyLogDto.getRoleName()));
+            if (!StrUtil.isEmpty(moneyLogDto.getContentType())) {
+                String ct = Constants.MONEYLOG_CONTENT.get(moneyLogDto.getContentType());
+                moneyLogDto.setContentTypeText(StrUtil.isEmpty(ct) ? moneyLogDto.getContentType() : ct);
+            }
             if (!StrUtil.isEmpty(moneyLogDto.getSymbol())){
                 moneyLogDto.setSymbol(moneyLogDto.getSymbol().toUpperCase());
             }
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/model/trader/TraderModel.java b/trading-order-admin/src/main/java/com/yami/trading/admin/model/trader/TraderModel.java
index b47e74a..8fc5542 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/admin/model/trader/TraderModel.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/model/trader/TraderModel.java
@@ -4,6 +4,7 @@
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.util.Date;
 
 @Data
@@ -75,11 +76,20 @@
     @ApiModelProperty("利润分成比例")
     private double profitShareRatio;
 
+    @ApiModelProperty("跟单佣金类型 LEGACY/MONTHLY_FIXED/DAILY_PROFIT_PCT")
+    private String followCommissionType;
+
+    @ApiModelProperty("月固定跟单费(USDT)")
+    private BigDecimal followCommissionMonthlyAmount;
+
+    @ApiModelProperty("按日总盈利提成比例(百分比 0~100)")
+    private Double followCommissionDailyPct;
+
     @ApiModelProperty("此次跟单最多跟随人数")
     private int followerMax;
 
-    @ApiModelProperty("跟单最小下单数")
-    private int followVolumnMin;
+    @ApiModelProperty("跟单最小下单数(支持小数)")
+    private double followVolumnMin;
 
     @ApiModelProperty("备注")
     private String remarks;
diff --git a/trading-order-admin/src/main/java/com/yami/trading/admin/task/FollowDailyPnlSettleJob.java b/trading-order-admin/src/main/java/com/yami/trading/admin/task/FollowDailyPnlSettleJob.java
new file mode 100644
index 0000000..9a9ba59
--- /dev/null
+++ b/trading-order-admin/src/main/java/com/yami/trading/admin/task/FollowDailyPnlSettleJob.java
@@ -0,0 +1,33 @@
+package com.yami.trading.admin.task;
+
+import com.yami.trading.service.trader.FollowCommissionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.ZoneId;
+
+/**
+ * 按自然日结算跟单「日盈利百分比」提成(处理跨天:每日一行,定时任务结算前一自然日)
+ */
+@Component
+@Slf4j
+public class FollowDailyPnlSettleJob {
+
+	@Resource
+	private FollowCommissionService followCommissionService;
+
+	/** 每日 00:30 结算上一自然日(与 JVM 默认时区一致,WebApplication 已设为 GMT+8) */
+	@Scheduled(cron = "0 30 0 * * ?")
+	public void settleYesterday() {
+		LocalDate yesterday = LocalDate.now(ZoneId.systemDefault()).minusDays(1);
+		try {
+			int n = followCommissionService.settleDailyPnlForDate(yesterday);
+			log.info("FollowDailyPnlSettleJob settled date={} rows={}", yesterday, n);
+		} catch (Exception e) {
+			log.error("FollowDailyPnlSettleJob failed date={}", yesterday, e);
+		}
+	}
+}
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiContractOrderController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiContractOrderController.java
index 898fd8a..66aa96b 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiContractOrderController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiContractOrderController.java
@@ -82,6 +82,10 @@
     @RequestMapping(action + "close.action")
     public Result<String> close(@RequestParam String order_no) {
         try {
+            ContractOrder order = contractOrderService.findByOrderNo(order_no);
+            if (order != null && ContractOrder.ORDER_FOLLOW == order.getFollow()) {
+                return Result.failed("跟单订单不支持手动平仓,请先停止跟单");
+            }
             // 建议使用线程池 TODO
             CloseDelayThread lockDelayThread = new CloseDelayThread(SecurityUtils.getCurrentUserId(), order_no, this.contractOrderService, false);
             Thread t = new Thread(lockDelayThread);
@@ -100,6 +104,15 @@
     @RequestMapping(action + "closeAll.action")
     public Result<String> closeAll() {
         try {
+            String partyId = SecurityUtils.getCurrentUserId();
+            List<ContractOrder> submittedOrders = contractOrderService.findSubmitted(partyId);
+            if (submittedOrders != null) {
+                for (ContractOrder one : submittedOrders) {
+                    if (ContractOrder.ORDER_FOLLOW == one.getFollow()) {
+                        return Result.failed("包含跟单订单,不能手动一键平仓,请先停止跟单");
+                    }
+                }
+            }
             CloseDelayThread lockDelayThread = new CloseDelayThread(SecurityUtils.getCurrentUserId(), "", this.contractOrderService, true);
             Thread t = new Thread(lockDelayThread);
             t.start();
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java
index 7ac8952..68b221d 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/ApiWithdrawController.java
@@ -222,8 +222,15 @@
 
             Withdraw withdraw = withdrawService.getOne(new LambdaQueryWrapper<>(Withdraw.class)
                     .eq(Withdraw::getOrderNo, order_no).last(" limit 1 "));
-            if(ObjectUtil.isEmpty(withdraw) && withdraw.getStatus() != 0 && !withdraw.getAddress().equals(address)){
-                log.info("withdraw failed:{}", withdraw);
+            if (ObjectUtil.isEmpty(withdraw)) {
+                log.warn("withdrawCallback ignore: order not found, orderNo={}, address={}", order_no, address);
+                resultMsg.setCode(200);
+                return resultMsg;
+            }
+            // 已处理状态或地址不匹配时忽略回调,避免重复处理/误处理
+            if (withdraw.getStatus() != 0 || !address.equals(withdraw.getAddress())) {
+                log.info("withdrawCallback ignore: orderNo={}, status={}, callbackAddress={}, orderAddress={}",
+                        order_no, withdraw.getStatus(), address, withdraw.getAddress());
                 resultMsg.setCode(200);
                 return resultMsg;
             }
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiFollowWalletController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiFollowWalletController.java
index a722ffa..df6a95e 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiFollowWalletController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiFollowWalletController.java
@@ -134,12 +134,14 @@
 //            followWallet.setMoney(followWallet.getMoney().add(amount));
 
             walletService.updateMoney(Constants.WALLET_USDT, partyId, BigDecimal.ZERO.subtract(amount), BigDecimal.ZERO, Constants.MONEYLOG_TRANSFER_IN,
-                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_IN, "现货账户转入跟单账户,订单号[" + walletTransferLog.getOrderNo() + "]");
+                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_IN,
+                    "[跟单账变]现货主钱包出账|去向:独立跟单账户|划转单号[" + walletTransferLog.getOrderNo() + "]");
 //            followWalletService.updateMoney(Constants.WALLET_USDT, partyId, BigDecimal.ZERO.add(amount), BigDecimal.ZERO, Constants.MONEYLOG_TRANSFER_IN,
 //                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_IN, "现货账户转入跟单账户,订单号[" + walletTransferLog.getOrderNo() + "]");
 //            followWalletService.update(followWallet, Wrappers.<FollowWallet>lambdaUpdate().eq(FollowWallet::getUserId, partyId));
             followWalletService.updateMoney(Constants.WALLET_USDT, partyId, amount, BigDecimal.ZERO, Constants.MONEYLOG_TRANSFER_IN,
-                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_IN, "现货账户转入跟单账户,订单号[" + walletTransferLog.getOrderNo() + "]");
+                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_IN,
+                    "[跟单账变]独立跟单账户入账|来源:现货主钱包划转|划转单号[" + walletTransferLog.getOrderNo() + "]");
         } else {
             if(followWallet.getMoney().compareTo(amount) < 0) {
                 throw new BusinessException("跟单账户余额不足!");
@@ -148,9 +150,11 @@
 //            wallet.setMoney(wallet.getMoney().add(amount));
 
             walletService.updateMoney(Constants.WALLET_USDT, partyId, amount, BigDecimal.ZERO, Constants.MONEYLOG_TRANSFER_OUT,
-                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_OUT, "跟单账户转入现货账户,订单号[" + walletTransferLog.getOrderNo() + "]");
+                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_OUT,
+                    "[跟单账变]现货主钱包入账|来源:独立跟单账户划出|划转单号[" + walletTransferLog.getOrderNo() + "]");
             followWalletService.updateMoney(Constants.WALLET_USDT, partyId, BigDecimal.ZERO.subtract(amount), BigDecimal.ZERO, Constants.MONEYLOG_TRANSFER_OUT,
-                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_OUT, "跟单账户转入现货账户,订单号[" + walletTransferLog.getOrderNo() + "]");
+                    Constants.WALLET_USDT, Constants.MONEYLOG_TRANSFER_OUT,
+                    "[跟单账变]独立跟单账户出账|去向:现货主钱包|划转单号[" + walletTransferLog.getOrderNo() + "]");
         }
         walletTransferLogService.saveRecord(walletTransferLog);
         return Result.succeed("划转成功");
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));
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java
index 4f9b6e1..0ce6584 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderFollowUserController.java
@@ -6,6 +6,7 @@
 import com.yami.trading.common.exception.BusinessException;
 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.trader.TraderFollowUserService;
 import com.yami.trading.service.trader.TraderService;
 import com.yami.trading.service.user.UserService;
@@ -17,6 +18,7 @@
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 
 /**
  * 用户准备跟随交易员api接口
@@ -39,6 +41,9 @@
 
 	@Autowired
 	private UserService userService;
+
+	@Autowired
+	private ContractOrderService contractOrderService;
 
 //	private String trader_id;
 //
@@ -78,12 +83,9 @@
 	@RequestMapping(action + "save.action")
 	public Object saveCreate(HttpServletRequest request) {
 		ResultObject resultObject = new ResultObject();
-		String follow_type = request.getParameter("follow_type");
-		String stop_loss = request.getParameter("stop_loss");
-		String stop_profit = request.getParameter("stop_profit");
 		String symbol = request.getParameter("symbol");
 		String volume = request.getParameter("volume");
-		String volume_max = request.getParameter("volume_max");
+		String leverRate = request.getParameter("lever_rate");
 		String trader_id = request.getParameter("trader_id");
 
 		String partyId = SecurityUtils.getCurrentUserId();
@@ -110,18 +112,22 @@
 			entity.setPartyId(partyId);
 			entity.setUsername(user.getUserName());
 			/**
-			 * 跟单固定张数/固定比例---选择 1,固定张数量,固定比例
+			 * 当前仅支持固定币数量跟单
 			 */
-			entity.setFollowType(follow_type);
-			entity.setStopLoss(Double.parseDouble(stop_loss));
-			entity.setStopProfit(Double.parseDouble(stop_profit));
+			entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
+			entity.setStopLoss(0D);
+			entity.setStopProfit(0D);
 			entity.setSymbol(symbol);
-			entity.setVolume(Double.parseDouble(volume));
-			entity.setVolumeMax(Double.parseDouble(volume_max));
+			double investAmount = parsePositiveDouble(volume, "跟单投入币数量");
+			entity.setVolume(investAmount);
+			entity.setVolumeMax(investAmount);
+			entity.setInvestAmount(BigDecimal.valueOf(investAmount));
+			entity.setLeverRate(parsePositiveDoubleOrDefault(leverRate, 1D, "杠杆倍数"));
+			ensureTraderCannotFollow(partyId);
 			/**
-			 * 状态 是否还在跟随状随 1,跟随,取消跟随
+			 * 状态 1-跟随中
 			 */
-			entity.setState("1");
+			entity.setState(TraderFollowUser.STATE_FOLLOWING);
 
 			this.traderFollowUserService.save(entity, trader_id);
 			resultObject.setCode("0");
@@ -158,12 +164,9 @@
 	@RequestMapping(action + "changeFollow.action")
 	public Object changeFollow(HttpServletRequest request) {
 		ResultObject resultObject = new ResultObject();
-		String follow_type = request.getParameter("follow_type");
-		String stop_loss = request.getParameter("stop_loss");
-		String stop_profit = request.getParameter("stop_profit");
 		String symbol = request.getParameter("symbol");
 		String volume = request.getParameter("volume");
-		String volume_max = request.getParameter("volume_max");
+		String leverRate = request.getParameter("lever_rate");
 		String trader_id = request.getParameter("trader_id");
 
 		String partyId = SecurityUtils.getCurrentUserId();
@@ -177,7 +180,6 @@
 //				return resultObject;
 //			}
 
-			User user = userService.getById(partyId);
 //			if (!party.getKyc_authority()) {
 //				resultObject.setCode("401");
 //				resultObject.setMsg(error);
@@ -189,18 +191,22 @@
 			TraderFollowUser entity = this.traderFollowUserService.findByPartyIdAndTrader_partyId(partyId,
 					trader.getPartyId());
 			/**
-			 * 跟单固定张数/固定比例---选择 1,固定张数�?2,固定比�?
+			 * 当前仅支持固定币数量跟单
 			 */
-			entity.setFollowType(follow_type);
-			entity.setStopLoss(Double.parseDouble(stop_loss));
-			entity.setStopProfit(Double.parseDouble(stop_profit));
+			entity.setFollowType(TraderFollowUser.FOLLOW_TYPE_FIXED);
+			entity.setStopLoss(0D);
+			entity.setStopProfit(0D);
 			entity.setSymbol(symbol);
-			entity.setVolume(Double.parseDouble(volume));
-			entity.setVolumeMax(Double.parseDouble(volume_max));
+			double investAmount = parsePositiveDouble(volume, "跟单投入币数量");
+			entity.setVolume(investAmount);
+			entity.setVolumeMax(investAmount);
+			entity.setInvestAmount(BigDecimal.valueOf(investAmount));
+			entity.setLeverRate(parsePositiveDoubleOrDefault(leverRate, entity.getLeverRate() > 0 ? entity.getLeverRate() : 1D, "杠杆倍数"));
+			ensureTraderCannotFollow(partyId);
 			/**
-			 * 状�?? 是否还在跟随状�?? 1,跟随�?2,取消跟�?
+			 * 状态保持跟随中
 			 */
-			entity.setState("1");
+			entity.setState(TraderFollowUser.STATE_FOLLOWING);
 
 			this.traderFollowUserService.update(entity);
 			resultObject.setCode("0");
@@ -232,10 +238,11 @@
 			TraderFollowUser traderFollowUser = this.traderFollowUserService.findByPartyIdAndTrader_partyId(partyId,
 					trader.getPartyId().toString());
 			if (traderFollowUser != null) {
-				this.traderFollowUserService.deleteCancel(traderFollowUser.getUuid());
+				this.traderFollowUserService.cancelFollowAsync(traderFollowUser.getUuid(), contractOrderService);
 			}
 
 			resultObject.setCode("0");
+			resultObject.setMsg("已提交停止跟单请求");
 		} catch (BusinessException e) {
 			resultObject.setCode(e.getSign() + "");
 			resultObject.setMsg(e.getMessage());
@@ -250,4 +257,65 @@
 		return resultObject;
 	}
 
+	@RequestMapping(action + "status.action")
+	public Object status(HttpServletRequest request) {
+		ResultObject resultObject = new ResultObject();
+		String trader_id = request.getParameter("trader_id");
+		String partyId = SecurityUtils.getCurrentUserId();
+		try {
+			Trader trader = traderService.findById(trader_id);
+			TraderFollowUser relation = trader == null ? null
+					: traderFollowUserService.findByPartyIdAndTrader_partyId(partyId, trader.getPartyId());
+			resultObject.setCode("0");
+			resultObject.setData(relation);
+		} catch (Exception e) {
+			resultObject.setCode("1");
+			resultObject.setMsg("程序错误");
+			logger.error("error:", e.fillInStackTrace());
+		}
+		return resultObject;
+	}
+
+	@RequestMapping(action + "list.action")
+	public Object list() {
+		ResultObject resultObject = new ResultObject();
+		String partyId = SecurityUtils.getCurrentUserId();
+		try {
+			resultObject.setCode("0");
+			resultObject.setData(traderFollowUserService.findByPartyId(partyId));
+		} catch (Exception e) {
+			resultObject.setCode("1");
+			resultObject.setMsg("程序错误");
+			logger.error("error:", e.fillInStackTrace());
+		}
+		return resultObject;
+	}
+
+	private double parsePositiveDouble(String value, String fieldName) {
+		double parsed;
+		try {
+			parsed = Double.parseDouble(value == null ? "" : value.trim());
+		} catch (NumberFormatException ex) {
+			throw new BusinessException(1, fieldName + "格式不正确");
+		}
+		if (parsed <= 0) {
+			throw new BusinessException(1, fieldName + "必须大于0");
+		}
+		return parsed;
+	}
+
+	private double parsePositiveDoubleOrDefault(String value, double defaultVal, String fieldName) {
+		if (value == null || value.trim().isEmpty()) {
+			return defaultVal;
+		}
+		return parsePositiveDouble(value, fieldName);
+	}
+
+	private void ensureTraderCannotFollow(String partyId) {
+		Trader trader = traderService.findByPartyId(partyId);
+		if (trader != null && trader.getChecked() == 1) {
+			throw new BusinessException(1, "交易员身份用户不能跟单其他交易员");
+		}
+	}
+
 }
diff --git a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderUserController.java b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderUserController.java
index 3622def..9269697 100644
--- a/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderUserController.java
+++ b/trading-order-admin/src/main/java/com/yami/trading/api/controller/trader/ApiTraderUserController.java
@@ -1,10 +1,12 @@
 package com.yami.trading.api.controller.trader;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 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.trader.domain.Trader;
 import com.yami.trading.bean.trader.domain.TraderFollowUser;
+import com.yami.trading.bean.trader.domain.TraderFollowUserOrder;
 import com.yami.trading.bean.trader.domain.TraderUser;
 import com.yami.trading.common.constants.Constants;
 import com.yami.trading.common.exception.BusinessException;
@@ -20,7 +22,6 @@
 import com.yami.trading.service.trader.TraderUserService;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.apache.poi.ss.formula.functions.T;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -121,6 +122,7 @@
 		ResultObject resultObject = new ResultObject();
 		String type = request.getParameter("type");
 		String page_no = request.getParameter("page_no");
+		String page_size = request.getParameter("page_size");
 		try {
 			if (StringUtils.isNullOrEmpty(page_no)) {
 				page_no = "1";
@@ -131,7 +133,11 @@
 			if (Integer.valueOf(page_no).intValue() <= 0) {
 				throw new YamiShopBindException("页码不能小于等于0");
 			}
-			Page<T> page = new Page<>(1, 1000000);
+			int pageSize = 10;
+			if (!StringUtils.isNullOrEmpty(page_size) && StringUtils.isInteger(page_size)) {
+				pageSize = Math.max(1, Math.min(50, Integer.parseInt(page_size)));
+			}
+			Page<?> page = new Page<>(Integer.parseInt(page_no), pageSize);
 
 			String partyId = SecurityUtils.getCurrentUserId();
 
@@ -152,41 +158,92 @@
 
 	}
 
+	/** 交易员 SYMBOLS 可能为多品种,取第一个用于展示/查 Item */
+	private static String firstTraderSymbol(String symbolsRaw) {
+		if (symbolsRaw == null || symbolsRaw.trim().isEmpty()) {
+			return "";
+		}
+		for (String part : symbolsRaw.split("[;;,,\\s]+")) {
+			if (part != null && !part.trim().isEmpty()) {
+				return part.trim();
+			}
+		}
+		return "";
+	}
+
+	/**
+	 * 供前端 i18n 映射的常见跟单失败原因(存库仍为原文)。
+	 */
+	private static String followFailReasonKey(String reason) {
+		if (reason == null) {
+			return null;
+		}
+		String r = reason.trim();
+		if (r.isEmpty()) {
+			return null;
+		}
+		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 Map<String, Object> bulidData(TraderUser entity, String type, Page page) throws ParseException {
 
 		List<Map<String, Object>> trader_order = new ArrayList<Map<String, Object>>();
-		List<TraderFollowUser> follow_users = new ArrayList<TraderFollowUser>();
 
 		List<Map<String, Object>> follow_traders = new ArrayList<Map<String, Object>>();
-		follow_users = traderFollowUserService.findByPartyId(entity.getPartyId());
-		double folllow_trader_num = 0;
-		if (follow_users != null) {
-			folllow_trader_num = follow_users.size();
-		}
+		long folllow_trader_num = traderFollowUserService.countByPartyId(entity.getPartyId());
+		IPage<TraderFollowUser> traderUserPage = null;
 
 		/**
-		 * 跟随的交易员
+		 * 跟随的交易员(按更新时间倒序分页,最新在前)
 		 */
 		if ("trader".equals(type)) {
-			if (follow_users != null) {
-				for (TraderFollowUser user : follow_users) {
+			Page<TraderFollowUser> pg = new Page<>(page.getCurrent(), page.getSize());
+			traderUserPage = traderFollowUserService.pageByPartyId(pg, entity.getPartyId());
+			if (traderUserPage.getRecords() != null) {
+				for (TraderFollowUser user : traderUserPage.getRecords()) {
 					Trader trader = traderService.findByPartyId(user.getTraderPartyId());
-					Item item = itemService.findBySymbol(trader.getSymbols());
+					if (trader == null) {
+						continue;
+					}
+					String primarySymbol = firstTraderSymbol(trader.getSymbols());
+					Item item = StringUtils.isEmptyString(primarySymbol) ? null : itemService.findBySymbol(primarySymbol);
 					Map<String, Object> follow_trader = new HashMap<String, Object>();
 					follow_trader.put("profit", user.getProfit());
-					follow_trader.put("profitRation", BigDecimal.valueOf(user.getProfit()).divide(BigDecimal.valueOf(user.getAmountSum()), RoundingMode.HALF_UP));
+					BigDecimal profitRatio = BigDecimal.ZERO;
+					if (user.getAmountSum() > 0D) {
+						profitRatio = BigDecimal.valueOf(user.getProfit()).divide(BigDecimal.valueOf(user.getAmountSum()), 8,
+								RoundingMode.HALF_UP);
+					}
+					follow_trader.put("profitRation", profitRatio);
 					follow_trader.put("amountSum", user.getAmountSum());
 					follow_trader.put("username", trader.getName());
 					String path = Constants.WEB_URL + "/public/showimg!showImg.action?imagePath=" + trader.getImg();
 					follow_trader.put("img", path);
 					follow_trader.put("id", trader.getUuid());
-					follow_trader.put("followState", "1");
+					follow_trader.put("followState", user.getState());
+					follow_trader.put("followFailReason", user.getFailReason());
+					String failKey = followFailReasonKey(user.getFailReason());
+					follow_trader.put("follow_fail_reason_key", failKey != null ? failKey : "");
+					follow_trader.put("followLastFailTime", user.getLastFailTime());
 					follow_trader.put("followType", user.getFollowType());
 					follow_trader.put("volume", user.getVolume());
 					follow_trader.put("volumeMax", user.getVolumeMax());
+					follow_trader.put("lever_rate", user.getLeverRate());
 					follow_trader.put("followNow", trader.getFollowerNow());
 					follow_trader.put("followMax", trader.getFollowerMax());
-					follow_trader.put("symbols", item.getName());
+					follow_trader.put("symbols", item != null ? item.getName() : (StringUtils.isEmptyString(trader.getSymbols()) ? "" : trader.getSymbols()));
 					follow_traders.add(follow_trader);
 				}
 			}
@@ -202,6 +259,10 @@
 		map.put("orders", trader_order);
 		map.put("traders", follow_traders);
 		map.put("folllow_trader_num", folllow_trader_num);
+		map.put("traders_total", traderUserPage != null ? traderUserPage.getTotal() : folllow_trader_num);
+		if ("orders".equals(type) || "hisorders".equals(type)) {
+			map.put("orders_total", page.getTotal());
+		}
 
 		map.put("id", entity.getUuid());
 
@@ -219,6 +280,54 @@
 
 	}
 
+	/**
+	 * 当前跟单持仓列表。
+	 * 同时注册无 {@code !} 的路径,避免部分网关/WAF 对 {@code traderUser!positions.action} 返回 404。
+	 */
+	@RequestMapping(value = { action + "positions.action", "/api/traderUser/positions" })
+	public Object positions() {
+		ResultObject resultObject = new ResultObject();
+		String partyId = SecurityUtils.getCurrentUserId();
+		try {
+			List<ContractOrder> positions = contractOrderService.selectContractOrderByUserIdAndFollowAndState(
+					partyId, ContractOrder.ORDER_FOLLOW, ContractOrder.STATE_SUBMITTED);
+			List<Map<String, Object>> data = new ArrayList<>();
+			Map<String, String> followTraderNameCache = new HashMap<>();
+			Map<String, String> followTraderUuidCache = new HashMap<>();
+			if (positions != null) {
+				for (ContractOrder position : positions) {
+					Map<String, Object> row = contractOrderService.bulidOne(position);
+					String traderName = "";
+					String traderUuid = "";
+					if (position != null && !StringUtils.isEmptyString(position.getOrderNo())) {
+						TraderFollowUserOrder link = traderFollowUserOrderService.findByPartyIdAndOrderNo(partyId,
+								position.getOrderNo());
+						if (link != null && !StringUtils.isEmptyString(link.getTraderPartyId())) {
+							String tp = link.getTraderPartyId();
+							if (!followTraderNameCache.containsKey(tp)) {
+								Trader tr = traderService.findByPartyId(tp);
+								followTraderNameCache.put(tp, tr != null && !StringUtils.isEmptyString(tr.getName()) ? tr.getName() : "");
+								followTraderUuidCache.put(tp, tr != null && !StringUtils.isEmptyString(tr.getUuid()) ? tr.getUuid() : "");
+							}
+							traderName = followTraderNameCache.getOrDefault(tp, "");
+							traderUuid = followTraderUuidCache.getOrDefault(tp, "");
+						}
+					}
+					row.put("follow_trader_name", traderName);
+					row.put("follow_trader_id", traderUuid);
+					data.add(row);
+				}
+			}
+			resultObject.setCode("0");
+			resultObject.setData(data);
+		} catch (Exception e) {
+			resultObject.setCode("1");
+			resultObject.setMsg("程序错误");
+			logger.error("error:", e);
+		}
+		return resultObject;
+	}
+
 	public static int daysBetween(Date smdate, Date bdate) throws ParseException {
 		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
 		smdate = sdf.parse(sdf.format(smdate));
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/contract/dto/TraderOwnClosedAggDTO.java b/trading-order-bean/src/main/java/com/yami/trading/bean/contract/dto/TraderOwnClosedAggDTO.java
new file mode 100644
index 0000000..77a4d1e
--- /dev/null
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/contract/dto/TraderOwnClosedAggDTO.java
@@ -0,0 +1,18 @@
+package com.yami.trading.bean.contract.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 交易员本人已平仓合约汇总(全品种):用于累计收益/收益率与持仓保证金口径一致。
+ */
+@Data
+public class TraderOwnClosedAggDTO {
+
+	private String partyId;
+
+	private BigDecimal closedProfitSum;
+
+	private BigDecimal closedMarginSum;
+}
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/FollowCommissionType.java b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/FollowCommissionType.java
new file mode 100644
index 0000000..d9f5450
--- /dev/null
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/FollowCommissionType.java
@@ -0,0 +1,39 @@
+package com.yami.trading.bean.trader;
+
+/**
+ * 跟单佣金模式(与 T_TRADER.FOLLOW_COMMISSION_TYPE 一致)
+ */
+public final class FollowCommissionType {
+
+	public static final String LEGACY = "LEGACY";
+	/** 按月固定金额,开始跟单时从主钱包扣除(同一自然月再次跟单不重复扣) */
+	public static final String MONTHLY_FIXED = "MONTHLY_FIXED";
+	/** 按自然日汇总当日已实现盈亏;日合计大于 0 时按配置比例抽取,否则不抽;日终任务结算 */
+	public static final String DAILY_PROFIT_PCT = "DAILY_PROFIT_PCT";
+
+	private FollowCommissionType() {
+	}
+
+	public static boolean isLegacy(String type) {
+		return type == null || type.isEmpty() || LEGACY.equalsIgnoreCase(type);
+	}
+
+	public static boolean isMonthlyFixed(String type) {
+		return MONTHLY_FIXED.equalsIgnoreCase(type);
+	}
+
+	public static boolean isDailyProfitPct(String type) {
+		return DAILY_PROFIT_PCT.equalsIgnoreCase(type);
+	}
+
+	public static String normalizeOrLegacy(String type) {
+		if (type == null || type.trim().isEmpty()) {
+			return LEGACY;
+		}
+		String t = type.trim().toUpperCase();
+		if (MONTHLY_FIXED.equals(t) || DAILY_PROFIT_PCT.equals(t) || LEGACY.equals(t)) {
+			return t;
+		}
+		return LEGACY;
+	}
+}
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/Trader.java b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/Trader.java
index 9d6e154..18a8143 100644
--- a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/Trader.java
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/Trader.java
@@ -2,9 +2,13 @@
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.yami.trading.bean.trader.FollowCommissionType;
 import com.yami.trading.common.domain.BaseEntity;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.type.JdbcType;
+
+import java.math.BigDecimal;
 
 /**
  * 交易员
@@ -36,10 +40,25 @@
 	private String symbols;
 
 	/**
-	 * 利润分成比例
+	 * 利润分成比例(LEGACY:单笔盈利平仓时按此比例抽成)
 	 */
 	@TableField("PROFIT_SHARE_RATIO")
 	private double profitShareRatio;
+
+	/**
+	 * 跟单佣金类型:LEGACY / MONTHLY_FIXED / DAILY_PROFIT_PCT
+	 */
+	@TableField("FOLLOW_COMMISSION_TYPE")
+	private String followCommissionType = FollowCommissionType.LEGACY;
+
+	@TableField("FOLLOW_COMMISSION_MONTHLY_AMOUNT")
+	private BigDecimal followCommissionMonthlyAmount = BigDecimal.ZERO;
+
+	/**
+	 * 按日总盈利提成比例,0~1(与 PROFIT_SHARE_RATIO 存法一致)
+	 */
+	@TableField("FOLLOW_COMMISSION_DAILY_PCT")
+	private double followCommissionDailyPct;
 
 	/**
 	 * 状态(是否开启跟单)---STATE,0为未开启,1为开启
@@ -205,10 +224,10 @@
 	@TableField("DEVIATION_FOLLOWER_NOW")
 	private int deviationFollowerNow;
 	/**
-	 * 跟单最小下单数
+	 * 跟单最小下单数(币数量,支持小数;与库表 DECIMAL 对齐,避免 ORM/JDBC 整型转换异常)
 	 */
-	@TableField("FOLLOW_VOLUMN_MIN")
-	private int followVolumnMin;
+	@TableField(value = "FOLLOW_VOLUMN_MIN", jdbcType = JdbcType.DECIMAL)
+	private BigDecimal followVolumnMin;
 
 	/**
 	 * 审核: 0-待审核,1-审核通过,-1审核不通过
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowDailyPnl.java b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowDailyPnl.java
new file mode 100644
index 0000000..9d86fa1
--- /dev/null
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowDailyPnl.java
@@ -0,0 +1,38 @@
+package com.yami.trading.bean.trader.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.yami.trading.common.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 跟随者与带单员在某个自然日的跟单已实现盈亏汇总(按平仓时间归属日期)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("T_TRADER_FOLLOW_DAILY_PNL")
+public class TraderFollowDailyPnl extends BaseEntity {
+
+	@TableField("PARTY_ID")
+	private String partyId;
+
+	@TableField("TRADER_PARTY_ID")
+	private String traderPartyId;
+
+	@TableField("PNL_DATE")
+	private Date pnlDate;
+
+	@TableField("REALIZED_PROFIT_SUM")
+	private BigDecimal realizedProfitSum = BigDecimal.ZERO;
+
+	/** 0 未结算 1 已结算 */
+	@TableField("SETTLED")
+	private Integer settled = 0;
+
+	@TableField("COMMISSION_AMOUNT")
+	private BigDecimal commissionAmount = BigDecimal.ZERO;
+}
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java
index 3eeaf8d..f859b59 100644
--- a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUser.java
@@ -6,6 +6,8 @@
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 
+import java.math.BigDecimal;
+
 /**
  * 用户跟随交易员详情表
  */
@@ -13,6 +15,16 @@
 @TableName("T_TRADER_FOLLOW_USER")
 @Slf4j
 public class TraderFollowUser extends BaseEntity {
+
+	public static final String FOLLOW_TYPE_FIXED = "1";
+
+	public static final String STATE_FOLLOWING = "1";
+
+	public static final String STATE_STOPPED = "2";
+
+	public static final String STATE_STOPPING = "3";
+
+	public static final String STATE_FAILED = "4";
 
 	private static final long serialVersionUID = -1617033543659508052L;
 	
@@ -37,26 +49,38 @@
 	private String symbol;
 
 	/**
-	 * 跟单固定张数/固定比例---选择 1,固定张数,2,固定比例
+	 * 跟单方式。当前仅支持固定币数量模式
 	 */
 	@TableField("FOLLOW_TYPE")
-	private String followType;
+	private String followType = FOLLOW_TYPE_FIXED;
 	/**
-	 * 状态 是否还在跟随状态 1,跟随,2,取消跟随
+	 * 状态 1-跟随中 2-已停止 3-停止中
 	 */
 	@TableField("STATE")
-	private String state;
+	private String state = STATE_FOLLOWING;
 
 	/**
-	 * 跟单张数或比例---具体值
+	 * 最小跟单币数量
 	 */
 	@TableField("VOLUME")
 	private double volume;
 	/**
-	 * 最大持仓张数
+	 * 最大跟单币数量
 	 */
 	@TableField("VOLUME_MAX")
 	private double volumeMax;
+
+	/**
+	 * 跟随者自定义杠杆倍数(需已执行 docs/db/V5__follow_leverage_support.sql 增加 LEVER_RATE 列)
+	 */
+	@TableField("LEVER_RATE")
+	private double leverRate = 1D;
+
+	/**
+	 * 跟单投入币数量(新模型核心字段)
+	 */
+	@TableField("INVEST_AMOUNT")
+	private BigDecimal investAmount = BigDecimal.ZERO;
 	
 	/**
 	 * 累计跟单收益 PROFIT
@@ -86,9 +110,59 @@
 	@TableField("DAYS_SETTING")
 	private String daysSetting;
 
+	/**
+	 * 请求停止跟单时间戳(秒)
+	 */
+	@TableField("STOP_REQUEST_TIME")
+	private Long stopRequestTime;
+
+	/**
+	 * 停止跟单完成时间戳(秒)
+	 */
+	@TableField("STOP_FINISH_TIME")
+	private Long stopFinishTime;
+
+	/**
+	 * 月固定跟单费已缴纳到的自然月 yyyy-MM(同月内停止后再跟单不重复扣)
+	 */
+	@TableField("MONTHLY_FEE_PAID_PERIOD")
+	private String monthlyFeePaidPeriod;
+
+	/**
+	 * 最近一次跟单失败原因(如余额不足)
+	 */
+	@TableField("FAIL_REASON")
+	private String failReason;
+
+	/**
+	 * 最近一次跟单失败时间戳(秒)
+	 */
+	@TableField("LAST_FAIL_TIME")
+	private Long lastFailTime;
+
 	@TableField(exist = false)
 	private String userCode;
 
 	@TableField(exist = false)
 	private String traderUserCode;
+
+	@TableField(exist = false)
+	private String traderName;
+
+	@TableField(exist = false)
+	private Long openOrderCount;
+
+	@TableField(exist = false)
+	private Double openOrderVolume;
+
+	@TableField(exist = false)
+	private Double openOrderProfit;
+
+	public boolean isFollowing() {
+		return STATE_FOLLOWING.equals(this.state);
+	}
+
+	public boolean isStopping() {
+		return STATE_STOPPING.equals(this.state);
+	}
 }
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java
index 6be6387..8059b72 100644
--- a/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/trader/domain/TraderFollowUserOrder.java
@@ -14,6 +14,14 @@
 @Slf4j
 public class TraderFollowUserOrder extends BaseEntity {
 
+	public static final String STATE_SUBMITTED = "submitted";
+
+	public static final String STATE_CREATED = "created";
+
+	public static final String STATE_CANCELED = "canceled";
+
+	public static final String STATE_PROCESSING_CLOSE = "processing_close";
+
 	private static final long serialVersionUID = -1617033543659508052L;
 	
 	@TableField("PARTY_ID")
@@ -32,7 +40,7 @@
 	@TableField("USER_ORDER_NO")
 	private String userOrderNo;
 	/**
-	 * 当前订单张数
+	 * 当前订单币数量
 	 */
 	@TableField("VOLUME")
 	private double volume;
@@ -44,9 +52,9 @@
 	private String traderOrderNo;
 
 	/**
-	 * 状态。submitted 已提交(持仓),canceled 已撤销, created 完成(平仓)
+	 * 状态。submitted 已提交(持仓),processing_close 平仓处理中,canceled 已撤销,created 已完成
 	 */
 	@TableField("STATE")
-	private String state = "submitted";
+	private String state = STATE_SUBMITTED;
 
 }
diff --git a/trading-order-bean/src/main/java/com/yami/trading/bean/user/dto/MoneyLogDto.java b/trading-order-bean/src/main/java/com/yami/trading/bean/user/dto/MoneyLogDto.java
index b68c22e..163d33e 100644
--- a/trading-order-bean/src/main/java/com/yami/trading/bean/user/dto/MoneyLogDto.java
+++ b/trading-order-bean/src/main/java/com/yami/trading/bean/user/dto/MoneyLogDto.java
@@ -69,6 +69,9 @@
     @ApiModelProperty("资金日志提供的内容 :提币 充币 永续建仓 永续平仓 手续费")
     private String contentType;
 
+    @ApiModelProperty("账变类型描述(中文)")
+    private String contentTypeText;
+
     @ApiModelProperty("推荐人 用户名")
     private  String recomUserName;
 
diff --git a/trading-order-common/src/main/java/com/yami/trading/common/constants/Constants.java b/trading-order-common/src/main/java/com/yami/trading/common/constants/Constants.java
index 0383b1f..42be74e 100644
--- a/trading-order-common/src/main/java/com/yami/trading/common/constants/Constants.java
+++ b/trading-order-common/src/main/java/com/yami/trading/common/constants/Constants.java
@@ -504,6 +504,10 @@
 		MONEYLOG_CONTENT.put(MONEYLOG_CONTENT_BANK_CARD_WITHDRAW, "银行卡提现");
 		MONEYLOG_CONTENT.put(MONEYLOG_CONTENT_BANK_CARD_RECHARGE, "银行卡充值");
 		MONEYLOG_CONTENT.put(MONEYLOG_CONTENT_BANK_CARD_ORDER_CANCEL, "银行卡订单取消");
+		MONEYLOG_CONTENT.put(MONEYLOG_CONTENT_FOLLOW_UP_FEE, "跟单佣金");
+		MONEYLOG_CONTENT.put(MONEYLOG_TRANSFER_IN, "跟单账户划入");
+		MONEYLOG_CONTENT.put(MONEYLOG_TRANSFER_OUT, "跟单账户划出");
+		MONEYLOG_CONTENT.put(MONEYLOG_CONTENT_REWARD, "推荐奖励");
 	}
 
 	/**
diff --git a/trading-order-service/src/main/java/com/yami/trading/dao/contract/ContractOrderMapper.java b/trading-order-service/src/main/java/com/yami/trading/dao/contract/ContractOrderMapper.java
index a2fbbeb..cc0e61d 100644
--- a/trading-order-service/src/main/java/com/yami/trading/dao/contract/ContractOrderMapper.java
+++ b/trading-order-service/src/main/java/com/yami/trading/dao/contract/ContractOrderMapper.java
@@ -9,6 +9,7 @@
 import com.yami.trading.bean.contract.dto.ContractApplyOrderDTO;
 import com.yami.trading.bean.contract.dto.ContractOrderDTO;
 import com.yami.trading.bean.contract.query.ContractApplyOrderQuery;
+import com.yami.trading.bean.contract.dto.TraderOwnClosedAggDTO;
 import com.yami.trading.bean.contract.query.ContractOrderQuery;
 import org.apache.ibatis.annotations.Param;
 
@@ -43,4 +44,9 @@
     IPage<ContractOrderDTO> listRecordHistory(Page page,@Param("query")  ContractOrderQuery query);
 
     void batchUpdateBuffer(List<ContractOrder> list);
+
+	/**
+	 * 交易员本人、已平仓、非跟单单:按用户汇总盈亏与开仓保证金(deposit_open),全品种。
+	 */
+	List<TraderOwnClosedAggDTO> sumClosedTraderOwnAggByPartyIds(@Param("partyIds") List<String> partyIds);
 }
diff --git a/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowDailyPnlMapper.java b/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowDailyPnlMapper.java
new file mode 100644
index 0000000..7910435
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowDailyPnlMapper.java
@@ -0,0 +1,9 @@
+package com.yami.trading.dao.trader;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yami.trading.bean.trader.domain.TraderFollowDailyPnl;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TraderFollowDailyPnlMapper extends BaseMapper<TraderFollowDailyPnl> {
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowUserOrderMapper.java b/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowUserOrderMapper.java
index 6feeb0f..807a2fe 100644
--- a/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowUserOrderMapper.java
+++ b/trading-order-service/src/main/java/com/yami/trading/dao/trader/TraderFollowUserOrderMapper.java
@@ -12,5 +12,7 @@
 public interface TraderFollowUserOrderMapper extends BaseMapper<TraderFollowUserOrder> {
     List<Map<String, Object>> listDatas(@Param("pageNo") long pageNo, @Param("pageSize")long pageSize, @Param("partyId") String partyId, @Param("state") String state);
 
+	Long countListDatas(@Param("partyId") String partyId, @Param("state") String state);
+
     List<Map<String, Object>> listMDatas(@Param("pageNo") long pageNo, @Param("pageSize") long pageSize, @Param("name") String name, @Param("rolename") String rolename, @Param("username") String username);
 }
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java
index 08eba0c..086bcef 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractApplyOrderService.java
@@ -14,7 +14,6 @@
 import com.yami.trading.bean.data.domain.Realtime;
 import com.yami.trading.bean.item.domain.Item;
 import com.yami.trading.bean.item.dto.ItemLeverageDTO;
-import com.yami.trading.bean.model.FollowWallet;
 import com.yami.trading.bean.model.User;
 import com.yami.trading.bean.model.Wallet;
 import com.yami.trading.bean.syspara.domain.Syspara;
@@ -26,7 +25,6 @@
 import com.yami.trading.common.util.RandomUtil;
 import com.yami.trading.common.util.StringUtils;
 import com.yami.trading.dao.contract.ContractApplyOrderMapper;
-import com.yami.trading.service.FollowWalletService;
 import com.yami.trading.service.MoneyLogService;
 import com.yami.trading.service.RechargeBonusService;
 import com.yami.trading.service.WalletService;
@@ -81,8 +79,6 @@
     private MoneyLogService moneyLogService;
     @Autowired
     private WalletService walletService;
-    @Autowired
-    private FollowWalletService followWalletService;
     @Autowired
     private ContractOrderService contractOrderService;
     @Autowired
@@ -303,43 +299,25 @@
 
         order.setVolumeOpen(order.getVolume());
 
-        if(ContractApplyOrder.ORDER_FOLLOW == order.getFollow()) { // 跟单订单
-            FollowWallet followWallet = followWalletService.findByUserId(order.getPartyId());
-            BigDecimal amountBefore = followWallet.getMoney();
+        Wallet wallet = this.walletService.findByUserId(order.getPartyId());
+        BigDecimal amountBefore = wallet.getMoney();
 //            fee = exchangeRateService.getUsdtByType(order.getFee(), type);
 //            deposit = exchangeRateService.getUsdtByType(deposit, type);
-            BigDecimal totalAmountCost = deposit.add(order.getFee());
-            if (amountBefore.compareTo(totalAmountCost) < 0) {
-                throw new YamiShopBindException("余额不足");
-            }
-
-            followWalletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(deposit), BigDecimal.ZERO
-                    , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_OPEN, "委托单,订单号[" + order.getOrderNo() + "]"
-            );
-            followWalletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(fee), BigDecimal.ZERO
-                    , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_FEE, "委托单手续费,订单号[" + order.getOrderNo() + "]"
-            );
-        } else {
-            Wallet wallet = this.walletService.findByUserId(order.getPartyId());
-            BigDecimal amountBefore = wallet.getMoney();
-//            fee = exchangeRateService.getUsdtByType(order.getFee(), type);
-//            deposit = exchangeRateService.getUsdtByType(deposit, type);
-            BigDecimal totalAmountCost = deposit.add(order.getFee());
-            if (amountBefore.compareTo(totalAmountCost) < 0) {
-                throw new YamiShopBindException("余额不足");
-            }
-
-            walletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(deposit), BigDecimal.ZERO
-                    , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_OPEN, "委托单,订单号[" + order.getOrderNo() + "]"
-            );
-            walletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(fee), BigDecimal.ZERO
-                    , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_FEE, "委托单手续费,订单号[" + order.getOrderNo() + "]"
-            );
-
-
-            //ICE盘定制交易手续费返佣
-           rechargeBonusService.saveTradeBounsHandle(order.getPartyId(),1, fee.doubleValue(),order.getOrderNo(),order.getSymbol());
+        BigDecimal totalAmountCost = deposit.add(order.getFee());
+        if (amountBefore.compareTo(totalAmountCost) < 0) {
+            throw new YamiShopBindException("余额不足");
         }
+
+        walletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(deposit), BigDecimal.ZERO
+                , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_OPEN, "委托单,订单号[" + order.getOrderNo() + "]"
+        );
+        walletService.updateMoney(order.getSymbol(), order.getPartyId(), BigDecimal.ZERO.subtract(fee), BigDecimal.ZERO
+                , Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_FEE, "委托单手续费,订单号[" + order.getOrderNo() + "]"
+        );
+
+
+        //ICE盘定制交易手续费返佣
+       rechargeBonusService.saveTradeBounsHandle(order.getPartyId(),1, fee.doubleValue(),order.getOrderNo(),order.getSymbol());
         save(order);
         User party = this.userService.getById(order.getPartyId());
         if (Constants.SECURITY_ROLE_MEMBER.equals(party.getRoleName())) {
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
index ff6eec0..cea79b3 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/contract/ContractOrderService.java
@@ -16,10 +16,10 @@
 import com.yami.trading.bean.contract.domain.ContractOrder;
 import com.yami.trading.bean.contract.domain.ContractOrderProfit;
 import com.yami.trading.bean.contract.dto.ContractOrderDTO;
+import com.yami.trading.bean.contract.dto.TraderOwnClosedAggDTO;
 import com.yami.trading.bean.contract.query.ContractOrderQuery;
 import com.yami.trading.bean.data.domain.Realtime;
 import com.yami.trading.bean.item.domain.Item;
-import com.yami.trading.bean.model.FollowWallet;
 import com.yami.trading.bean.model.User;
 import com.yami.trading.bean.model.Wallet;
 import com.yami.trading.bean.syspara.domain.Syspara;
@@ -31,12 +31,12 @@
 import com.yami.trading.common.constants.ContractRedisKeys;
 import com.yami.trading.common.constants.RedisKeys;
 import com.yami.trading.common.constants.TipConstants;
+import com.yami.trading.common.util.Arith;
 import com.yami.trading.common.util.DateUtils;
 import com.yami.trading.common.util.RandomUtil;
 import com.yami.trading.common.util.RedisUtil;
 import com.yami.trading.common.util.StringUtils;
 import com.yami.trading.dao.contract.ContractOrderMapper;
-import com.yami.trading.service.FollowWalletService;
 import com.yami.trading.service.WalletService;
 import com.yami.trading.service.item.ItemService;
 import com.yami.trading.service.syspara.SysparaService;
@@ -69,6 +69,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -142,10 +143,6 @@
 
     @Autowired
     private WalletService walletService;
-
-    @Autowired
-    private FollowWalletService followWalletService;
-
 
     @Autowired
     private UserService userService;
@@ -240,6 +237,89 @@
         queryWrapper.orderByDesc("create_time");
         return list(queryWrapper);
     }
+
+    /**
+     * 仅查询交易员本人持仓(排除其作为跟单用户产生的跟单订单)。
+     */
+    public List<ContractOrder> findSubmittedTraderOwn(String partyId, String symbol) {
+        QueryWrapper<ContractOrder> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq(StrUtil.isNotBlank(partyId), "party_id", partyId);
+        queryWrapper.eq(StrUtil.isNotBlank(symbol), "symbol", symbol);
+        queryWrapper.eq("state", ContractOrder.STATE_SUBMITTED);
+        // 交易员本人持仓:排除明确的跟单单(follow=1),并兼容历史数据 follow 为空
+        queryWrapper.and(w -> w.ne("follow", ContractOrder.ORDER_FOLLOW).or().isNull("follow"));
+        queryWrapper.orderByDesc("create_time");
+        return list(queryWrapper);
+    }
+
+	/**
+	 * 交易员本人、已平仓、非跟单单:全品种已实现盈亏合计(与 T_TRADER.profit 相比以合约表为准)。
+	 */
+	public BigDecimal sumClosedProfitTraderOwn(String partyId) {
+		if (StrUtil.isBlank(partyId)) {
+			return BigDecimal.ZERO;
+		}
+		Map<String, TraderOwnClosedAggDTO> m = mapClosedTraderOwnAggByPartyIds(Collections.singletonList(partyId));
+		TraderOwnClosedAggDTO row = m.get(partyId);
+		if (row == null || row.getClosedProfitSum() == null) {
+			return BigDecimal.ZERO;
+		}
+		return row.getClosedProfitSum();
+	}
+
+	/**
+	 * 交易员本人、已平仓、非跟单单:全品种开仓保证金 deposit_open 合计(与当前持仓 deposit 同一口径,用于收益率分母)。
+	 */
+	public BigDecimal sumClosedDepositOpenTraderOwn(String partyId) {
+		if (StrUtil.isBlank(partyId)) {
+			return BigDecimal.ZERO;
+		}
+		Map<String, TraderOwnClosedAggDTO> m = mapClosedTraderOwnAggByPartyIds(Collections.singletonList(partyId));
+		TraderOwnClosedAggDTO row = m.get(partyId);
+		if (row == null || row.getClosedMarginSum() == null) {
+			return BigDecimal.ZERO;
+		}
+		return row.getClosedMarginSum();
+	}
+
+	public Map<String, TraderOwnClosedAggDTO> mapClosedTraderOwnAggByPartyIds(List<String> partyIds) {
+		if (partyIds == null || partyIds.isEmpty()) {
+			return Collections.emptyMap();
+		}
+		List<String> distinct = partyIds.stream().filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
+		if (distinct.isEmpty()) {
+			return Collections.emptyMap();
+		}
+		List<TraderOwnClosedAggDTO> rows = getBaseMapper().sumClosedTraderOwnAggByPartyIds(distinct);
+		if (rows == null || rows.isEmpty()) {
+			return Collections.emptyMap();
+		}
+		return rows.stream()
+				.filter(r -> r != null && StrUtil.isNotBlank(r.getPartyId()))
+				.collect(Collectors.toMap(TraderOwnClosedAggDTO::getPartyId, r -> r, (a, b) -> a));
+	}
+
+	/**
+	 * 累计收益/收益率:历史已实现盈亏(合约表全品种)+ 偏差;历史分母优先用已平仓保证金合计(与当前持仓保证金一致),否则回退 T_TRADER.order_amount。
+	 */
+	public double historyProfitForTraderTotalYield(Trader entity) {
+		if (entity == null || StrUtil.isBlank(entity.getPartyId())) {
+			return 0D;
+		}
+		double closed = sumClosedProfitTraderOwn(entity.getPartyId()).doubleValue();
+		return Arith.add(closed, entity.getDeviationProfit());
+	}
+
+	public double historyAmountBasisForTraderTotalYield(Trader entity) {
+		if (entity == null) {
+			return 0D;
+		}
+		BigDecimal marginSum = sumClosedDepositOpenTraderOwn(entity.getPartyId());
+		if (marginSum != null && marginSum.compareTo(BigDecimal.ZERO) > 0) {
+			return Arith.add(marginSum.doubleValue(), entity.getDeviationOrderAmount());
+		}
+		return Arith.add(entity.getOrderAmount(), entity.getDeviationOrderAmount());
+	}
 
 
     public List<ContractOrder> findSubmitted(String partyId, String symbol, String direction, String startTime, String endTime, String symbolType) {
@@ -413,31 +493,12 @@
         order.setFundingFee(defaultZero(order.getFundingFee()).add(fundingPnlChunk));
 //        Item item = itemService.findBySymbol(symbol);
 //        profit = exchangeRateService.getUsdtByType(profit, item.getType());
-        if (ContractOrder.ORDER_FOLLOW == order.getFollow()) { // 跟单订单
-//		if (profit > 0) {
-            FollowWallet wallet = followWalletService.findByUserId(order.getPartyId());
-            BigDecimal amount_before = wallet.getMoney();
-
-//		wallet.setMoney(Arith.add(wallet.getMoney(), profit));
-
-            if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
-                profit = wallet.getMoney().negate();
-            }
-
-            followWalletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
-                    Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
-        } else {
-            //		if (profit > 0) {
-            Wallet wallet = walletService.findByUserId(order.getPartyId());
-            BigDecimal amount_before = wallet.getMoney();
-
-//		wallet.setMoney(Arith.add(wallet.getMoney(), profit));
-            if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
-                profit = wallet.getMoney().negate();
-            }
-            walletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
-                    Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
+        Wallet wallet = walletService.findByUserId(order.getPartyId());
+        if (wallet.getMoney().add(profit).compareTo(BigDecimal.ZERO) < 0) {
+            profit = wallet.getMoney().negate();
         }
+        walletService.updateMoney(order.getSymbol(), partyId, profit, BigDecimal.ZERO,
+                Constants.MONEYLOG_CATEGORY_CONTRACT, Constants.WALLET_USDT, Constants.MONEYLOG_CONTENT_CONTRACT_CLOSE, "平仓,平仓合约数[" + volume + "],订单号[" + order.getOrderNo() + "]");
 //        List<Realtime> list = this.dataService.realtime(order.getSymbol());
 //        // 平仓时候把当前价格先更新回去
 //        if (list.size() != 0) {
@@ -450,6 +511,10 @@
         }
 
         update(order);
+
+        if (ContractOrder.STATE_CREATED.equals(order.getState())) {
+            traderFollowUserOrderService.syncFollowUserOrderLinkAfterContractClose(order);
+        }
 
         /**
          * 交易员带单,用户跟单
@@ -676,29 +741,7 @@
         BigDecimal realizedProfit = originProfit.multiply(closeRatio).setScale(8, RoundingMode.HALF_UP);
         BigDecimal profit = releasedDeposit.add(realizedProfit);
 
-        if (ContractOrder.ORDER_FOLLOW == order.getFollow()) { // 跟单还得减去利息收益
-            BigDecimal orderAmount = order.getUnitAmount().multiply(order.getTradeAvgPrice()).multiply(order.getLeverRate()); //订单总金额
-            TraderFollowUserOrder traderFollowUserOrder = traderFollowUserOrderService.findByPartyIdAndOrderNo(order.getPartyId(), order.getOrderNo());
-            if (null != traderFollowUserOrder) {
-                TraderFollowUser traderFollowUser = traderFollowUserService.findByPartyIdAndTrader_partyId(order.getPartyId(), traderFollowUserOrder.getTraderPartyId());
-                if (StringUtils.isNotEmpty(traderFollowUser.getDaysSetting())) {
-                    TraderDaysSetting traderDaysSetting = traderDaysSettingService.selectById(traderFollowUser.getDaysSetting());
-                    if (null != traderDaysSetting) { // 借款利率
-                        int days = 0;
-                        try {
-                            days = daysBetween(order.getCreateTime(), new Date());
-                        } catch (ParseException e) {
-//                            throw new RuntimeException(e);
-                            log.error(e.getMessage());
-                        }
-                        if (days < 0) {
-                            days = 0;
-                        }
-                    }
-                }
-            }
-
-        }
+        // 跟单利息功能已下线,此处不再做历史利率扣减
 
         order.setAmountClose(order.getAmountClose().add(profit));
         BigDecimal remainVolume = currentVolume.subtract(volume);
@@ -1093,6 +1136,10 @@
         return data;
     }
 
+    public List<Map<String, Object>> buildDataFromOrders(List<ContractOrder> list) {
+        return bulidData(list);
+    }
+
     public Map<String, Object> bulidOne(ContractOrder order) {
         Map<String, Object> map = new HashMap<String, Object>();
         map.put("order_no", order.getOrderNo());
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/impl/InternalEmailSenderServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/impl/InternalEmailSenderServiceImpl.java
index eba8d87..b1750c4 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/impl/InternalEmailSenderServiceImpl.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/impl/InternalEmailSenderServiceImpl.java
@@ -70,8 +70,10 @@
             .build();
 
     private static final String API_URL = "https://apiv2.aoksend.com/index/api/send_email";
-    private static final String APP_KEY = "7c653bf4e8398f676de6182a5ac100ed";  // 从环境变量或配置文件中获取
-    private static final String TEMPLATE_ID = "E_139512804023"; //模板
+    private static final String APP_KEY = "5573452ba45eb7743b970c1b66fcd6a2";
+    private static final String TEMPLATE_ID_REGISTER = "E_143737413621";
+    private static final String TEMPLATE_ID_RECHARGE = "E_143738794901";
+    private static final String TEMPLATE_ID_WITHDRAW = "E_143732815203";
     @Override
     public void send(EmailMessage emailMessage) {
         // 验证邮件信息数据的有效性
@@ -84,15 +86,21 @@
             logger.info("----- 开始发送邮件 -----");
             logger.info("发送邮件到: " + emailMessage.getTomail() + ", 来自: " + emailMessage.getContent());
 
+            String templateId = resolveTemplateId(emailMessage);
+            String codeValue = emailMessage.getContent();
+            if (codeValue == null) {
+                codeValue = "";
+            }
+
             // 使用 URL 构建器构建带有查询参数的 URL
             HttpUrl.Builder urlBuilder = HttpUrl.parse(API_URL).newBuilder();
             urlBuilder.addQueryParameter("app_key", APP_KEY);
-            urlBuilder.addQueryParameter("template_id", TEMPLATE_ID);
+            urlBuilder.addQueryParameter("template_id", templateId);
             urlBuilder.addQueryParameter("to", emailMessage.getTomail());
 
             ObjectMapper objectMapper = new ObjectMapper();
             Map<String, String> dataMap = new HashMap<>();
-            dataMap.put("code", emailMessage.getContent());
+            dataMap.put("code", codeValue);
 
             String json = objectMapper.writeValueAsString(dataMap);
             urlBuilder.addQueryParameter("data", json);
@@ -126,6 +134,19 @@
         } catch (Exception e) {
             logger.error("邮件发送失败【Exception】", e);
         }
+    }
+
+    private String resolveTemplateId(EmailMessage emailMessage) {
+        String subject = emailMessage == null || emailMessage.getSubject() == null
+                ? ""
+                : emailMessage.getSubject().toLowerCase();
+        if (subject.contains("deposit") || subject.contains("recharge") || subject.contains("充值")) {
+            return TEMPLATE_ID_RECHARGE;
+        }
+        if (subject.contains("withdraw") || subject.contains("提现")) {
+            return TEMPLATE_ID_WITHDRAW;
+        }
+        return TEMPLATE_ID_REGISTER;
     }
 
     /**
@@ -166,7 +187,7 @@
             // 使用 URL 构建器构建带有查询参数的 URL
             HttpUrl.Builder urlBuilder = HttpUrl.parse(API_URL).newBuilder();
             urlBuilder.addQueryParameter("app_key", APP_KEY);
-            urlBuilder.addQueryParameter("template_id", TEMPLATE_ID);
+            urlBuilder.addQueryParameter("template_id", TEMPLATE_ID_REGISTER);
             urlBuilder.addQueryParameter("to", emailMessage.getTomail());
 
             // 将邮件内容以 JSON 形式传递
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/notify/WalletTransactionMailNotificationService.java b/trading-order-service/src/main/java/com/yami/trading/service/notify/WalletTransactionMailNotificationService.java
index ff9cb37..85a25d3 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/notify/WalletTransactionMailNotificationService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/notify/WalletTransactionMailNotificationService.java
@@ -26,16 +26,10 @@
             return;
         }
         String subject = "Deposit successful";
-        String amt = amount == null ? "-" : amount.stripTrailingZeros().toPlainString();
-        String asset = assetDescription == null ? "" : assetDescription;
-        String body = "Hello,\n\n"
-                + "Your deposit has been credited successfully.\n\n"
-                + "Order number: " + orderNo + "\n"
-                + "Amount: " + amt + " " + asset + "\n\n"
-                + "If you did not make this transaction, please contact support immediately.\n\n"
-                + "Best regards";
+        String amountCode = amount == null ? "-" : amount.stripTrailingZeros().toPlainString();
         try {
-            emailSendService.sendEmail(to, subject, body);
+            // 充值邮件走模板,code=充值金额
+            emailSendService.sendEmail(to, subject, amountCode);
         } catch (Exception e) {
             log.error("Failed to send deposit success email, orderNo={}", orderNo, e);
         }
@@ -48,20 +42,10 @@
             return;
         }
         String subject = "Withdrawal successful";
-        String amt = amount == null ? "-" : amount.stripTrailingZeros().toPlainString();
-        String feeStr = (fee == null || fee.compareTo(BigDecimal.ZERO) == 0)
-                ? "none"
-                : fee.stripTrailingZeros().toPlainString();
-        String asset = assetDescription == null ? "" : assetDescription;
-        String body = "Hello,\n\n"
-                + "Your withdrawal has been completed successfully.\n\n"
-                + "Order number: " + orderNo + "\n"
-                + "Amount: " + amt + " " + asset + "\n"
-                + "Fee: " + feeStr + "\n\n"
-                + "If you did not request this withdrawal, please contact support immediately.\n\n"
-                + "Best regards";
+        String amountCode = amount == null ? "-" : amount.stripTrailingZeros().toPlainString();
         try {
-            emailSendService.sendEmail(to, subject, body);
+            // 提现邮件走模板,code=提现金额
+            emailSendService.sendEmail(to, subject, amountCode);
         } catch (Exception e) {
             log.error("Failed to send withdrawal success email, orderNo={}", orderNo, e);
         }
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/FollowCommissionService.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/FollowCommissionService.java
new file mode 100644
index 0000000..dbf1a4b
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/FollowCommissionService.java
@@ -0,0 +1,31 @@
+package com.yami.trading.service.trader;
+
+import com.yami.trading.bean.trader.domain.Trader;
+import com.yami.trading.bean.trader.domain.TraderFollowUser;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 跟单佣金:月固定预扣、按日已实现盈亏汇总与日终提成
+ */
+public interface FollowCommissionService {
+
+	/**
+	 * 跟单平仓后累加「自然日」已实现盈亏(仅 DAILY_PROFIT_PCT 模式使用;按平仓时间归属日期,与是否当日停跟无关)
+	 */
+	void accumulateDailyRealizedPnl(String followerPartyId, String traderPartyId, BigDecimal realizedProfit,
+			long closeEpochSec);
+
+	/**
+	 * 结算某一自然日的汇总行:日合计盈利则按带单员配置比例抽成,否则不抽
+	 *
+	 * @return 处理条数
+	 */
+	int settleDailyPnlForDate(LocalDate pnlDate);
+
+	/**
+	 * 开始跟单前:若带单员为月费模式且本周期未缴,则从跟随者主钱包扣费并写入 outEntity.monthlyFeePaidPeriod
+	 */
+	void applyMonthlyFeeIfNeeded(Trader trader, TraderFollowUser existing, TraderFollowUser outEntity);
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserOrderService.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserOrderService.java
index 4ea410b..2542260 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserOrderService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserOrderService.java
@@ -23,13 +23,28 @@
 	 * 平仓,按订单进行平仓
 	 */
 	public void traderClose(ContractOrder order, ContractOrderService contractOrderService);
-	
+
+	/**
+	 * 跟单用户合约已完全平仓时,将跟单映射表状态从 submitted/processing_close 同步为 created。
+	 * 停止跟单会对跟单方直接走合约 saveClose;跟单方不是交易员时不会触发异步 traderClose,
+	 * 若不同步会导致映射长期为 submitted,再次跟单时仍占用 volumeMax、无法跟随新开仓。
+	 */
+	void syncFollowUserOrderLinkAfterContractClose(ContractOrder contractOrder);
+
+	/**
+	 * 按用户+交易员维度,将「合约已 created 但跟单映射仍为 submitted」的历史脏数据纠正为 created(再次开始跟单时调用)。
+	 */
+	void reconcileStaleSubmittedMappings(String partyId, String traderPartyId);
+
 	
 	/**
 	 * @param partyId  用户partyId
 	 * @param apply_oder_no 委托单订单号
 	 */
 	public TraderFollowUserOrder findByPartyIdAndOrderNo(String partyId, String apply_oder_no);
+
+	public List<TraderFollowUserOrder> findByPartyIdAndTraderPartyIdAndState(String partyId, String trader_partyId,
+			String state);
 	
 	public void update(TraderFollowUserOrder entity);
 	
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserService.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserService.java
index ad44c5c..786e554 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserService.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/TraderFollowUserService.java
@@ -1,7 +1,9 @@
 package com.yami.trading.service.trader;
 
 import com.yami.trading.bean.trader.domain.TraderFollowUser;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yami.trading.service.contract.ContractOrderService;
 
 import java.util.List;
 import java.util.Map;
@@ -40,10 +42,14 @@
 	 */
 	public void deleteCancel(String id);
 
+	public void cancelFollowAsync(String id, ContractOrderService contractOrderService);
+
 	/**
 	 * 查询跟随交易员的用户
 	 */
 	public List<TraderFollowUser> findByTrader_partyId(String trader_partyId);
+
+	public List<TraderFollowUser> findActiveByTraderPartyId(String trader_partyId);
 
 	/**
 	 * 查询跟随交易员的某个用户
@@ -55,4 +61,19 @@
 	 */
 	public List<TraderFollowUser> findByPartyId(String partyId);
 
+	/**
+	 * 当前用户跟单关系总数(含已停止/失败等全部状态)
+	 */
+	long countByPartyId(String partyId);
+
+	/**
+	 * 分页:按更新时间、创建时间倒序(最新在前)
+	 */
+	IPage<TraderFollowUser> pageByPartyId(Page<TraderFollowUser> page, String partyId);
+
+	/**
+	 * 跟单员开仓跟单失败(如余额不足):当前跟随会话置为失败并记录原因,当前跟随人数减一。
+	 */
+	void markFollowOpenFailed(String partyId, String traderPartyId, String reason);
+
 }
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/AdminTraderServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/AdminTraderServiceImpl.java
index 1d0a047..16d72c7 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/AdminTraderServiceImpl.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/AdminTraderServiceImpl.java
@@ -41,7 +41,12 @@
 //			return list.get(0);
 //		}
 //		return null;
-        return traderMapper.selectOne(Wrappers.<Trader>lambdaQuery().eq(Trader::getPartyId, partyId));
+        return traderMapper.selectOne(Wrappers.<Trader>lambdaQuery()
+				.eq(Trader::getPartyId, partyId)
+				.eq(Trader::getDelFlag, 0)
+				.orderByDesc(Trader::getChecked)
+				.orderByDesc(Trader::getCreateTime)
+				.last("limit 1"));
 	}
 	
 	public Trader findById(String id) {
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/FollowCommissionServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/FollowCommissionServiceImpl.java
new file mode 100644
index 0000000..253d7a9
--- /dev/null
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/FollowCommissionServiceImpl.java
@@ -0,0 +1,203 @@
+package com.yami.trading.service.trader.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.yami.trading.bean.model.MoneyLog;
+import com.yami.trading.bean.model.Wallet;
+import com.yami.trading.bean.trader.FollowCommissionType;
+import com.yami.trading.bean.trader.domain.Trader;
+import com.yami.trading.bean.trader.domain.TraderFollowDailyPnl;
+import com.yami.trading.bean.trader.domain.TraderFollowUser;
+import com.yami.trading.common.constants.Constants;
+import com.yami.trading.common.exception.BusinessException;
+import com.yami.trading.common.util.ApplicationUtil;
+import com.yami.trading.common.util.Arith;
+import com.yami.trading.common.util.StringUtils;
+import com.yami.trading.dao.trader.TraderFollowDailyPnlMapper;
+import com.yami.trading.service.MoneyLogService;
+import com.yami.trading.service.WalletService;
+import com.yami.trading.service.trader.FollowCommissionService;
+import com.yami.trading.service.trader.TraderService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.List;
+
+@Service
+@Slf4j
+public class FollowCommissionServiceImpl implements FollowCommissionService {
+
+	@Resource
+	private TraderFollowDailyPnlMapper traderFollowDailyPnlMapper;
+	@Resource
+	private TraderService traderService;
+	@Resource
+	private WalletService walletService;
+	@Resource
+	private MoneyLogService moneyLogService;
+
+	@Override
+	public void accumulateDailyRealizedPnl(String followerPartyId, String traderPartyId, BigDecimal realizedProfit,
+			long closeEpochSec) {
+		if (realizedProfit == null) {
+			return;
+		}
+		long ts = closeEpochSec > 0 ? closeEpochSec : Instant.now().getEpochSecond();
+		LocalDate day = Instant.ofEpochSecond(ts).atZone(ZoneId.systemDefault()).toLocalDate();
+		Date pnlDate = Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+		TraderFollowDailyPnl row = traderFollowDailyPnlMapper.selectOne(Wrappers.<TraderFollowDailyPnl>lambdaQuery()
+				.eq(TraderFollowDailyPnl::getPartyId, followerPartyId)
+				.eq(TraderFollowDailyPnl::getTraderPartyId, traderPartyId)
+				.eq(TraderFollowDailyPnl::getPnlDate, pnlDate));
+		if (row == null) {
+			TraderFollowDailyPnl insert = new TraderFollowDailyPnl();
+			insert.setUuid(ApplicationUtil.getCurrentTimeUUID());
+			insert.setPartyId(followerPartyId);
+			insert.setTraderPartyId(traderPartyId);
+			insert.setPnlDate(pnlDate);
+			insert.setRealizedProfitSum(realizedProfit);
+			insert.setSettled(0);
+			insert.setCommissionAmount(BigDecimal.ZERO);
+			traderFollowDailyPnlMapper.insert(insert);
+			return;
+		}
+		if (row.getSettled() != null && row.getSettled() == 1) {
+			log.warn("accumulateDailyRealizedPnl skip: row already settled follower={} trader={} day={}", followerPartyId,
+					traderPartyId, day);
+			return;
+		}
+		BigDecimal sum = row.getRealizedProfitSum() == null ? BigDecimal.ZERO : row.getRealizedProfitSum();
+		row.setRealizedProfitSum(sum.add(realizedProfit));
+		traderFollowDailyPnlMapper.updateById(row);
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public int settleDailyPnlForDate(LocalDate pnlDate) {
+		Date d = Date.from(pnlDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+		List<TraderFollowDailyPnl> list = traderFollowDailyPnlMapper.selectList(Wrappers.<TraderFollowDailyPnl>lambdaQuery()
+				.eq(TraderFollowDailyPnl::getPnlDate, d)
+				.eq(TraderFollowDailyPnl::getSettled, 0));
+		int n = 0;
+		for (TraderFollowDailyPnl row : list) {
+			if (settleOneRow(row, pnlDate)) {
+				n++;
+			}
+		}
+		return n;
+	}
+
+	private boolean settleOneRow(TraderFollowDailyPnl row, LocalDate pnlDate) {
+		TraderFollowDailyPnl fresh = traderFollowDailyPnlMapper.selectById(row.getUuid());
+		if (fresh == null || (fresh.getSettled() != null && fresh.getSettled() == 1)) {
+			return false;
+		}
+		Trader trader = traderService.findByPartyId(fresh.getTraderPartyId());
+		if (trader == null || !FollowCommissionType.isDailyProfitPct(FollowCommissionType.normalizeOrLegacy(trader.getFollowCommissionType()))) {
+			fresh.setSettled(1);
+			fresh.setCommissionAmount(BigDecimal.ZERO);
+			traderFollowDailyPnlMapper.updateById(fresh);
+			return true;
+		}
+		BigDecimal sum = fresh.getRealizedProfitSum() == null ? BigDecimal.ZERO : fresh.getRealizedProfitSum();
+		BigDecimal fee = BigDecimal.ZERO;
+		if (sum.compareTo(BigDecimal.ZERO) > 0 && trader.getFollowCommissionDailyPct() > 0) {
+			fee = sum.multiply(BigDecimal.valueOf(trader.getFollowCommissionDailyPct())).setScale(8, RoundingMode.DOWN);
+			if (fee.compareTo(BigDecimal.ZERO) > 0) {
+				String traderLabel = StringUtils.isEmptyString(trader.getName()) ? fresh.getTraderPartyId() : trader.getName().trim();
+				String sumStr = sum.stripTrailingZeros().toPlainString();
+				String feeStr = fee.stripTrailingZeros().toPlainString();
+				double pctDisp = Arith.mul(trader.getFollowCommissionDailyPct(), 100D);
+				String businessMemo = String.format(
+						"[跟单佣金-按日利润提成]账目日%s|当日已实现盈亏合计:USDT %s|带单员日提成比例:%.4f%%|本次提成:USDT %s|带单员:%s",
+						pnlDate, sumStr, pctDisp, feeStr, traderLabel);
+				if (!tryTransferFollowerToTrader(fresh.getPartyId(), fresh.getTraderPartyId(), fee, businessMemo)) {
+					return false;
+				}
+			}
+		}
+		fresh.setCommissionAmount(fee);
+		fresh.setSettled(1);
+		traderFollowDailyPnlMapper.updateById(fresh);
+		return true;
+	}
+
+	private boolean tryTransferFollowerToTrader(String followerPartyId, String traderPartyId, BigDecimal fee, String businessMemo) {
+		double feeD = fee.doubleValue();
+		Wallet walletFollower = walletService.saveWalletByPartyId(followerPartyId);
+		double beforeF = walletFollower.getMoney().doubleValue();
+		if (beforeF + 1e-12 < feeD) {
+			log.warn("tryTransferFollowerToTrader insufficient follower={} need={} has={}", followerPartyId, feeD, beforeF);
+			return false;
+		}
+		walletService.update(followerPartyId, Arith.sub(0, feeD));
+		MoneyLog logF = new MoneyLog();
+		logF.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
+		logF.setAmountBefore(new BigDecimal(beforeF));
+		logF.setAmount(BigDecimal.valueOf(Arith.sub(0, feeD)));
+		logF.setAmountAfter(BigDecimal.valueOf(Arith.sub(beforeF, feeD)));
+		logF.setLog(businessMemo + "|账变:跟随者主钱包扣款");
+		logF.setUserId(followerPartyId);
+		logF.setWalletType(Constants.WALLET);
+		logF.setSymbol(Constants.WALLET_USDT);
+		logF.setContentType(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
+		moneyLogService.save(logF);
+
+		Wallet walletTrader = walletService.saveWalletByPartyId(traderPartyId);
+		double beforeT = walletTrader.getMoney().doubleValue();
+		walletService.update(traderPartyId, feeD);
+		MoneyLog logT = new MoneyLog();
+		logT.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
+		logT.setAmountBefore(new BigDecimal(beforeT));
+		logT.setAmount(BigDecimal.valueOf(feeD));
+		logT.setAmountAfter(BigDecimal.valueOf(Arith.add(beforeT, feeD)));
+		logT.setLog(businessMemo + "|账变:带单员主钱包入账");
+		logT.setUserId(traderPartyId);
+		logT.setWalletType(Constants.WALLET);
+		logT.setSymbol(Constants.WALLET_USDT);
+		logT.setContentType(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
+		moneyLogService.save(logT);
+		return true;
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public void applyMonthlyFeeIfNeeded(Trader trader, TraderFollowUser existing, TraderFollowUser outEntity) {
+		String type = FollowCommissionType.normalizeOrLegacy(trader.getFollowCommissionType());
+		if (!FollowCommissionType.isMonthlyFixed(type)) {
+			return;
+		}
+		BigDecimal amount = trader.getFollowCommissionMonthlyAmount();
+		if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+			throw new BusinessException(1, "该交易员月跟单费未正确配置");
+		}
+		String ym = YearMonth.now(ZoneId.systemDefault()).toString();
+		if (existing != null && ym.equals(existing.getMonthlyFeePaidPeriod())) {
+			outEntity.setMonthlyFeePaidPeriod(existing.getMonthlyFeePaidPeriod());
+			return;
+		}
+		Wallet wallet = walletService.saveWalletByPartyId(outEntity.getPartyId());
+		if (wallet.getMoney().compareTo(amount) < 0) {
+			throw new BusinessException(1, "主钱包余额不足,无法缴纳月跟单费");
+		}
+		String traderLabel = StringUtils.isEmptyString(trader.getName()) ? trader.getPartyId() : trader.getName().trim();
+		String followerLabel = StringUtils.isEmptyString(outEntity.getUsername()) ? outEntity.getPartyId()
+				: outEntity.getUsername().trim();
+		String amtStr = amount.stripTrailingZeros().toPlainString();
+		String businessMemo = String.format("[跟单佣金-月固定费]账单月%s|跟随用户:%s|带单员:%s|应付:USDT %s", ym, followerLabel,
+				traderLabel, amtStr);
+		if (!tryTransferFollowerToTrader(outEntity.getPartyId(), trader.getPartyId(), amount, businessMemo)) {
+			throw new BusinessException(1, "主钱包余额不足,无法缴纳月跟单费");
+		}
+		outEntity.setMonthlyFeePaidPeriod(ym);
+	}
+}
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java
index f9f7a99..f5e66cc 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderFollowUserOrderServiceImpl.java
@@ -5,23 +5,24 @@
 import com.yami.trading.bean.contract.domain.ContractApplyOrder;
 import com.yami.trading.bean.contract.domain.ContractOrder;
 import com.yami.trading.bean.model.*;
+import com.yami.trading.bean.syspara.domain.Syspara;
 import com.yami.trading.bean.trader.domain.*;
 import com.yami.trading.common.constants.Constants;
 import com.yami.trading.common.constants.RedisKeys;
 import com.yami.trading.common.util.*;
 import com.yami.trading.dao.trader.TraderFollowUserOrderMapper;
-import com.yami.trading.service.FollowMoneyLogService;
-import com.yami.trading.service.FollowWalletService;
 import com.yami.trading.service.MoneyLogService;
 import com.yami.trading.service.WalletService;
 import com.yami.trading.service.contract.ContractApplyOrderService;
 import com.yami.trading.service.contract.ContractOrderService;
 import com.yami.trading.service.syspara.SysparaService;
+import com.yami.trading.bean.trader.FollowCommissionType;
 import com.yami.trading.service.trader.*;
 import com.yami.trading.service.user.UserRecomService;
 import com.yami.trading.service.user.UserService;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -29,9 +30,17 @@
 import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 @Service
 public class TraderFollowUserOrderServiceImpl implements TraderFollowUserOrderService {
+	private static final ExecutorService FOLLOW_EXECUTOR = Executors.newFixedThreadPool(8);
+
+	private final ConcurrentMap<String, Boolean> followTaskKeys = new ConcurrentHashMap<>();
+
 	@Resource
 	private TraderService traderService;
 	@Resource
@@ -42,13 +51,7 @@
 	private WalletService walletService;
 
 	@Resource
-	private FollowWalletService followWalletService;
-
-	@Resource
 	private MoneyLogService moneyLogService;
-
-	@Resource
-	private FollowMoneyLogService followMoneyLogService;
 
 	@Resource
 	private TraderOrderService traderOrderService;
@@ -62,7 +65,14 @@
 
 	@Resource
 	private UserService userService;
-	
+
+	@Resource
+	private FollowCommissionService followCommissionService;
+
+	@Lazy
+	@Resource
+	private ContractOrderService contractOrderService;
+
 	private static Log logger = LogFactory.getLog(TraderFollowUserOrderServiceImpl.class);
 
 	public List<Map<String, Object>> getPaged(Page page, String partyId, String state) {
@@ -86,6 +96,12 @@
 //		parameters.put("partyId", partyId);
 //
 //		queryString.append(" order by trader_user_order.CREATE_TIME desc ");
+		Long total = traderFollowUserOrderMapper.countListDatas(partyId, state);
+		if (total != null) {
+			page.setTotal(total.longValue());
+		} else {
+			page.setTotal(0L);
+		}
 		List<Map<String, Object>> datas = traderFollowUserOrderMapper.listDatas((page.getCurrent() - 1) * page.getSize(), page.getSize(), partyId, state);
 
 		List<Map<String, Object>> data = this.bulidData(datas);
@@ -146,8 +162,10 @@
 			map.put("force_close_price", entity.get("force_close_price"));
 
 			String trader_party_id = (String) entity.get("trader_party_id");
-			User user = userService.findByUserId(trader_party_id);
-			map.put("trader_username", user.getUserName());
+			User user = trader_party_id == null ? null : userService.findByUserId(trader_party_id);
+			map.put("trader_username", user != null && user.getUserName() != null ? user.getUserName() : "");
+			Object fn = entity.get("follow_trader_name");
+			map.put("follow_trader_name", fn != null ? fn.toString() : "");
 
 			result_traders.add(map);
 		}
@@ -190,20 +208,63 @@
 	}
 
 	@Override
+	public void syncFollowUserOrderLinkAfterContractClose(ContractOrder contractOrder) {
+		if (contractOrder == null || !ContractOrder.STATE_CREATED.equals(contractOrder.getState())) {
+			return;
+		}
+		String partyId = contractOrder.getPartyId();
+		String orderNo = contractOrder.getOrderNo();
+		if (StringUtils.isEmptyString(partyId) || StringUtils.isEmptyString(orderNo)) {
+			return;
+		}
+		TraderFollowUserOrder link = findByPartyIdAndOrderNo(partyId, orderNo);
+		if (link == null) {
+			return;
+		}
+		if (!TraderFollowUserOrder.STATE_SUBMITTED.equals(link.getState())
+				&& !TraderFollowUserOrder.STATE_PROCESSING_CLOSE.equals(link.getState())) {
+			return;
+		}
+		link.setState(ContractOrder.STATE_CREATED);
+		update(link);
+	}
+
+	@Override
+	public void reconcileStaleSubmittedMappings(String partyId, String traderPartyId) {
+		if (StringUtils.isEmptyString(partyId) || StringUtils.isEmptyString(traderPartyId)) {
+			return;
+		}
+		List<TraderFollowUserOrder> list = traderFollowUserOrderMapper.selectList(
+				Wrappers.<TraderFollowUserOrder>lambdaQuery()
+						.eq(TraderFollowUserOrder::getPartyId, partyId)
+						.eq(TraderFollowUserOrder::getTraderPartyId, traderPartyId)
+						.eq(TraderFollowUserOrder::getState, TraderFollowUserOrder.STATE_SUBMITTED));
+		if (list == null || list.isEmpty()) {
+			return;
+		}
+		for (TraderFollowUserOrder link : list) {
+			if (StringUtils.isEmptyString(link.getUserOrderNo())) {
+				continue;
+			}
+			ContractOrder co = contractOrderService.findByOrderNo(link.getUserOrderNo());
+			if (co != null && ContractOrder.STATE_CREATED.equals(co.getState())) {
+				link.setState(ContractOrder.STATE_CREATED);
+				update(link);
+			}
+		}
+	}
+
+	@Override
 	public void traderOpen(ContractOrder contractOrder, ContractApplyOrderService contractApplyOrderService, ContractOrderService contractOrderService, int follow) {
 		if (isOrNotTrader(contractOrder.getPartyId())) {
-			CreateDelayThread lockDelayThread = new CreateDelayThread(contractOrder, contractApplyOrderService, contractOrderService, follow);
-			Thread t = new Thread(lockDelayThread);
-			t.start();
+			FOLLOW_EXECUTOR.submit(new CreateDelayThread(contractOrder, contractApplyOrderService, contractOrderService, follow));
 		}
 	}
 	
 	@Override
 	public void traderClose(ContractOrder contractOrder, ContractOrderService contractOrderService) {
 		if (isOrNotTrader(contractOrder.getPartyId())) {
-			CloseDelayThread lockDelayThread = new CloseDelayThread(contractOrder, contractOrderService);
-			Thread t = new Thread(lockDelayThread);
-			t.start();
+			FOLLOW_EXECUTOR.submit(new CloseDelayThread(contractOrder, contractOrderService));
 
 		}
 //			else {
@@ -228,7 +289,7 @@
 
 		public void run() {
 			try {
-				List<TraderFollowUser> users = traderFollowUserService.findByTrader_partyId(contractOrder.getPartyId()); //查找当前交易员的跟随者
+				List<TraderFollowUser> users = traderFollowUserService.findActiveByTraderPartyId(contractOrder.getPartyId()); // 查找当前交易员的有效跟随者
 				if (users != null) {
 					for (TraderFollowUser user : users) {
 						if (!"".equals(user.getPartyId())) {
@@ -236,8 +297,16 @@
 							 * 判断当前用户最多还可以买几张
 							 */
 							try {
+								String taskKey = buildTaskKey(contractOrder.getOrderNo(), user.getPartyId(), ContractApplyOrder.OFFSET_OPEN);
+								if (followTaskKeys.putIfAbsent(taskKey, Boolean.TRUE) != null) {
+									continue;
+								}
+								if (hasOpenFollowMapping(user.getPartyId(), contractOrder.getOrderNo())) {
+									followTaskKeys.remove(taskKey);
+									continue;
+								}
 								List<TraderFollowUserOrder> userOrders = findByPartyIdAndTraderPartyIdAndState(user.getPartyId(), contractOrder.getPartyId(), ContractOrder.STATE_SUBMITTED);
-								double volume_last = user.getVolumeMax();  // 跟单时设置的最大持仓张数
+								double volume_last = user.getVolumeMax();  // 跟单时设置的最大持仓币数量
 								if (userOrders != null) {
 									for (TraderFollowUserOrder userOrder : userOrders) {
 										volume_last = Arith.sub(volume_last, userOrder.getVolume());
@@ -247,6 +316,10 @@
 									continue;
 								}
 
+								if (user.getSymbol() != null && !user.getSymbol().trim().isEmpty()
+										&& !user.getSymbol().trim().equalsIgnoreCase(contractOrder.getSymbol())) {
+									continue;
+								}
 								ContractApplyOrder order = new ContractApplyOrder();
 								order.setOrderNo(DateUtil.getToday("yyMMddHHmmss") + RandomUtil.getRandomNum(8));
 								order.setPartyId(user.getPartyId());
@@ -255,28 +328,12 @@
 								order.setOffset(ContractApplyOrder.OFFSET_OPEN);
 								order.setFollow(follow); // 标记为跟单订单
 
-								/**
-								 * 跟单固定张数/固定比例---选择 1,固定张数,2,固定比例
-								 */
-								if ("1".equals(user.getFollowType())) {
-									if (volume_last < user.getVolume()) { // 剩余可下单张数小于用户设置的固定开仓单数
-										order.setVolume(new BigDecimal(volume_last));
-										order.setVolumeOpen(new BigDecimal(volume_last));
-									} else {
-										order.setVolume(BigDecimal.valueOf(user.getVolume()));
-										order.setVolumeOpen(BigDecimal.valueOf(user.getVolume()));
-									}
-								}
-								if ("2".equals(user.getFollowType())) {
-									if (volume_last < Arith.mul(contractOrder.getVolumeOpen().doubleValue(), user.getVolume())) {
-										order.setVolume(new BigDecimal(volume_last));
-										order.setVolumeOpen(new BigDecimal(volume_last));
-									} else {
-										order.setVolume(BigDecimal.valueOf(Arith.mul(contractOrder.getVolumeOpen().doubleValue(), user.getVolume())));
-										order.setVolumeOpen(BigDecimal.valueOf(Arith.mul(contractOrder.getVolumeOpen().doubleValue(), user.getVolume())));
-									}
-								}
-								order.setLeverRate(contractOrder.getLeverRate()); // 杠杆
+								double targetVolume = Math.min(volume_last, user.getVolume());
+								order.setVolume(BigDecimal.valueOf(targetVolume));
+								order.setVolumeOpen(BigDecimal.valueOf(targetVolume));
+								// 跟随者 LEVER_RATE:仅当 >0 时采用;未设置/null(库空→0)/≤0 一律默认 1 倍,不回退交易员持仓杠杆
+								double configuredLever = user.getLeverRate() > 0D ? user.getLeverRate() : 1D;
+								order.setLeverRate(BigDecimal.valueOf(configuredLever));
 								order.setPrice(contractOrder.getTradeAvgPrice()); // 永续合约交易委托价格,设置为交易员成交无效
 								order.setStopPriceProfit(contractOrder.getStopPriceProfit());
 								order.setStopPriceLoss(contractOrder.getStopPriceLoss());
@@ -331,6 +388,14 @@
 								traderFollowUserService.update(user);
 							} catch (Exception e) {
 								logger.error("TraderFollowUserOrderServiceImpl_error:", e);
+								String msg = e.getMessage();
+								if (msg == null || msg.isEmpty()) {
+									msg = e.getClass().getSimpleName();
+								}
+								traderFollowUserService.markFollowOpenFailed(user.getPartyId(), contractOrder.getPartyId(), msg);
+							} finally {
+								String taskKey = buildTaskKey(contractOrder.getOrderNo(), user.getPartyId(), ContractApplyOrder.OFFSET_OPEN);
+								followTaskKeys.remove(taskKey);
 							}
 						}
 						ThreadUtils.sleep(10);
@@ -379,12 +444,19 @@
 					if (orders != null) {
 						for (TraderFollowUserOrder order : orders) {
 							try {
+								String taskKey = buildTaskKey(contractOrder.getOrderNo(), order.getPartyId(), ContractApplyOrder.OFFSET_CLOSE);
+								if (followTaskKeys.putIfAbsent(taskKey, Boolean.TRUE) != null) {
+									continue;
+								}
 								if (ContractOrder.STATE_SUBMITTED.equals(order.getState())) {
+									order.setState(TraderFollowUserOrder.STATE_PROCESSING_CLOSE);
+									traderFollowUserOrderMapper.updateById(order);
 									ContractOrder user_contract_order = contractOrderService
 											.saveClose(order.getPartyId(), order.getUserOrderNo());
-									order.setState(ContractOrder.STATE_CREATED);
-									traderFollowUserOrderMapper.updateById(order);
-//									ApplicationUtil.executeUpdate(order);
+									if (user_contract_order == null) {
+										order.setState(TraderFollowUserOrder.STATE_SUBMITTED);
+										traderFollowUserOrderMapper.updateById(order);
+									}
 
 									if (user_contract_order != null) {
 										closeUserContractOrder(user_contract_order);
@@ -394,6 +466,8 @@
 							} catch (Exception e) {
 								logger.error("error:", e);
 							} finally {
+								String taskKey = buildTaskKey(contractOrder.getOrderNo(), order.getPartyId(), ContractApplyOrder.OFFSET_CLOSE);
+								followTaskKeys.remove(taskKey);
 							}
 							ThreadUtils.sleep(10);
 						}
@@ -426,7 +500,7 @@
 					trader_order.setCloseTime(new Date(contractOrder.getCloseTime()));
 					trader_order.setCreateTime(contractOrder.getCreateTime());
 					trader_order.setDirection(contractOrder.getDirection());
-					trader_order.setLeverRate(contractOrder.getLeverRate().doubleValue());
+					trader_order.setLeverRate(contractOrder.getLeverRate() == null ? 1D : contractOrder.getLeverRate().doubleValue());
 					trader_order.setState(contractOrder.getState());
 					trader_order.setVolumeOpen(contractOrder.getVolumeOpen().doubleValue());
 
@@ -461,70 +535,84 @@
 		 */
 
 		double follow_order_profit = 0;
-		if (traderFollowUserOrder != null && contractOrder.getProfit().doubleValue() > 0) {
-			Trader trader = traderService.findByPartyId(traderFollowUserOrder.getTraderPartyId());
-			follow_order_profit = Arith.mul(contractOrder.getProfit().doubleValue(), trader.getProfitShareRatio());
+		if (traderFollowUserOrder != null) {
+			traderFollowUserOrder.setState(contractOrder.getState());
+			update(traderFollowUserOrder);
 
-			FollowWallet wallet = followWalletService.saveWalletByPartyId(contractOrder.getPartyId());
-			double wallet_before = wallet.getMoney().doubleValue();
-			followWalletService.update(contractOrder.getPartyId(), Arith.sub(0, follow_order_profit));
+			TraderUser traderUser = traderUserService.saveTraderUserByPartyId(contractOrder.getPartyId());
+			traderUser.setProfit(Arith.add(traderUser.getProfit(), contractOrder.getProfit().doubleValue()));
+			traderUserService.update(traderUser);
 
-			FollowMoneyLog moneylog = new FollowMoneyLog();
-			moneylog.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
-			moneylog.setAmount_before(new BigDecimal(wallet_before));
-			moneylog.setAmount(BigDecimal.valueOf(Arith.sub(0, follow_order_profit)));
-			moneylog.setAmount_after(BigDecimal.valueOf(Arith.sub(wallet.getMoney().doubleValue(), follow_order_profit)));
-			moneylog.setLog("交易员订单号[" + traderFollowUserOrder.getTraderOrderNo() + "],跟单用户订单号["
-					+ contractOrder.getOrderNo() + "],跟单手续费[" + Arith.sub(0, follow_order_profit) + "]");
-			moneylog.setUserId(contractOrder.getPartyId());
-			moneylog.setWalletType(Constants.WALLET);
-			moneylog.setContent_type(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
-
-			followMoneyLogService.save(moneylog);
-
-			Wallet wallet_trader = walletService.saveWalletByPartyId(trader.getPartyId());
-			double wallet_trader_before = wallet_trader.getMoney().doubleValue();
-			walletService.update(wallet_trader.getUserId(), follow_order_profit);
-
-			MoneyLog moneylog_trader = new MoneyLog();
-			moneylog_trader.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
-			moneylog_trader.setAmount_before(new BigDecimal(wallet_trader_before));
-			moneylog_trader.setAmount(new BigDecimal(follow_order_profit));
-			moneylog_trader.setAmount_after(BigDecimal.valueOf(Arith.add(wallet_trader.getMoney().doubleValue(), follow_order_profit)));
-			moneylog_trader.setLog("交易员订单号[" + traderFollowUserOrder.getTraderOrderNo() + "],跟单用户订单号["
-					+ contractOrder.getOrderNo() + "],带单手续费收益[" + follow_order_profit + "]");
-			moneylog_trader.setUserId(wallet_trader.getUserId());
-			moneylog_trader.setWalletType(Constants.WALLET);
-			moneylog_trader.setContent_type(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
-
-			moneyLogService.save(moneylog_trader);
-
-			/**
-			 * 检查是否是跟单订单,如果是需要将TraderFollowUserOrder里的订单状态修改
-			 */
-
-			if (traderFollowUserOrder != null) {
-				traderFollowUserOrder.setState(contractOrder.getState());
-				update(traderFollowUserOrder);
-
-				/**
-				 * 将收益加入用户跟随累计
-				 */
-				TraderUser traderUser = traderUserService.saveTraderUserByPartyId(contractOrder.getPartyId());
-				traderUser.setProfit(Arith.add(traderUser.getProfit(), contractOrder.getProfit().doubleValue()));
-				traderUserService.update(traderUser);
-
-				TraderFollowUser traderFollowUser = traderFollowUserService.findByPartyIdAndTrader_partyId(
-						traderFollowUserOrder.getPartyId(),
-						traderFollowUserOrder.getTraderPartyId());
-				/**
-				 * 给用户跟随表添加累计金额
-				 */
+			TraderFollowUser traderFollowUser = traderFollowUserService.findByPartyIdAndTrader_partyId(
+					traderFollowUserOrder.getPartyId(),
+					traderFollowUserOrder.getTraderPartyId());
+			if (traderFollowUser != null) {
 				traderFollowUser.setProfit(Arith.add(traderFollowUser.getProfit(), contractOrder.getProfit().doubleValue()));
 				traderFollowUserService.update(traderFollowUser);
-
 			}
-			saveProfitBounsHandle(contractOrder);
+		}
+
+		if (traderFollowUserOrder != null) {
+			Trader trader = traderService.findByPartyId(traderFollowUserOrder.getTraderPartyId());
+			String commissionType = FollowCommissionType.normalizeOrLegacy(trader.getFollowCommissionType());
+			long closeSec = contractOrder.getCloseTime() != null && contractOrder.getCloseTime() > 0
+					? contractOrder.getCloseTime()
+					: System.currentTimeMillis() / 1000L;
+			if (FollowCommissionType.isDailyProfitPct(commissionType)) {
+				followCommissionService.accumulateDailyRealizedPnl(contractOrder.getPartyId().toString(),
+						traderFollowUserOrder.getTraderPartyId(), contractOrder.getProfit(), closeSec);
+			} else if (FollowCommissionType.isLegacy(commissionType) && contractOrder.getProfit().doubleValue() > 0) {
+				follow_order_profit = Arith.mul(contractOrder.getProfit().doubleValue(), trader.getProfitShareRatio());
+
+				Wallet wallet = walletService.saveWalletByPartyId(contractOrder.getPartyId());
+				double wallet_before = wallet.getMoney().doubleValue();
+				walletService.update(contractOrder.getPartyId(), Arith.sub(0, follow_order_profit));
+
+				String sym = contractOrder.getSymbol() == null ? "" : contractOrder.getSymbol().trim();
+				if (sym.isEmpty()) {
+					sym = "-";
+				}
+				String traderName = StringUtils.isEmptyString(trader.getName()) ? trader.getPartyId() : trader.getName().trim();
+				double sharePct = Arith.mul(trader.getProfitShareRatio(), 100D);
+				String feeStr = BigDecimal.valueOf(follow_order_profit).stripTrailingZeros().toPlainString();
+				String followerLog = String.format(
+						"[跟单佣金-盈利分润(经典模式)]交易对:%s|跟单用户平仓盈利分成|分润比例:%.4f%%|交易员委托单:%s|跟单持仓单:%s|主钱包扣款:USDT %s|带单员:%s",
+						sym, sharePct, traderFollowUserOrder.getTraderOrderNo(), contractOrder.getOrderNo(), feeStr, traderName);
+				String traderLog = String.format(
+						"[跟单佣金-盈利分润(经典模式)]交易对:%s|带单员分润入账|来源跟单用户平仓盈利|分润比例:%.4f%%|跟单持仓单:%s|对应交易员委托:%s|主钱包入账:USDT %s",
+						sym, sharePct, contractOrder.getOrderNo(), traderFollowUserOrder.getTraderOrderNo(), feeStr);
+
+				MoneyLog moneylog = new MoneyLog();
+				moneylog.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
+				moneylog.setAmountBefore(new BigDecimal(wallet_before));
+				moneylog.setAmount(BigDecimal.valueOf(Arith.sub(0, follow_order_profit)));
+				moneylog.setAmountAfter(BigDecimal.valueOf(Arith.sub(wallet.getMoney().doubleValue(), follow_order_profit)));
+				moneylog.setLog(followerLog + "|账变:跟随者主钱包扣款");
+				moneylog.setUserId(contractOrder.getPartyId());
+				moneylog.setWalletType(Constants.WALLET);
+				moneylog.setSymbol(Constants.WALLET_USDT);
+				moneylog.setContentType(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
+
+				moneyLogService.save(moneylog);
+
+				Wallet wallet_trader = walletService.saveWalletByPartyId(trader.getPartyId());
+				double wallet_trader_before = wallet_trader.getMoney().doubleValue();
+				walletService.update(wallet_trader.getUserId(), follow_order_profit);
+
+				MoneyLog moneylog_trader = new MoneyLog();
+				moneylog_trader.setCategory(Constants.MONEYLOG_CATEGORY_CONTRACT);
+				moneylog_trader.setAmountBefore(new BigDecimal(wallet_trader_before));
+				moneylog_trader.setAmount(new BigDecimal(follow_order_profit));
+				moneylog_trader.setAmountAfter(BigDecimal.valueOf(Arith.add(wallet_trader.getMoney().doubleValue(), follow_order_profit)));
+				moneylog_trader.setLog(traderLog + "|账变:带单员主钱包入账");
+				moneylog_trader.setUserId(wallet_trader.getUserId());
+				moneylog_trader.setWalletType(Constants.WALLET);
+				moneylog_trader.setSymbol(Constants.WALLET_USDT);
+				moneylog_trader.setContentType(Constants.MONEYLOG_CONTENT_FOLLOW_UP_FEE);
+
+				moneyLogService.save(moneylog_trader);
+				saveProfitBounsHandle(contractOrder);
+			}
 		}
 	}
 
@@ -540,6 +628,7 @@
 		return null;
 	}
 
+	@Override
 	public List<TraderFollowUserOrder> findByPartyIdAndTraderPartyIdAndState(String partyId, String trader_partyId,
 			String state) {
 //		StringBuffer queryString = new StringBuffer(
@@ -568,73 +657,89 @@
 		return null;
 	}
 
+	private boolean hasOpenFollowMapping(String partyId, String traderOrderNo) {
+		List<TraderFollowUserOrder> list = traderFollowUserOrderMapper.selectList(
+				Wrappers.<TraderFollowUserOrder>lambdaQuery()
+						.eq(TraderFollowUserOrder::getPartyId, partyId)
+						.eq(TraderFollowUserOrder::getTraderOrderNo, traderOrderNo)
+						.in(TraderFollowUserOrder::getState,
+								TraderFollowUserOrder.STATE_SUBMITTED,
+								TraderFollowUserOrder.STATE_PROCESSING_CLOSE));
+		return list != null && !list.isEmpty();
+	}
+
+	private String buildTaskKey(String traderOrderNo, String followerPartyId, String actionType) {
+		return traderOrderNo + ":" + followerPartyId + ":" + actionType;
+	}
+
 	/**
 	 * 跟单产生手续费,奖励给推荐人
 	 * 
 	 * @param entity
 	 */
 	public void saveFeeBounsHandle(ContractApplyOrder entity) {
-		List<UserRecom> recom_parents = userRecomService.getParents(entity.getPartyId());
-		if (recom_parents == null) {
-			return;
-		}
-		if (recom_parents.isEmpty()) {
-			return;
-		}
-		/**
-		 * 上级为空则直接结束
-		 */
-
-		if ("".equals(recom_parents.get(0).getRecomUserId()) || recom_parents.get(0).getRecomUserId() == null) {
-			return;
-		}
-
-		/**
-		 * 获取数据库奖金分成比例
-		 */
-//		String trade_follow_bonus_parameters = sysparaService.find("trade_follow_bonus_parameters").getValue();
-		String trade_follow_bonus_parameters = sysparaService.find("trade_follow_bonus_parameters").getSvalue();
-		String[] trade_follow_bonus_array = trade_follow_bonus_parameters.split(",");
-
-		/**
-		 * 判断有几个父级代理,最多不超过3个有奖励
-		 */
-		for (int i = 0; i < recom_parents.size(); i++) {
-			if (i >= 3) {
+		try {
+			List<UserRecom> recom_parents = userRecomService.getParents(entity.getPartyId());
+			if (recom_parents == null) {
 				return;
 			}
-			/**
-			 * 邀请人是正式用户和演示用户才加奖金
-			 */
-			User party = new User();
-			party = userService.cacheUserBy(recom_parents.get(i).getRecomUserId());
-			if (!"MEMBER".equals(party.getRoleName()) && !"GUEST".equals(party.getRoleName())) {
-				continue;
+			if (recom_parents.isEmpty()) {
+				return;
 			}
-			double pip_amount = Double.parseDouble(trade_follow_bonus_array[i]);
-			double get_money = Arith.mul(entity.getFee().doubleValue(), pip_amount);
+			if ("".equals(recom_parents.get(0).getRecomUserId()) || recom_parents.get(0).getRecomUserId() == null) {
+				return;
+			}
 
-			Wallet wallet = walletService.saveWalletByPartyId(recom_parents.get(i).getRecomUserId());
-			double amount_before = wallet.getMoney().doubleValue();
-//				wallet.setMoney(Arith.add(wallet.getMoney(), get_money));
-			walletService.update(wallet.getUserId(), get_money);
+			Syspara bonusPara = sysparaService.find("trade_follow_bonus_parameters");
+			if (bonusPara == null || StringUtils.isEmptyString(bonusPara.getSvalue())) {
+				logger.warn("saveFeeBounsHandle: syspara trade_follow_bonus_parameters missing or empty, skip");
+				return;
+			}
+			String trade_follow_bonus_parameters = bonusPara.getSvalue().trim();
+			String[] trade_follow_bonus_array = trade_follow_bonus_parameters.split(",");
+			if (trade_follow_bonus_array.length == 0) {
+				return;
+			}
 
-			/**
-			 * 保存资金日志
-			 */
-			MoneyLog moneyLog = new MoneyLog();
-			moneyLog.setCategory(Constants.MONEYLOG_CATEGORY_REWARD);
-			moneyLog.setAmount_before(new BigDecimal(amount_before));
-			moneyLog.setAmount(new BigDecimal(get_money));
-			moneyLog.setAmount_after(BigDecimal.valueOf(Arith.add(wallet.getMoney().doubleValue(), get_money)));
-			moneyLog.setLog("第" + (i + 1) + "代用户跟单产生了交易,手续费奖励[" + get_money + "]");
-			moneyLog.setUserId(recom_parents.get(i).getRecomUserId());
-			moneyLog.setWalletType(Constants.WALLET);
-			moneyLog.setContent_type(Constants.MONEYLOG_CONTENT_REWARD);
-			moneyLogService.save(moneyLog);
+			for (int i = 0; i < recom_parents.size(); i++) {
+				if (i >= 3) {
+					return;
+				}
+				if (i >= trade_follow_bonus_array.length) {
+					logger.warn("saveFeeBounsHandle: bonus ratio array shorter than parent index " + i + ", skip rest");
+					return;
+				}
+				User party = userService.cacheUserBy(recom_parents.get(i).getRecomUserId());
+				if (party == null || (!"MEMBER".equals(party.getRoleName()) && !"GUEST".equals(party.getRoleName()))) {
+					continue;
+				}
+				String ratioStr = trade_follow_bonus_array[i] == null ? "" : trade_follow_bonus_array[i].trim();
+				if (ratioStr.isEmpty()) {
+					continue;
+				}
+				double pip_amount = Double.parseDouble(ratioStr);
+				double get_money = Arith.mul(entity.getFee().doubleValue(), pip_amount);
 
+				Wallet wallet = walletService.saveWalletByPartyId(recom_parents.get(i).getRecomUserId());
+				double amount_before = wallet.getMoney().doubleValue();
+				walletService.update(wallet.getUserId(), get_money);
+
+				MoneyLog moneyLog = new MoneyLog();
+				moneyLog.setCategory(Constants.MONEYLOG_CATEGORY_REWARD);
+				moneyLog.setAmountBefore(new BigDecimal(amount_before));
+				moneyLog.setAmount(new BigDecimal(get_money));
+				moneyLog.setAmountAfter(BigDecimal.valueOf(Arith.add(wallet.getMoney().doubleValue(), get_money)));
+				moneyLog.setLog("第" + (i + 1) + "代用户跟单产生了交易,手续费奖励[" + get_money + "]");
+				moneyLog.setUserId(recom_parents.get(i).getRecomUserId());
+				moneyLog.setWalletType(Constants.WALLET);
+				moneyLog.setSymbol(Constants.WALLET_USDT);
+				moneyLog.setContentType(Constants.MONEYLOG_CONTENT_REWARD);
+				moneyLogService.save(moneyLog);
+			}
+		} catch (Exception e) {
+			logger.error("saveFeeBounsHandle failed (ignored so follow open is not rolled into markFollowOpenFailed), orderNo="
+					+ (entity != null ? entity.getOrderNo() : "null"), e);
 		}
-
 	}
 
 	/**
@@ -693,13 +798,14 @@
 			 */
 			MoneyLog moneyLog = new MoneyLog();
 			moneyLog.setCategory(Constants.MONEYLOG_CATEGORY_REWARD);
-			moneyLog.setAmount_before(new BigDecimal(amount_before));
+			moneyLog.setAmountBefore(new BigDecimal(amount_before));
 			moneyLog.setAmount(new BigDecimal(get_money));
-			moneyLog.setAmount_after(BigDecimal.valueOf(Arith.add(wallet.getMoney().doubleValue(), get_money)));
+			moneyLog.setAmountAfter(BigDecimal.valueOf(Arith.add(wallet.getMoney().doubleValue(), get_money)));
 			moneyLog.setLog("第" + (i + 1) + "代用户跟单产生了交易,分红奖励[" + get_money + "]");
 			moneyLog.setUserId(recom_parents.get(i).getRecomUserId());
 			moneyLog.setWalletType(Constants.WALLET);
-			moneyLog.setContent_type(Constants.MONEYLOG_CONTENT_REWARD);
+			moneyLog.setSymbol(Constants.WALLET_USDT);
+			moneyLog.setContentType(Constants.MONEYLOG_CONTENT_REWARD);
 			moneyLogService.save(moneyLog);
 
 		}
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);
+		}
+	}
+
 }
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java
index a69eb76..ab1d42d 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/trader/impl/TraderServiceImpl.java
@@ -4,12 +4,15 @@
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.yami.trading.bean.contract.domain.ContractOrder;
+import com.yami.trading.bean.contract.dto.TraderOwnClosedAggDTO;
 import com.yami.trading.bean.trader.domain.Trader;
 import com.yami.trading.common.constants.Constants;
 import com.yami.trading.common.util.Arith;
 import com.yami.trading.common.util.StringUtils;
 import com.yami.trading.dao.trader.TraderMapper;
+import com.yami.trading.service.contract.ContractOrderService;
 import com.yami.trading.service.trader.TraderService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -18,12 +21,16 @@
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.stream.Collectors;
 
 @Service
 public class TraderServiceImpl implements TraderService {
 
 	@Resource
 	private TraderMapper traderMapper;
+	@Resource
+	@Lazy
+	private ContractOrderService contractOrderService;
 
 	public Trader findById(String id) {
 
@@ -32,9 +39,12 @@
 
 	public Trader findByPartyId(String partyId) {
 		LambdaQueryWrapper<Trader> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-		lambdaQueryWrapper.eq(Trader::getPartyId, partyId);
-		Trader trader = traderMapper.selectOne(lambdaQueryWrapper);
-		return trader;
+		lambdaQueryWrapper.eq(Trader::getPartyId, partyId)
+				.eq(Trader::getDelFlag, 0)
+				.orderByDesc(Trader::getChecked)
+				.orderByDesc(Trader::getCreateTime)
+				.last("limit 1");
+		return traderMapper.selectOne(lambdaQueryWrapper);
 	}
 
 	@Override
@@ -73,9 +83,11 @@
 //			lambdaQueryWrapper.orderByDesc(Trader::getCreate_time);
 //		}
 		lambdaQueryWrapper.eq(Trader::getDelFlag, 0);
+		lambdaQueryWrapper.eq(Trader::getChecked, 1);
+		lambdaQueryWrapper.eq(Trader::getState, "1");
 		lambdaQueryWrapper.orderByDesc(Trader::getCreateTime);
 
-		IPage<Trader> page = traderMapper.selectPage(pageparam, new LambdaQueryWrapper<>());
+		IPage<Trader> page = traderMapper.selectPage(pageparam, lambdaQueryWrapper);
 
 //		Page page = this.pagedQueryDao.pagedQuerySQL(pageNo, pageSize, queryString.toString(), parameters);
 		List<Map<String, Object>> data = this.bulidData(page.getRecords());
@@ -98,6 +110,12 @@
 		if (traders == null) {
 			return result_traders;
 		}
+		List<String> partyIds = traders.stream()
+				.filter(Objects::nonNull)
+				.map(Trader::getPartyId)
+				.filter(pid -> !StringUtils.isNullOrEmpty(pid))
+				.collect(Collectors.toList());
+		Map<String, TraderOwnClosedAggDTO> closedAggByParty = contractOrderService.mapClosedTraderOwnAggByPartyIds(partyIds);
 		for (int i = 0; i < traders.size(); i++) {
 			Map<String, Object> map = new HashMap<String, Object>();
 //			Trader entity = BeanUtil.mapToBean(traders.get(i), Trader.class, true);
@@ -141,7 +159,22 @@
 					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())));
+			TraderOwnClosedAggDTO closedAgg = closedAggByParty.get(entity.getPartyId());
+			double closedProfitSum = 0D;
+			double closedMarginSum = 0D;
+			if (closedAgg != null) {
+				if (closedAgg.getClosedProfitSum() != null) {
+					closedProfitSum = closedAgg.getClosedProfitSum().doubleValue();
+				}
+				if (closedAgg.getClosedMarginSum() != null) {
+					closedMarginSum = closedAgg.getClosedMarginSum().doubleValue();
+				}
+			}
+			double historyProfit = Arith.add(closedProfitSum, entity.getDeviationProfit());
+			double historyAmountBasis = closedMarginSum > 0D
+					? Arith.add(closedMarginSum, entity.getDeviationOrderAmount())
+					: Arith.add(entity.getOrderAmount(), entity.getDeviationOrderAmount());
+			map.put("profit", df2.format(historyProfit));
 
 			map.put("order_profit", (int) Arith.add(entity.getOrderProfit(), entity.getDeviationOrderProfit()));
 
@@ -188,6 +221,47 @@
 					Arith.mul(entity.getProfitRatio(), 100))));
 
 			map.put("profit_share_ratio", df2.format(Arith.mul(entity.getProfitShareRatio(), 100)));
+			map.put("follow_commission_type",
+					com.yami.trading.bean.trader.FollowCommissionType.normalizeOrLegacy(entity.getFollowCommissionType()));
+			map.put("follow_commission_monthly_amount",
+					entity.getFollowCommissionMonthlyAmount() == null ? "0"
+							: entity.getFollowCommissionMonthlyAmount().stripTrailingZeros().toPlainString());
+			map.put("follow_commission_daily_pct", df2.format(Arith.mul(entity.getFollowCommissionDailyPct(), 100)));
+
+			// 累计收益/收益率:历史=合约表已平仓全品种盈亏+偏差,分母优先已平仓保证金合计(与当前持仓 deposit 一致)+偏差;再加实时持仓
+			double openProfit = 0D;
+			double openDeposit = 0D;
+			int openPositionCount = 0;
+			List<ContractOrder> openOrders = contractOrderService.findSubmittedTraderOwn(entity.getPartyId(), "");
+			if (openOrders != null && !openOrders.isEmpty()) {
+				openPositionCount = openOrders.size();
+				for (ContractOrder one : openOrders) {
+					// submitted 持仓利润实时值在缓存中,先包裹后再聚合,口径与详情页一致
+					contractOrderService.wrapProfit(one);
+					openProfit = Arith.add(openProfit, one.getProfit() == null ? 0D : one.getProfit().doubleValue());
+					openDeposit = Arith.add(openDeposit, one.getDeposit() == null ? 0D : one.getDeposit().doubleValue());
+				}
+			}
+			double totalProfit = Arith.add(historyProfit, openProfit);
+			double totalRatio = 0D;
+			double totalAmount = Arith.add(historyAmountBasis, openDeposit);
+			if (totalAmount > 0D) {
+				totalRatio = Arith.mul(Arith.div(totalProfit, totalAmount), 100);
+			}
+			map.put("history_profit", df2.format(historyProfit));
+			double historyProfitRatioOnly = 0D;
+			if (historyAmountBasis > 0D) {
+				historyProfitRatioOnly = Arith.mul(Arith.div(historyProfit, historyAmountBasis), 100);
+			} else {
+				historyProfitRatioOnly = Arith.add(Arith.mul(entity.getDeviationProfitRatio(), 100),
+						Arith.mul(entity.getProfitRatio(), 100));
+			}
+			map.put("history_profit_ratio", df2.format(historyProfitRatioOnly));
+			map.put("open_profit", df2.format(openProfit));
+			map.put("open_deposit", df2.format(openDeposit));
+			map.put("open_position_count", openPositionCount);
+			map.put("total_profit", df2.format(totalProfit));
+			map.put("total_profit_ratio", df2.format(totalRatio));
 
 			result_traders.add(map);
 		}
diff --git a/trading-order-service/src/main/java/com/yami/trading/service/user/impl/QRGenerateServiceImpl.java b/trading-order-service/src/main/java/com/yami/trading/service/user/impl/QRGenerateServiceImpl.java
index 64d1c10..158ea53 100644
--- a/trading-order-service/src/main/java/com/yami/trading/service/user/impl/QRGenerateServiceImpl.java
+++ b/trading-order-service/src/main/java/com/yami/trading/service/user/impl/QRGenerateServiceImpl.java
@@ -1,6 +1,7 @@
 package com.yami.trading.service.user.impl;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -49,11 +50,11 @@
         int width = 260;
         int height = 260;
         String format = "png";
-        Map hints = new HashMap();
+        Map<EncodeHintType, Object> hints = new HashMap<>();
         hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
         try {
             BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
-            MatrixToImageWriter.writeToFile(bitMatrix, format, file);
+            writeQrImageToFile(bitMatrix, format, file);
         } catch (Exception e) {
             log.error("write to image error:", e);
         }
@@ -89,11 +90,11 @@
         int width = 260;
         int height = 260;
         String format = "png";
-        Map hints = new HashMap();
+        Map<EncodeHintType, Object> hints = new HashMap<>();
         hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
         try {
             BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
-            MatrixToImageWriter.writeToFile(bitMatrix, format, file);
+            writeQrImageToFile(bitMatrix, format, file);
         } catch (Exception e) {
         	log.error("write to image error:", e);
         }
@@ -111,13 +112,13 @@
         int width = 185;
         int height = 185;
         String format = "png";
-        Map hints = new HashMap();
+        Map<EncodeHintType, Object> hints = new HashMap<>();
         hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
         hints.put(EncodeHintType.MARGIN, 1);// 二维码空白区域,最小为0也有白边,只是很小,最小是6像素左右
 
         try {
             BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
-            MatrixToImageWriter.writeToFile(bitMatrix, format, file);
+            writeQrImageToFile(bitMatrix, format, file);
         } catch (Exception e) {
         	log.error("write to image error:", e);
         }
@@ -151,19 +152,15 @@
     public String generateWithdraw(String content, String address) {
         String image_name = "/qr/" + content + ".png";
         String filepath = Constants.IMAGES_DIR + image_name;
-        File file=new File( Constants.IMAGES_DIR);
-        if (!file.isDirectory()){
-            file.mkdirs();
-        }
-        file = new File(filepath);
+        File file = new File(filepath);
         int width = 260;
         int height = 260;
         String format = "png";
-        Map hints = new HashMap();
+        Map<EncodeHintType, Object> hints = new HashMap<>();
         hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
         try {
             BitMatrix bitMatrix = new MultiFormatWriter().encode(address, BarcodeFormat.QR_CODE, width, height, hints);
-            MatrixToImageWriter.writeToFile(bitMatrix, format, file);
+            writeQrImageToFile(bitMatrix, format, file);
         } catch (Exception e) {
         	log.error("write to image error:", e);
         }
@@ -188,4 +185,18 @@
 
         return list_image;
     }
+
+    private void writeQrImageToFile(BitMatrix bitMatrix, String format, File file) throws IOException {
+        if (bitMatrix == null) {
+            throw new IOException("bitMatrix is null");
+        }
+        if (file == null) {
+            throw new IOException("target file is null");
+        }
+        File parent = file.getParentFile();
+        if (parent != null && !parent.exists() && !parent.mkdirs()) {
+            throw new IOException("failed to create qr image directory: " + parent.getAbsolutePath());
+        }
+        MatrixToImageWriter.writeToFile(bitMatrix, format, file);
+    }
 }
diff --git a/trading-order-service/src/main/resources/mapper/contract/ContractOrderMapper.xml b/trading-order-service/src/main/resources/mapper/contract/ContractOrderMapper.xml
index e482124..02da1e6 100644
--- a/trading-order-service/src/main/resources/mapper/contract/ContractOrderMapper.xml
+++ b/trading-order-service/src/main/resources/mapper/contract/ContractOrderMapper.xml
@@ -165,6 +165,18 @@
         </foreach>
     </update>
 
-
+    <select id="sumClosedTraderOwnAggByPartyIds" resultType="com.yami.trading.bean.contract.dto.TraderOwnClosedAggDTO">
+        SELECT party_id AS partyId,
+               COALESCE(SUM(IFNULL(profit, 0)), 0) AS closedProfitSum,
+               COALESCE(SUM(IFNULL(deposit_open, 0)), 0) AS closedMarginSum
+        FROM t_contract_order
+        WHERE state = 'created'
+          AND (follow IS NULL OR follow != 1)
+          AND party_id IN
+        <foreach collection="partyIds" item="pid" open="(" separator="," close=")">
+            #{pid}
+        </foreach>
+        GROUP BY party_id
+    </select>
 
 </mapper>
diff --git a/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserMapper.xml b/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserMapper.xml
index 78430d7..007ffb5 100644
--- a/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserMapper.xml
+++ b/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserMapper.xml
@@ -10,6 +10,7 @@
         LEFT JOIN TZ_USER party ON  party.user_id  = trader_user.PARTY_ID
         LEFT JOIN T_TRADER trader ON  trader.PARTY_ID   = trader_user.TRADER_PARTY_ID
         WHERE 1 = 1
+        AND trader_user.DEL_FLAG = 0
         <if test="null != name and name != ''">
             AND trader.NAME =#{name}
         </if>
diff --git a/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserOrderMapper.xml b/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserOrderMapper.xml
index b085c46..d620873 100644
--- a/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserOrderMapper.xml
+++ b/trading-order-service/src/main/resources/mapper/trader/TraderFollowUserOrderMapper.xml
@@ -7,11 +7,13 @@
                orders.STATE state,orders.FEE fee,orders.PROFIT profit,orders.DEPOSIT deposit,orders.DEPOSIT_OPEN deposit_open,orders.CLOSE_AVG_PRICE close_avg_price,
                DATE_FORMAT(orders.CLOSE_TIME, '%Y-%m-%d %H:%i:%S') closeTime,DATE_FORMAT(orders.CREATE_TIME, '%Y-%m-%d %H:%i:%S') createTime,
                orders.VOLUME_OPEN volume_open,orders.VOLUME volume,item.NAME itemname,trader_user_order.USER_ORDER_NO order_no,
-               orders.stop_price_profit stop_price_profit, orders.stop_price_loss stop_price_loss, orders.lever_rate lever_rate, orders.force_close_price force_close_price,
-               trader_user_order.TRADER_PARTY_ID trader_party_id
+               orders.stop_price_profit stop_price_profit, orders.stop_price_loss stop_price_loss, orders.force_close_price force_close_price,
+               trader_user_order.TRADER_PARTY_ID trader_party_id,
+               IFNULL(tr.NAME, '') follow_trader_name
         FROM T_TRADER_FOLLOW_USER_ORDER trader_user_order
         INNER JOIN T_CONTRACT_ORDER orders ON orders.ORDER_NO  = trader_user_order.USER_ORDER_NO
         INNER JOIN T_ITEM item ON orders.SYMBOL = item.SYMBOL
+        LEFT JOIN T_TRADER tr ON tr.PARTY_ID = trader_user_order.TRADER_PARTY_ID
         WHERE 1 = 1
         <if test="null != state and state != ''">
             AND orders.state =#{state}
@@ -21,6 +23,18 @@
         LIMIT #{pageNo}, #{pageSize}
     </select>
 
+    <select id="countListDatas" resultType="java.lang.Long">
+        SELECT COUNT(1)
+        FROM T_TRADER_FOLLOW_USER_ORDER trader_user_order
+        INNER JOIN T_CONTRACT_ORDER orders ON orders.ORDER_NO = trader_user_order.USER_ORDER_NO
+        INNER JOIN T_ITEM item ON orders.SYMBOL = item.SYMBOL
+        WHERE 1 = 1
+        <if test="null != state and state != ''">
+            AND orders.state = #{state}
+        </if>
+        AND trader_user_order.PARTY_ID = #{partyId}
+    </select>
+
     <select id="listMDatas" resultType="java.util.Map">
         SELECT trader.NAME trader_name,trader_user.USERNAME username,party.user_code usercode,party.role_name rolename,trader_user_order.UUID id,trader_user_order.STATE state,
         trader_user_order.VOLUME  volume,trader_user_order.USER_ORDER_NO user_order_no,trader_user_order.TRADER_ORDER_NO trader_order_no ,DATE_FORMAT(trader_user_order.CREATE_TIME, '%Y-%m-%d %H:%i:%S') create_time
diff --git a/trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml b/trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml
index 5686963..2d931d2 100644
--- a/trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml
+++ b/trading-order-service/src/main/resources/mapper/trader/TraderMapper.xml
@@ -7,7 +7,12 @@
         trader.PROFIT profit ,trader.PROFIT_RATIO profit_ratio ,trader.ORDER_PROFIT  order_profit,trader.ORDER_LOSS order_loss, trader.ORDER_SUM order_sum, trader.FOLLOWER_SUM follower_sum,
         trader.FOLLOWER_NOW follower_now,trader.DEVIATION_PROFIT deviation_profit ,trader.DEVIATION_PROFIT_RATIO deviation_profit_ratio,trader.DEVIATION_ORDER_PROFIT  deviation_order_profit,
         trader.DEVIATION_ORDER_LOSS deviation_order_loss, trader.DEVIATION_ORDER_SUM deviation_order_sum,trader.DEVIATION_FOLLOWER_SUM deviation_follower_sum,trader.DEVIATION_FOLLOWER_NOW deviation_follower_now,
-        trader.PROFIT_SHARE_RATIO profit_share_ratio,trader.STATE state,trader.FOLLOWER_MAX  follower_max,trader.IMG img  ,DATE_FORMAT(trader.CREATE_TIME, '%Y-%m-%d %H:%i:%S') create_time, trader.CHECKED checked,
+        trader.PROFIT_SHARE_RATIO profit_share_ratio,trader.STATE state,trader.FOLLOWER_MAX  follower_max,
+        trader.FOLLOW_VOLUMN_MIN follow_volumn_min,
+        trader.FOLLOW_COMMISSION_TYPE follow_commission_type,
+        trader.FOLLOW_COMMISSION_MONTHLY_AMOUNT follow_commission_monthly_amount,
+        trader.FOLLOW_COMMISSION_DAILY_PCT follow_commission_daily_pct,
+        trader.IMG img  ,DATE_FORMAT(trader.CREATE_TIME, '%Y-%m-%d %H:%i:%S') create_time, trader.CHECKED checked,
         trader.DEL_FLAG del_flag
         FROM T_TRADER trader
         LEFT JOIN TZ_USER party ON  party.user_id  = trader.PARTY_ID
diff --git a/trading-order-service/src/main/resources/mapper/trader/TraderOrderMapper.xml b/trading-order-service/src/main/resources/mapper/trader/TraderOrderMapper.xml
index 25e56eb..408117e 100644
--- a/trading-order-service/src/main/resources/mapper/trader/TraderOrderMapper.xml
+++ b/trading-order-service/src/main/resources/mapper/trader/TraderOrderMapper.xml
@@ -7,7 +7,7 @@
         SELECT trader_order.SYMBOL symbol, trader_order.TRADE_AVG_PRICE trade_avg_price, trader_order.DIRECTION direction, trader_order.STATE state,trader_order.PROFIT profit,
                trader_order.CLOSE_AVG_PRICE close_avg_price,trader_order.CHANGE_RATIO change_ratio , DATE_FORMAT(trader_order.CLOSE_TIME, '%Y-%m-%d %H:%i:%S') close_time,
                DATE_FORMAT(trader_order.CREATE_TIME, '%Y-%m-%d %H:%i:%S') create_time, trader_order.VOLUME_OPEN volume_open,item.NAME itemname, trader_order.ORDER_NO order_no, trader_order.DEL_FLAG del_flag,
-               trader_order.LEVER_RATE lever_rate, trader.FOLLOWER_NOW follow_now, trader.FOLLOWER_MAX follow_max
+               trader.FOLLOWER_NOW follow_now, trader.FOLLOWER_MAX follow_max
         FROM T_TRADER_ORDER trader_order
         LEFT JOIN T_ITEM item ON trader_order.SYMBOL = item.SYMBOL
         LEFT JOIN T_TRADER trader ON  trader.PARTY_ID   = trader_order.PARTY_ID
@@ -21,7 +21,7 @@
         SELECT trader.NAME trader_name,party.user_name username,party.user_code usercode,party.role_name rolename,trader_order.UUID id,trader_order.STATE state,
                trader_order.VOLUME_OPEN  volume_open,trader_order.ORDER_NO order_no,DATE_FORMAT(trader_order.CREATE_TIME, '%Y-%m-%d %H:%i:%S') create_time,DATE_FORMAT(trader_order.CLOSE_TIME, '%Y-%m-%d %H:%i:%S') close_time,
                trader_order.TRADE_AVG_PRICE trade_avg_price,trader_order.close_avg_price CLOSE_AVG_PRICE,trader_order.CLOSE_TIME close_time,trader_order.CHANGE_RATIO change_ratio,
-               trader_order.DIRECTION direction,trader_order.PROFIT profit,item.NAME itemname, trader_order.DEL_FLAG del_flag, trader_order.LEVER_RATE lever_rate, trader.FOLLOWER_NOW follow_now, trader.FOLLOWER_MAX follow_max
+               trader_order.DIRECTION direction,trader_order.PROFIT profit,item.NAME itemname, trader_order.DEL_FLAG del_flag, trader.FOLLOWER_NOW follow_now, trader.FOLLOWER_MAX follow_max
         FROM T_TRADER_ORDER trader_order
         LEFT JOIN TZ_USER party ON  party.user_id  = trader_order.PARTY_ID
         LEFT JOIN T_TRADER trader ON  trader.PARTY_ID   = trader_order.PARTY_ID

--
Gitblit v1.9.3