gaoluyang
昨天 90b3ad820eea45d021835197eac5bc3ad55b12ed
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package cn.iocoder.yudao.module.finance.service.ledger;
 
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerReqVO;
import cn.iocoder.yudao.module.finance.controller.admin.ledger.vo.FinLedgerRespVO;
import cn.iocoder.yudao.module.finance.dal.dataobject.ledger.FinLedgerEntryDO;
import cn.iocoder.yudao.module.finance.dal.dataobject.subject.FinAccountSubjectDO;
import cn.iocoder.yudao.module.finance.dal.mysql.ledger.FinLedgerMapper;
import cn.iocoder.yudao.module.finance.enums.FinBalanceDirectionEnum;
import cn.iocoder.yudao.module.finance.enums.FinLedgerRowTypeEnum;
import cn.iocoder.yudao.module.finance.enums.FinVoucherStatusEnum;
import cn.iocoder.yudao.module.finance.service.subject.FinAccountSubjectService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_MONTH_RANGE_INVALID;
import static cn.iocoder.yudao.module.finance.enums.ErrorCodeConstants.LEDGER_SUBJECT_NOT_EXISTS;
 
/**
 * 总账 / 明细账 Service 实现类
 *
 * 口径说明:
 * 1. 只统计已过账(posted)的凭证,未过账、已作废的凭证不计入账簿;
 * 2. 余额一律「借方正、贷方负」,期初余额 = 起始月份之前所有已过账分录的借贷净额;
 * 3. 科目的下级科目由 parentId 递归收集(不使用科目编码前缀匹配,避免把形近编码误算进来)。
 *
 * @author 超级管理员
 */
@Service
@Validated
public class FinLedgerServiceImpl implements FinLedgerService {
 
    @Resource
    private FinLedgerMapper ledgerMapper;
    @Resource
    private FinAccountSubjectService accountSubjectService;
 
    @Override
    public List<FinLedgerRespVO> getGeneralLedgerList(FinLedgerReqVO reqVO) {
        // 1. 校验科目与期间
        FinAccountSubjectDO subject = validateSubject(reqVO);
        LocalDate startDate = reqVO.getStartMonth().atDay(1);
        LocalDate endDate = reqVO.getEndMonth().atEndOfMonth();
        validateMonthRange(startDate, endDate);
        Set<String> subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren());
 
