chengf
2026-02-25 64b14b0549344f13b191e2c26ed199f62655c43f
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
package com.java110.fee.cmd.fee;
 
import com.alibaba.fastjson.JSONObject;
import com.java110.core.annotation.Java110Cmd;
import com.java110.core.context.ICmdDataFlowContext;
import com.java110.core.event.cmd.Cmd;
import com.java110.core.event.cmd.CmdEvent;
import com.java110.dto.community.CommunityDto;
import com.java110.dto.fee.FeeConfigDto;
import com.java110.dto.room.RoomDto;
import com.java110.intf.community.ICommunityInnerServiceSMO;
import com.java110.intf.community.IRoomInnerServiceSMO;
import com.java110.intf.fee.IFeeConfigInnerServiceSMO;
import com.java110.intf.fee.IReportFeeInnerServiceSMO;
import com.java110.utils.exception.CmdException;
import com.java110.utils.util.Assert;
import com.java110.utils.util.BeanConvertUtil;
import com.java110.utils.util.DateUtil;
import com.java110.vo.ResultVo;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
 
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
 
@Java110Cmd(serviceCode = "fee.exportPropertyFeeExcelCmd")
public class ExportPropertyFeeExcelCmd extends Cmd {
 
    @Autowired
    private IReportFeeInnerServiceSMO reportFeeInnerServiceSMOImpl;
 
    @Autowired
    private ICommunityInnerServiceSMO communityInnerServiceSMOImpl;
 
    @Autowired
    private IRoomInnerServiceSMO roomInnerServiceSMOImpl;
 
    @Autowired
    private IFeeConfigInnerServiceSMO feeConfigInnerServiceSMOImpl;
 
    @Override
    public void validate(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException {
        Assert.hasKey(reqJson, "communityId", "请求报文中未包含小区编号");
    }
 
    @Override
    public void doCmd(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException {
        // 设置年份范围:2020年至当前年份
        int startYear = 2020;
        int currentYear = DateUtil.getYear();
        int endYear = currentYear;
 
        String communityId = reqJson.getString("communityId");
        
        // 获取小区信息
        CommunityDto communityDto = new CommunityDto();
        communityDto.setCommunityId(communityId);
        List<CommunityDto> communityDtos = communityInnerServiceSMOImpl.queryCommunitys(communityDto);
        Assert.notEmpty(communityDtos, "未查询到小区信息");
 
        // 查询房屋数据(不分页,获取全部)
        RoomDto roomDto = new RoomDto();
        roomDto.setCommunityId(communityId);
        roomDto.setFloorNum(reqJson.containsKey("floorId") && !reqJson.getString("floorId").equals("") ? reqJson.getString("floorId") : null);
        roomDto.setOwnerName(reqJson.containsKey("ownerName") && !reqJson.getString("ownerName").equals("") ? reqJson.getString("ownerName") : null);
        // 取消分页限制
        roomDto.setRow(Integer.MAX_VALUE);
        roomDto.setPage(1);
        List<Map> rooms = roomInnerServiceSMOImpl.queryRoomsAsReport(roomDto);
        
        if (rooms.isEmpty()) {
            ResultVo resultVo = new ResultVo("未查询到数据");
            ResponseEntity<String> responseEntity = new ResponseEntity<>(resultVo.toString(), HttpStatus.OK);
            context.setResponseEntity(responseEntity);
            return;
        }
 
        // 获取费用配置
        FeeConfigDto feeConfigDto = new FeeConfigDto();
        feeConfigDto.setCommunityId(communityId);
        if (reqJson.containsKey("feeTypeCd") && !reqJson.getString("feeTypeCd").equals("")) {
            feeConfigDto.setFeeTypeCd(reqJson.getString("feeTypeCd"));
        } else {
            feeConfigDto.setFeeTypeCds(new String[]{"630000001"});
        }
        if (reqJson.containsKey("importFee") && !reqJson.getString("importFee").equals("")) {
            feeConfigDto.setFeeNameEq(reqJson.getString("importFee"));
        } else {
            feeConfigDto.setFeeTypeCds(new String[]{"630000001"});
        }
        List<FeeConfigDto> originalDtos = feeConfigInnerServiceSMOImpl.queryFeeConfigs(feeConfigDto);
 
        // 处理费用配置:去重、排序
        List<FeeConfigDto> feeConfigDtos = originalDtos.stream()
                .sorted(Comparator.comparing(FeeConfigDto::getSquarePrice, Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(FeeConfigDto::getStartTime, Comparator.nullsLast(Comparator.reverseOrder())))
                .collect(Collectors.groupingBy(dto -> (dto.getSquarePrice() == null) ? new Object() : dto.getSquarePrice(), Collectors.toList()))
                .values().stream()
                .map(group -> group.get(0))
                .collect(Collectors.toList());
 
        // 创建Excel
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 内存中保留100行,其余写入临时文件
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
 
            // 创建工作表
            Sheet sheet = workbook.createSheet("物业费报表_" + communityDtos.get(0).getName());
            
            // 创建表头样式
            CellStyle headerStyle = createHeaderStyle(workbook);
            // 创建内容样式
            CellStyle contentStyle = createContentStyle(workbook);
 
            // 生成表头
            int headerRowIndex = createExcelHeader(sheet, headerStyle, feeConfigDtos, startYear, endYear);
 
            // 填充数据
            fillExcelData(sheet, contentStyle, rooms, feeConfigDtos, startYear, endYear, headerRowIndex);
 
            // 调整列宽
            autoSizeColumns(sheet, 0, 15 + 2 * feeConfigDtos.size() + 3 * (endYear - startYear + 1));
 
            // 写入输出流
            workbook.write(out);
            workbook.dispose(); // 清理临时文件
 
            // 构建响应
            byte[] bytes = out.toByteArray();
            HttpHeaders headers = createExcelHeaders("物业费报表_" + DateUtil.getNow("yyyy-MM-dd") + ".xlsx");
            ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, HttpStatus.OK);
            context.setResponseEntity(responseEntity);
 
        } catch (IOException e) {
            throw new CmdException("导出Excel失败:" + e.getMessage());
        }
    }
 
    /**
     * 创建表头样式
     */
    private CellStyle createHeaderStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        // 设置边框
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        // 设置居中
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        // 设置背景色
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 设置字体
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        return style;
    }
 
