1
zj
yesterday befbf57e4112d07003bff18102f556a1e5a154de
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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);
    }
}