Sfoglia il codice sorgente

feat: 新增台账导入模板下载功能

- 新增 LedgerTemplateController,提供 GET /ledger/template 端点
- 无参数时返回合并模板(全部20个Sheet,含原始Sheet名和列名)
- ?type=xxx 返回对应单台账模板
- 每个Sheet第一行为大标题(合并单元格),第二行为列名;小额奖励审批单只有一行列名
simonlll 1 mese fa
parent
commit
bcbe8b1e24

+ 317 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/ledger/LedgerTemplateController.java

@@ -0,0 +1,317 @@
1
+package com.sundot.airport.web.controller.ledger;
2
+
3
+import com.sundot.airport.common.annotation.Log;
4
+import com.sundot.airport.common.core.controller.BaseController;
5
+import com.sundot.airport.common.enums.BusinessType;
6
+import org.apache.poi.ss.usermodel.*;
7
+import org.apache.poi.ss.util.CellRangeAddress;
8
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.RequestMapping;
12
+import org.springframework.web.bind.annotation.RequestParam;
13
+import org.springframework.web.bind.annotation.RestController;
14
+
15
+import javax.servlet.http.HttpServletResponse;
16
+import java.net.URLEncoder;
17
+import java.util.*;
18
+
19
+/**
20
+ * 台账导入模板下载
21
+ * GET /ledger/template             → 下载合并模板(全部20个Sheet)
22
+ * GET /ledger/template?type={key} → 下载单个台账模板
23
+ */
24
+@RestController
25
+@RequestMapping("/ledger/template")
26
+public class LedgerTemplateController extends BaseController {
27
+
28
+    // ── 模板定义 ────────────────────────────────────────────────────────
29
+
30
+    private static final Map<String, String> KEY_TO_SHEET;   // 业务key → Sheet名
31
+    private static final Map<String, Integer> KEY_TO_HEADS;  // 业务key → 表头行数
32
+    private static final Map<String, String[]> KEY_TO_COLS;  // 业务key → 列名数组
33
+
34
+    static {
35
+        Map<String, String> nm = new LinkedHashMap<>();
36
+        Map<String, Integer> hm = new LinkedHashMap<>();
37
+        Map<String, String[]> cm = new LinkedHashMap<>();
38
+
39
+        // 1. 部门监察问题记录表 (25列)
40
+        nm.put("supervisionProblem", "部门监察问题记录表");
41
+        hm.put("supervisionProblem", 2);
42
+        cm.put("supervisionProblem", new String[]{
43
+            "时间","区域","工作点","岗位","责任人","问题描述","问题类型","整改措施","依据","问题层级",
44
+            "班组","队室质控员","质控推送队室负责人","附件","整改情况","佐证材料","本月内发生问题次数",
45
+            "上月质控问题超过三次人员","检查人员","分管质控经理","录入时间","部门培训教员","队室内勤",
46
+            "发送至相关人员","队室负责人"
47
+        });
48
+
49
+        // 2. 队室三级质控巡查记录表 (15列)
50
+        nm.put("patrolInspection", "队室三级质控巡查记录表");
51
+        hm.put("patrolInspection", 2);
52
+        cm.put("patrolInspection", new String[]{
53
+            "巡查日期","区域","工作点","时间段","巡查岗位","被检查人员","有无问题","检查情况描述",
54
+            "类型","整改措施","附件","整改情况","佐证材料","班组","责任组长"
55
+        });
56
+
57
+        // 3. 部门实时质控拦截情况记录表 (29列)
58
+        nm.put("realtimeInterception", "部门实时质控拦截情况记录表");
59
+        hm.put("realtimeInterception", 2);
60
+        cm.put("realtimeInterception", new String[]{
61
+            "时间","区域","工作点","岗位","责任人","实时质控拦截物品","个数","问题类型","整改措施","问题层级",
62
+            "班组","附件","问题类别","个人复盘","队室复盘","队室质控员","质控推送队室负责人","开机年限",
63
+            "责任人开机年龄","发送至相关人员","本月内发生问题次数","上月发生次数","实时图像漏检难易度",
64
+            "检查人员","部门培训教员","分管质控经理","队室内勤","备注","队室负责人"
65
+        });
66
+
67
+        // 4. 服务巡查 (22列)
68
+        nm.put("servicePatrol", "服务巡查");
69
+        hm.put("servicePatrol", 2);
70
+        cm.put("servicePatrol", new String[]{
71
+            "时间","区域","工作点","岗位","责任人","问题描述","问题类型","整改措施","班组","问题层级",
72
+            "队室服务联络人","服务推送队室负责人","附件","整改情况","佐证材料","本月内发生问题次数",
73
+            "上月服务问题超三次人员","检查人员","分管服务经理","发送至相关人员","队室内勤","队室负责人"
74
+        });
75
+
76
+        // 5. 投诉情况 (13列)
77
+        nm.put("complaint", "投诉情况");
78
+        hm.put("complaint", 2);
79
+        cm.put("complaint", new String[]{
80
+            "时间","航班号","旅客姓名","班组","责任人","投诉情况","旅客诉求","类别","渠道来源",
81
+            "是否有责","处理进度","责任队长","队室内勤"
82
+        });
83
+
84
+        // 6. 安保测试记录表(部门)(19列)
85
+        nm.put("securityTest", "安保测试记录表(部门)");
86
+        hm.put("securityTest", 2);
87
+        cm.put("securityTest", new String[]{
88
+            "开展时间","测试区域","测试通道","测试项目","被测试人员","被测试岗位","测试物品","图片或视频",
89
+            "是否通过","层级","整改措施","扣分","整改材料","班组","推送队室质控员","推送质控队长",
90
+            "队室内勤","推送按钮","队室负责人"
91
+        });
92
+
93
+        // 7. 通道过检率 (横向月份列, 2行表头)
94
+        nm.put("channelPassRate", "通道过检率");
95
+        hm.put("channelPassRate", 2);
96
+        cm.put("channelPassRate", new String[]{
97
+            "组长","班组","内勤",
98
+            "2026年1月平均过检率","2026年2月平均过检率","2026年3月平均过检率",
99
+            "2026年4月平均过检率","2026年5月平均过检率","2026年6月平均过检率",
100
+            "2026年7月平均过检率","2026年8月平均过检率","2026年9月平均过检率",
101
+            "2026年10月平均过检率","2026年11月平均过检率","2026年12月平均过检率"
102
+        });
103
+
104
+        // 8. 不安全事件 (11列)
105
+        nm.put("unsafeEvent", "不安全事件");
106
+        hm.put("unsafeEvent", 2);
107
+        cm.put("unsafeEvent", new String[]{
108
+            "时间","事件描述","类别","航班号","责任人","涉及班组","涉及物品","岗位","区域","通道号","图像"
109
+        });
110
+
111
+        // 9. 2026查获违规品统计 (20列)
112
+        nm.put("seizureStats", "2026查获违规品统计");
113
+        hm.put("seizureStats", 2);
114
+        cm.put("seizureStats", new String[]{
115
+            "查获时间","地点","通道号","部门/队室","区域","安检员","安检员岗位","值班组长",
116
+            "航班号","航班目的地","机票号","购票时间","旅客姓名","旅客性别","证件号码","旅客来源地",
117
+            "违规类别","物品种类","藏匿部位","数量"
118
+        });
119
+
120
+        // 10. 航站楼加分 (7列)
121
+        nm.put("terminalBonus", "航站楼加分");
122
+        hm.put("terminalBonus", 2);
123
+        cm.put("terminalBonus", new String[]{
124
+            "审核日期","姓名","班组","加分分数","队室内勤","总加分数","队室负责人"
125
+        });
126
+
127
+        // 11. 成绩收集 (9列)
128
+        nm.put("examScore", "成绩收集");
129
+        hm.put("examScore", 2);
130
+        cm.put("examScore", new String[]{
131
+            "类别","期数","考试人员","理论成绩","图像成绩","班组","分类","备注(补考分数)","队室教员"
132
+        });
133
+
134
+        // 12. 小额奖励审批单(1行表头)
135
+        nm.put("rewardApproval", "小额奖励审批单-安检-质控");
136
+        hm.put("rewardApproval", 1);
137
+        cm.put("rewardApproval", new String[]{
138
+            "员工编码","姓名","查获(事件)时间","奖励类别","查获物品","类别(一类/二类)",
139
+            "奖励事由(安全)","人员类别","主要事由简述"
140
+        });
141
+
142
+        // 13. 部门奖惩记录表 (5列)
143
+        nm.put("rewardPenalty", "部门奖惩记录表");
144
+        hm.put("rewardPenalty", 2);
145
+        cm.put("rewardPenalty", new String[]{
146
+            "姓名","班组","事由","扣罚金额","最后更新时间"
147
+        });
148
+
149
+        // 14. 请、休假记录表(特殊)(6列)
150
+        nm.put("leaveSpecial", "请、休假记录表(特殊)");
151
+        hm.put("leaveSpecial", 2);
152
+        cm.put("leaveSpecial", new String[]{
153
+            "姓名","班组","时间(起)","时间(止)","休假类别","天数/时长"
154
+        });
155
+
156
+        // 15. 锦旗及感谢信 (7列)
157
+        nm.put("bannerLetter", "锦旗及感谢信");
158
+        hm.put("bannerLetter", 2);
159
+        cm.put("bannerLetter", new String[]{
160
+            "时间","姓名","内容","类别","图片附件","班组","队室内勤"
161
+        });
162
+
163
+        // 16. 日常培训记录 (12列)
164
+        nm.put("dailyTraining", "日常培训记录");
165
+        hm.put("dailyTraining", 2);
166
+        cm.put("dailyTraining", new String[]{
167
+            "月度培训记录日期","班组","队室负责教员","队室培训负责队长","课时","项目名称",
168
+            "培训内容","培训教员","培训地点","参训人数","是否完成","备注"
169
+        });
170
+
171
+        // 17. 组长履职情况记录表 (6列)
172
+        nm.put("leaderDuty", "组长履职情况记录表");
173
+        hm.put("leaderDuty", 2);
174
+        cm.put("leaderDuty", new String[]{
175
+            "本班点评","问题处置及整改措施","工作提示","提交人","队室负责人","队室质控员"
176
+        });
177
+
178
+        // 18. 健康锐兵(2026_3)(21列)
179
+        nm.put("healthSoldier", "健康锐兵(2026_3)");
180
+        hm.put("healthSoldier", 2);
181
+        cm.put("healthSoldier", new String[]{
182
+            "慢性病情况","提交人","近期身体不适情况","是否进行过体检","体检报告是否有异常",
183
+            "近期是否有重大疾病","作息及生活习惯","心理状态","工作压力情况","佐证材料",
184
+            "重大疾病(其他)","体检异常(其他)","近期不适(其他)","慢性病(轻度)","慢性病(重度)",
185
+            "既往病史","既往病史(其他)","提交时间","班组","队室负责人","亚健康人员状态"
186
+        });
187
+
188
+        // 19. 宿舍消防安全专项自查表(部门)(11列)
189
+        nm.put("dormFireSafety", "宿舍消防安全专项自查表(部门)");
190
+        hm.put("dormFireSafety", 2);
191
+        cm.put("dormFireSafety", new String[]{
192
+            "检查日期","寝室所在位置","寝室号","自查覆盖项","风险类型排查情况","发现具体隐患",
193
+            "照片","整改责任人","整改完成时间","提交时间","提交人"
194
+        });
195
+
196
+        // 20. 培训台账问题通报 (7列)
197
+        nm.put("trainingIssue", "培训台账问题通报");
198
+        hm.put("trainingIssue", 2);
199
+        cm.put("trainingIssue", new String[]{
200
+            "问题台账日期","问题台账内容","具体问题","处理人","班组","二次复核问题","是否完成整改"
201
+        });
202
+
203
+        KEY_TO_SHEET = Collections.unmodifiableMap(nm);
204
+        KEY_TO_HEADS = Collections.unmodifiableMap(hm);
205
+        KEY_TO_COLS  = Collections.unmodifiableMap(cm);
206
+    }
207
+
208
+    // ── 端点 ─────────────────────────────────────────────────────────────
209
+
210
+    @PreAuthorize("@ss.hasPermi('ledger:import:combined')")
211
+    @Log(title = "台账模板下载", businessType = BusinessType.EXPORT)
212
+    @GetMapping
213
+    public void downloadTemplate(
214
+            @RequestParam(required = false) String type,
215
+            HttpServletResponse response) throws Exception {
216
+
217
+        boolean combined = (type == null || type.isEmpty());
218
+
219
+        if (!combined && !KEY_TO_SHEET.containsKey(type)) {
220
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "不支持的模板类型: " + type);
221
+            return;
222
+        }
223
+
224
+        String rawName = combined
225
+                ? "旅检三部\"三三\"数字管理平台_模板.xlsx"
226
+                : KEY_TO_SHEET.get(type) + "_导入模板.xlsx";
227
+
228
+        try (XSSFWorkbook wb = new XSSFWorkbook()) {
229
+            CellStyle titleStyle  = buildTitleStyle(wb);
230
+            CellStyle headerStyle = buildHeaderStyle(wb);
231
+
232
+            if (combined) {
233
+                for (String key : KEY_TO_SHEET.keySet()) {
234
+                    addSheet(wb, key, titleStyle, headerStyle);
235
+                }
236
+            } else {
237
+                addSheet(wb, type, titleStyle, headerStyle);
238
+            }
239
+
240
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
241
+            response.setHeader("Content-Disposition",
242
+                    "attachment; filename=" + URLEncoder.encode(rawName, "UTF-8").replace("+", "%20"));
243
+            wb.write(response.getOutputStream());
244
+        }
245
+    }
246
+
247
+    // ── 私有辅助 ──────────────────────────────────────────────────────────
248
+
249
+    private void addSheet(XSSFWorkbook wb, String key, CellStyle titleStyle, CellStyle headerStyle) {
250
+        String   sheetName = KEY_TO_SHEET.get(key);
251
+        String[] cols      = KEY_TO_COLS.get(key);
252
+        int      headRows  = KEY_TO_HEADS.getOrDefault(key, 2);
253
+
254
+        Sheet sheet = wb.createSheet(sheetName);
255
+
256
+        int colCount = cols.length;
257
+        int headerRowIndex;
258
+
259
+        if (headRows == 2) {
260
+            // 第1行:大标题(合并单元格)
261
+            Row titleRow = sheet.createRow(0);
262
+            titleRow.setHeightInPoints(22);
263
+            Cell titleCell = titleRow.createCell(0);
264
+            titleCell.setCellValue(sheetName);
265
+            titleCell.setCellStyle(titleStyle);
266
+            if (colCount > 1) {
267
+                sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, colCount - 1));
268
+            }
269
+            headerRowIndex = 1;
270
+        } else {
271
+            headerRowIndex = 0;
272
+        }
273
+
274
+        // 列头行
275
+        Row headerRow = sheet.createRow(headerRowIndex);
276
+        headerRow.setHeightInPoints(20);
277
+        for (int i = 0; i < colCount; i++) {
278
+            Cell cell = headerRow.createCell(i);
279
+            cell.setCellValue(cols[i]);
280
+            cell.setCellStyle(headerStyle);
281
+            // 列宽:按列名长度自适应(最小16,最大50个字符宽度)
282
+            int charLen = cols[i].length();
283
+            int width = Math.min(Math.max(charLen + 4, 16), 50) * 256;
284
+            sheet.setColumnWidth(i, width);
285
+        }
286
+    }
287
+
288
+    private CellStyle buildTitleStyle(XSSFWorkbook wb) {
289
+        CellStyle s = wb.createCellStyle();
290
+        s.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
291
+        s.setFillPattern(FillPatternType.SOLID_FOREGROUND);
292
+        s.setAlignment(HorizontalAlignment.CENTER);
293
+        s.setVerticalAlignment(VerticalAlignment.CENTER);
294
+        Font f = wb.createFont();
295
+        f.setBold(true);
296
+        f.setFontHeightInPoints((short) 12);
297
+        s.setFont(f);
298
+        return s;
299
+    }
300
+
301
+    private CellStyle buildHeaderStyle(XSSFWorkbook wb) {
302
+        CellStyle s = wb.createCellStyle();
303
+        s.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
304
+        s.setFillPattern(FillPatternType.SOLID_FOREGROUND);
305
+        s.setAlignment(HorizontalAlignment.CENTER);
306
+        s.setVerticalAlignment(VerticalAlignment.CENTER);
307
+        s.setBorderBottom(BorderStyle.THIN);
308
+        s.setBorderTop(BorderStyle.THIN);
309
+        s.setBorderLeft(BorderStyle.THIN);
310
+        s.setBorderRight(BorderStyle.THIN);
311
+        Font f = wb.createFont();
312
+        f.setBold(true);
313
+        f.setFontHeightInPoints((short) 10);
314
+        s.setFont(f);
315
+        return s;
316
+    }
317
+}