        // 2. 期初余额
        List<FinLedgerRespVO> rows = new ArrayList<>();
        BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate,
                FinVoucherStatusEnum.POSTED.getStatus()));
        rows.add(buildOpeningRow(subject, startDate, balance));
 
        // 3. 按月汇总
        List<FinLedgerEntryDO> entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate,
                FinVoucherStatusEnum.POSTED.getStatus());
        Map<String, List<FinLedgerEntryDO>> monthMap = entries.stream()
                .collect(Collectors.groupingBy(entry -> YearMonth.from(entry.getVoucherDate()).toString()));
        BigDecimal totalDebit = BigDecimal.ZERO;
        BigDecimal totalCredit = BigDecimal.ZERO;
        YearMonth endMonth = YearMonth.from(endDate);
        for (YearMonth month = YearMonth.from(startDate); !month.isAfter(endMonth); month = month.plusMonths(1)) {
            List<FinLedgerEntryDO> monthEntries = monthMap.getOrDefault(month.toString(), List.of());
            BigDecimal debit = sum(monthEntries, true);
            BigDecimal credit = sum(monthEntries, false);
            balance = balance.add(debit).subtract(credit);
            totalDebit = totalDebit.add(debit);
            totalCredit = totalCredit.add(credit);
            LocalDate monthEndDate = minDate(month.atEndOfMonth(), endDate);
            rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, month.toString(), monthEndDate, subject, null,
                    FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), debit, credit, balance));
        }
 
        // 4. 本期累计
        rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null,
                FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance));
        return rows;
    }
 
    @Override
    public List<FinLedgerRespVO> getDetailLedgerList(FinLedgerReqVO reqVO) {
        // 1. 校验科目与期间
        FinAccountSubjectDO subject = validateSubject(reqVO);
        LocalDate startDate = reqVO.getStartMonth().atDay(1);
        LocalDate endDate = reqVO.getEndMonth().atEndOfMonth();
        validateMonthRange(startDate, endDate);
        Set<String> subjectCodes = buildSubjectCodeSet(subject, reqVO.getIncludeChildren());
 
        // 2. 期初余额
        List<FinLedgerRespVO> rows = new ArrayList<>();
        BigDecimal balance = defaultIfNull(ledgerMapper.selectBalanceBeforeDate(subjectCodes, startDate,
                FinVoucherStatusEnum.POSTED.getStatus()));
        rows.add(buildOpeningRow(subject, startDate, balance));
 
        // 3. 逐条明细,并对每条分录结转余额;每逢月末补一行「本月合计」
        List<FinLedgerEntryDO> entries = ledgerMapper.selectEntryList(subjectCodes, startDate, endDate,
                FinVoucherStatusEnum.POSTED.getStatus());
        BigDecimal totalDebit = BigDecimal.ZERO;
        BigDecimal totalCredit = BigDecimal.ZERO;
        BigDecimal monthDebit = BigDecimal.ZERO;
        BigDecimal monthCredit = BigDecimal.ZERO;
        YearMonth currentMonth = null;
        for (FinLedgerEntryDO entry : entries) {
            YearMonth month = YearMonth.from(entry.getVoucherDate());
            // 3.1 跨月:先补上个月的本月合计
            if (currentMonth != null && !currentMonth.equals(month)) {
                rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(),
                        currentMonth.atEndOfMonth(), subject, null, FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(),
                        monthDebit, monthCredit, balance));
                monthDebit = BigDecimal.ZERO;
                monthCredit = BigDecimal.ZERO;
            }
            currentMonth = month;
            // 3.2 明细行
            BigDecimal debit = defaultIfNull(entry.getDebit());
            BigDecimal credit = defaultIfNull(entry.getCredit());
            balance = balance.add(debit).subtract(credit);
            monthDebit = monthDebit.add(debit);
            monthCredit = monthCredit.add(credit);
            totalDebit = totalDebit.add(debit);
            totalCredit = totalCredit.add(credit);
            rows.add(buildRow(FinLedgerRowTypeEnum.ENTRY, month.toString(), entry.getVoucherDate(), subject,
                    entry.getVoucherNo(), entry.getSummary(), debit, credit, balance)
                    .setSubjectCode(entry.getSubjectCode()).setSubjectName(entry.getSubjectName()));
        }
 
        // 4. 最后一个月的本月合计
        if (currentMonth != null) {
            rows.add(buildRow(FinLedgerRowTypeEnum.MONTHLY_TOTAL, currentMonth.toString(),
                    minDate(currentMonth.atEndOfMonth(), endDate), subject, null,
                    FinLedgerRowTypeEnum.MONTHLY_TOTAL.getName(), monthDebit, monthCredit, balance));
        }
        // 5. 本期累计
        rows.add(buildRow(FinLedgerRowTypeEnum.YEARLY_TOTAL, null, endDate, subject, null,
                FinLedgerRowTypeEnum.YEARLY_TOTAL.getName(), totalDebit, totalCredit, balance));
        return rows;
    }
 
    private FinAccountSubjectDO validateSubject(FinLedgerReqVO reqVO) {
        FinAccountSubjectDO subject = accountSubjectService.getAccountSubjectByCode(reqVO.getSubjectCode());
        if (subject == null) {
            throw exception(LEDGER_SUBJECT_NOT_EXISTS, reqVO.getSubjectCode());
        }
        return subject;
    }
 
    private void validateMonthRange(LocalDate startDate, LocalDate endDate) {
        if (startDate.isAfter(endDate)) {
            throw exception(LEDGER_MONTH_RANGE_INVALID, YearMonth.from(startDate), YearMonth.from(endDate));
        }
    }
 
    /**
     * 收集查询用的科目编码集合:默认包含所有下级科目
     */
    private Set<String> buildSubjectCodeSet(FinAccountSubjectDO subject, Boolean includeChildren) {
        Set<String> subjectCodes = new HashSet<>();
        subjectCodes.add(subject.getSubjectCode());
        if (!Boolean.TRUE.equals(includeChildren)) {
            return subjectCodes;
        }
        // 用 parentId 递归收集下级,visited 由 subjectCodes 的 add 结果兼作防护,避免脏数据成环时死循环
        Map<Long, List<FinAccountSubjectDO>> childrenMap = accountSubjectService.getAccountSubjectList().stream()
                .filter(item -> item.getParentId() != null)
                .collect(Collectors.groupingBy(FinAccountSubjectDO::getParentId));
        Deque<Long> queue = new ArrayDeque<>();
        queue.add(subject.getId());
        while (!queue.isEmpty()) {
            for (FinAccountSubjectDO child : childrenMap.getOrDefault(queue.poll(), List.of())) {
                if (subjectCodes.add(child.getSubjectCode())) {
                    queue.add(child.getId());
                }
            }
        }
        return subjectCodes;
    }
 
    private FinLedgerRespVO buildOpeningRow(FinAccountSubjectDO subject, LocalDate date, BigDecimal balance) {
        return buildRow(FinLedgerRowTypeEnum.OPENING, YearMonth.from(date).toString(), date, subject, null,
                FinLedgerRowTypeEnum.OPENING.getName(), null, null, balance);
    }
 
    private FinLedgerRespVO buildRow(FinLedgerRowTypeEnum rowType, String period, LocalDate date,
                                     FinAccountSubjectDO subject, String voucherNo, String summary,
                                     BigDecimal debit, BigDecimal credit, BigDecimal balance) {
        return new FinLedgerRespVO()
                .setRowType(rowType.getType())
                .setRowTypeName(rowType.getName())
                .setPeriod(period)
                .setDate(date)
                .setVoucherNo(voucherNo)
                .setSubjectCode(subject.getSubjectCode())
                .setSubjectName(subject.getSubjectName())
                .setSummary(summary)
                .setDebit(debit)
                .setCredit(credit)
                .setDirection(FinBalanceDirectionEnum.getByBalance(balance))
                .setBalance(balance);
    }
 
    private static BigDecimal sum(List<FinLedgerEntryDO> entries, boolean debit) {
        return entries.stream()
                .map(entry -> debit ? defaultIfNull(entry.getDebit()) : defaultIfNull(entry.getCredit()))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
 
    private static BigDecimal defaultIfNull(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO : amount;
    }
 
    private static LocalDate minDate(LocalDate date, LocalDate maxDate) {
        return date.isAfter(maxDate) ? maxDate : date;
    }
 
}