1
zj
14 hours ago d615fc515fc52d6ed970c11d59a017e48de4be32
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
package com.nq.utils;
 
import com.nq.utils.stock.GetStayDays;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
 
public final class StayFeeUtil {
 
    private static final BigDecimal WAN_UNIT = new BigDecimal("10000");
 
    public static final LocalTime CHARGE_TIME = LocalTime.of(14, 50);
 
    private StayFeeUtil() {
    }
 
    /**
     * 递延费计费基数:不足1万按1万;超过1万按每1万向上取整(如5999→10000,35999→40000)。
     */
    public static BigDecimal calcBillingMarketValue(BigDecimal marketValue) {
        BigDecimal mv = marketValue == null ? BigDecimal.ZERO : marketValue;
        if (mv.compareTo(BigDecimal.ZERO) <= 0) {
            return WAN_UNIT;
        }
        return mv.divide(WAN_UNIT, 0, RoundingMode.CEILING).multiply(WAN_UNIT);
    }
 
    public static BigDecimal calcDailyStayFee(BigDecimal marketValue, BigDecimal stayFeeRate) {
        BigDecimal base = calcBillingMarketValue(marketValue);
        BigDecimal rate = stayFeeRate == null ? BigDecimal.ZERO : stayFeeRate;
        return base.multiply(rate).setScale(2, RoundingMode.HALF_UP);
    }
 
    public static LocalDate toLocalDate(Date date) {
        if (date == null) {
            return LocalDate.now();
        }
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
 
    /** 自然日:买入日不计,自次日起每个自然日计 1 天(含周末、法定假日本身) */
    public static int getCalendarDaysSinceBuy(Date buyOrderTime) {
        LocalDate buy = toLocalDate(GetStayDays.getBeginDate(buyOrderTime));
        LocalDate today = LocalDate.now();
        if (!buy.isBefore(today)) {
            return 0;
        }
        return (int) ChronoUnit.DAYS.between(buy, today);
    }
 
    /** 交易日:买入日不计,仅统计非周末且非法定假日本身的工作日 */
    public static int getTradingDaysSinceBuy(Date buyOrderTime) {
        LocalDate buy = toLocalDate(GetStayDays.getBeginDate(buyOrderTime));
        LocalDate today = LocalDate.now();
        if (!buy.isBefore(today)) {
            return 0;
        }
        int count = 0;
        LocalDate d = buy.plusDays(1);
        while (!d.isAfter(today)) {
            if (TradingDayUtil.isTradingDay(d)) {
                count++;
            }
            d = d.plusDays(1);
        }
        return count;
    }
 
    /**
     * 应计费天数(买入日不计):
     * 节假日休市开 → 仅交易日;关 → 每个自然日(不管节假日)。
     */
    public static int getChargeableDaysSinceBuy(Date buyOrderTime, boolean respectHoliday) {
        return respectHoliday ? getTradingDaysSinceBuy(buyOrderTime) : getCalendarDaysSinceBuy(buyOrderTime);
    }
 
    /**
     * 本次定时任务应扣天数(最多 1 天,不补扣历史):
     * 开 → 仅交易日扣 1 天;关 → 每个自然日扣 1 天(节假日也扣,不补扣周末/假日前遗漏的天数)。
     */
    public static int getDaysToChargeThisRun(Date buyOrderTime, Integer orderStayDays, boolean respectHoliday) {
        int chargeableDays = getChargeableDaysSinceBuy(buyOrderTime, respectHoliday);
        int billed = orderStayDays == null ? 0 : orderStayDays;
        if (chargeableDays < 1 || billed >= chargeableDays) {
            return 0;
        }
        if (respectHoliday && !TradingDayUtil.isTradingDay(LocalDate.now())) {
            return 0;
        }
        return 1;
    }
 
    public static boolean isEligibleForCharge(Date buyOrderTime, Integer orderStayDays, boolean respectHoliday) {
        return getDaysToChargeThisRun(buyOrderTime, orderStayDays, respectHoliday) > 0;
    }
 
    public static long getNextStayFeeTimestamp(Date buyOrderTime, Integer orderStayDays, boolean respectHoliday) {
        LocalDate buy = toLocalDate(GetStayDays.getBeginDate(buyOrderTime));
        LocalDate today = LocalDate.now();
        LocalDateTime now = LocalDateTime.now();
        int chargeableDays = getChargeableDaysSinceBuy(buyOrderTime, respectHoliday);
        int billed = orderStayDays == null ? 0 : orderStayDays;
 
        LocalDate targetDate;
        if (respectHoliday) {
            if (chargeableDays < 1) {
                targetDate = firstTradingDayOnOrAfter(buy.plusDays(1));
            } else if (billed < chargeableDays
                    && TradingDayUtil.isTradingDay(today)
                    && now.toLocalTime().isBefore(CHARGE_TIME)) {
                targetDate = today;
            } else {
                targetDate = firstTradingDayOnOrAfter(today.plusDays(1));
            }
        } else {
            if (chargeableDays < 1) {
                targetDate = buy.plusDays(1);
            } else if (billed < chargeableDays && now.toLocalTime().isBefore(CHARGE_TIME)) {
                targetDate = today;
            } else {
                targetDate = today.plusDays(1);
            }
        }
        LocalDateTime next = LocalDateTime.of(targetDate, CHARGE_TIME);
        return next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }
 
    public static long getStayFeeCountdownSec(long nextStayFeeTimestamp) {
        long diff = nextStayFeeTimestamp - System.currentTimeMillis();
        return diff <= 0 ? 0 : diff / 1000;
    }
 
    /** 节假日休市开:非交易日不跑递延费任务 */
    public static boolean shouldSkipStayFeeTaskToday(boolean respectHoliday) {
        return respectHoliday && !TradingDayUtil.isTradingDay(LocalDate.now());
    }
 
    private static LocalDate firstTradingDayOnOrAfter(LocalDate from) {
        LocalDate d = from;
        while (!TradingDayUtil.isTradingDay(d)) {
            d = d.plusDays(1);
        }
        return d;
    }
}