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