    /**
     * 创建内容样式
     */
    private CellStyle createContentStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        // 设置边框
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        // 设置居中
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }
 
    /**
     * 创建Excel表头
     */
    private int createExcelHeader(Sheet sheet, CellStyle headerStyle, List<FeeConfigDto> feeConfigDtos, int startYear, int endYear) {
        int rowIndex = 0;
        int feeCount = feeConfigDtos.size();
        int yearCount = endYear - startYear + 1;
 
        // 第一行表头
        Row row1 = sheet.createRow(rowIndex++);
        int colIndex = 0;
        
        // 基础信息列
        String[] baseHeaders = {"序号", "物业类型", "楼栋号/弄", "门号", "室号", "门室号", "产证地址", "收费面积(m²)", "购房人姓名"};
        for (String header : baseHeaders) {
            createCell(row1, colIndex++, header, headerStyle);
        }
        
        // 费用配置列
        for (int i = 0; i < feeCount; i++) {
            createCell(row1, colIndex++, feeConfigDtos.get(i).getFeeName(), headerStyle);
        }
        
        // 其他固定列
        createCell(row1, colIndex++, "设备运作费", headerStyle);
        for (int i = 0; i < feeCount; i++) {
            createCell(row1, colIndex++, feeConfigDtos.get(i).getFeeName(), headerStyle);
        }
        createCell(row1, colIndex++, "年应收款", headerStyle);
        createCell(row1, colIndex++, "合计(" + startYear + "年1月-至今)", headerStyle);
        createCell(row1, colIndex++, "历年实收", headerStyle);
        createCell(row1, colIndex++, "操作", headerStyle);
 
        // 第二行表头
        Row row2 = sheet.createRow(rowIndex++);
        colIndex = 0;
        
        // 跳过基础信息列
        colIndex += baseHeaders.length + feeCount + 1 + feeCount + 1;
        
        // 统计信息列
        String[] statsHeaders = {"应收", "实收", "代收", startYear + "年", (startYear + 1) + "年", "待收月数", "待收金额", "代收区间"};
        for (String header : statsHeaders) {
            createCell(row2, colIndex++, header, headerStyle);
        }
        
        // 年份列
        for (int i = startYear; i <= endYear; i++) {
            createCell(row2, colIndex++, String.valueOf(i), headerStyle);
        }
        
        createCell(row2, colIndex++, "打折金额汇总", headerStyle);
 
        // 第三行表头
        Row row3 = sheet.createRow(rowIndex++);
        colIndex = 0;
        
        // 跳过前面的列
        colIndex += baseHeaders.length + feeCount + 1 + feeCount + 1 + statsHeaders.length;
        
        // 年份详情列
        for (int i = startYear; i <= endYear; i++) {
            createCell(row3, colIndex++, "已收月数", headerStyle);
            createCell(row3, colIndex++, "已收区间", headerStyle);
            createCell(row3, colIndex++, "未收区间", headerStyle);
        }
 
        // 合并单元格
        // 这里可以根据需要添加单元格合并逻辑
 
        return rowIndex;
    }
 
    /**
     * 填充Excel数据
     */
    private void fillExcelData(Sheet sheet, CellStyle contentStyle, List<Map> rooms, 
                              List<FeeConfigDto> feeConfigDtos, int startYear, int endYear, int startRowIndex) {
        int rowIndex = startRowIndex;
        int feeCount = feeConfigDtos.size();
        int yearCount = endYear - startYear + 1;
 
        for (int i = 0; i < rooms.size(); i++) {
            Map room = rooms.get(i);
            Row row = sheet.createRow(rowIndex++);
            int colIndex = 0;
            
            // 基础信息
            createCell(row, colIndex++, String.valueOf(i + 1), contentStyle); // 序号
            createCell(row, colIndex++, room.get("property_type") != null ? room.get("property_type").toString() : "", contentStyle);
            createCell(row, colIndex++, room.get("floor_num") != null ? room.get("floor_num").toString() : "", contentStyle);
            createCell(row, colIndex++, room.get("unit_num") != null ? room.get("unit_num").toString() : "", contentStyle);
            createCell(row, colIndex++, room.get("room_num") != null ? room.get("room_num").toString() : "", contentStyle);
            createCell(row, colIndex++, (room.get("unit_num") != null ? room.get("unit_num").toString() : "") + "-" + 
                    (room.get("room_num") != null ? room.get("room_num").toString() : ""), contentStyle);
            createCell(row, colIndex++, room.get("property_address") != null ? room.get("property_address").toString() : "", contentStyle);
            createCell(row, colIndex++, room.get("room_area") != null ? room.get("room_area").toString() : "", contentStyle);
            createCell(row, colIndex++, room.get("name") != null ? room.get("name").toString() : "", contentStyle);
            
            // 费用配置信息
            for (FeeConfigDto config : feeConfigDtos) {
                createCell(row, colIndex++, config.getSquarePrice() != null ? config.getSquarePrice() : "", contentStyle);
            }
            
            // 设备运作费
            createCell(row, colIndex++, "", contentStyle);
            
            // 费用计算
            BigDecimal roomArea = room.get("room_area") != null ? (BigDecimal) room.get("room_area") : BigDecimal.ZERO;
            for (FeeConfigDto config : feeConfigDtos) {
                double price = config.getSquarePrice() != null ? Double.parseDouble(config.getSquarePrice()) : 0;
                double total = price * roomArea.doubleValue();
                createCell(row, colIndex++, String.valueOf(doublequ2(total)), contentStyle);
            }
            
            // 年应收款
            double year应收 = feeConfigDtos.stream()
                    .mapToDouble(config -> {
                        double price = config.getSquarePrice() != null ? Double.parseDouble(config.getSquarePrice()) : 0;
                        return price * roomArea.doubleValue() * 12;
                    })
                    .sum();
            createCell(row, colIndex++, String.valueOf(doublequ2(year应收)), contentStyle);
            
            // 获取费用数据
            HashMap<Object, Object> params = new HashMap<>();
            params.put("payObjId", room.get("room_id"));
            List<Map> feeData = reportFeeInnerServiceSMOImpl.onceRoomFee(params);
            
            // 计算合计信息
            double allMoney = 0;
            double allPayMoney = 0;
            int monthCount = 0;
            int noPayMoney = 0;
            double discountMoney = 0;
            
            for (Map fee : feeData) {
                if (fee.containsKey("统计维度") && !fee.get("统计维度").toString().contains("总计")) {
                    allMoney += ((BigDecimal) fee.get("应收金额")).doubleValue();
                    allPayMoney += ((BigDecimal) fee.get("实收金额")).doubleValue();
                    monthCount += ((BigDecimal) fee.get("未收月数")).intValue();
                    noPayMoney += ((BigDecimal) fee.get("未收金额")).intValue();
                    discountMoney += ((BigDecimal) fee.get("折扣金额")).doubleValue();
                }
            }
            
            // 填充合计信息
            createCell(row, colIndex++, String.valueOf(doublequ2(allMoney)), contentStyle); // 合计
            createCell(row, colIndex++, String.valueOf(doublequ2(allPayMoney)), contentStyle); // 历年实收
            createCell(row, colIndex++, "", contentStyle); // 操作
            
            // 填充统计信息
            createCell(row, colIndex++, String.valueOf(doublequ2(allMoney)), contentStyle); // 应收
            createCell(row, colIndex++, String.valueOf(doublequ2(allPayMoney)), contentStyle); // 实收
            createCell(row, colIndex++, "", contentStyle); // 代收
            createCell(row, colIndex++, "", contentStyle); // 2020年
            createCell(row, colIndex++, "", contentStyle); // 2021年
            createCell(row, colIndex++, String.valueOf(monthCount), contentStyle); // 待收月数
            createCell(row, colIndex++, String.valueOf(noPayMoney), contentStyle); // 待收金额
            createCell(row, colIndex++, "", contentStyle); // 代收区间
            
            // 填充年份数据
            for (int year = startYear; year <= endYear; year++) {
                // 年份总数据(占位)
                createCell(row, colIndex++, "", contentStyle);
                
                // 已收月数、已收区间、未收区间
                String receivedMonths = "0";
                String receivedRange = "";
                String unpaidRange = "";
                
                for (Map fee : feeData) {
                    if (fee.get("统计维度").equals(year + "年")) {
                        receivedMonths = ((BigDecimal) fee.get("已收月数")).intValue() + "";
                        receivedRange = fee.get("已收区间") != null ? fee.get("已收区间").toString() : "";
                        unpaidRange = fee.get("未收区间") != null ? fee.get("未收区间").toString() : "";
                        break;
                    }
                }
                
                createCell(row, colIndex++, receivedMonths, contentStyle);
                createCell(row, colIndex++, receivedRange, contentStyle);
                createCell(row, colIndex++, unpaidRange, contentStyle);
            }
            
            // 打折金额汇总
            createCell(row, colIndex++, String.valueOf(doublequ2(discountMoney)), contentStyle);
        }
    }
 
    /**
     * 创建单元格并设置值和样式
     */
    private void createCell(Row row, int column, String value, CellStyle style) {
        Cell cell = row.createCell(column);
        cell.setCellValue(value);
        cell.setCellStyle(style);
    }
 
    /**
     * 自动调整列宽
     */
    private void autoSizeColumns(Sheet sheet, int startCol, int endCol) {
        for (int i = startCol; i <= endCol; i++) {
            sheet.autoSizeColumn(i);
            // 适当调整自动列宽,避免过窄
            int width = sheet.getColumnWidth(i) + 256;
            sheet.setColumnWidth(i, width > 65535 ? 65535 : width);
        }
    }
 
    /**
     * 创建Excel响应头
     */
    private HttpHeaders createExcelHeaders(String fileName) {
        HttpHeaders headers = new HttpHeaders();
        try {
            // 设置文件名编码
            String encodedFileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
            headers.setContentDispositionFormData("attachment", encodedFileName);
        } catch (Exception e) {
            headers.setContentDispositionFormData("attachment", fileName);
        }
        headers.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM);
        return headers;
    }
 
    /**
     * 保留两位小数
     */
    public double doublequ2(double num) {
        return (int) (num * 100) / 100.0;
    }
}