shinnosuke
2026-08-05 25729fa1ae7b630cbd4a9547a5693acd03c09c2a
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
package com.talentcard.server.business.portal;
 
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
 
import javax.imageio.ImageIO;
 
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class TalentCardPortalService {
 
    private static final int DYNAMIC_CODE_SECONDS = 60;
 
    private final JdbcTemplate jdbcTemplate;
    private final Set<String> allowedExternalWebHosts;
 
    public TalentCardPortalService(
            JdbcTemplate jdbcTemplate,
            @Value("${app.external-web.allowed-hosts:}") String allowedExternalWebHosts
    ) {
        this.jdbcTemplate = jdbcTemplate;
        this.allowedExternalWebHosts = Arrays.stream(allowedExternalWebHosts.split(","))
                .map(String::trim)
                .filter(value -> !value.isEmpty())
                .map(value -> value.toLowerCase(Locale.ROOT))
                .collect(Collectors.toUnmodifiableSet());
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> profile(Long talentId) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("hasTalentCard", false);
        result.put("applicationOpen", true);
        result.put("applicationMessage", "请前往东区政务服务中心人才综合服务专窗办理人才卡申请。");
        result.put("applicationPhone", "0812-3509008");
        result.put("cardTypes", List.of(
                Map.of("level", "卓越人才卡", "theme", "EXCELLENCE"),
                Map.of("level", "杰出人才卡", "theme", "OUTSTANDING"),
                Map.of("level", "菁英人才卡", "theme", "ELITE")
        ));
        Map<String, Object> familyCardAccess = familyCardAccess(talentId);
        result.put("familyCardAccess", familyCardAccess);
        if (talentId == null) return result;
 
        List<Map<String, Object>> cards = query("""
                select c.id as card_id, c.card_no, c.card_level, c.valid_start, c.valid_end, c.status as card_status,
                       c.is_family_card_enabled, c.family_card_limit,
                       t.id as talent_id, t.name, t.gender, t.work_unit, t.status as talent_status
                from talent_card c
                join talent_user t on t.id = c.talent_id and t.deleted = 0
                where c.deleted = 0 and c.talent_id = ? and c.card_type = 'PERSONAL'
                order by c.id desc
                limit 1
                """, talentId);
        if (cards.isEmpty()) return result;
 
        Map<String, Object> row = cards.get(0);
        String status = cardStatus(row);
        String level = value(row.get("cardLevel"), "人才卡");
        Map<String, Object> card = new LinkedHashMap<>();
        card.put("id", row.get("cardId"));
        card.put("level", level);
        card.put("theme", cardTheme(level));
        card.put("name", maskName((String) row.get("name")));
        card.put("gender", genderText(row.get("gender")));
        card.put("cardNo", maskCardNo((String) row.get("cardNo")));
        card.put("workUnit", maskWorkUnit((String) row.get("workUnit")));
        card.put("status", status);
        card.put("statusText", cardStatusText(status));
        card.put("canShowCode", "ACTIVE".equals(status));
        card.put("validEnd", row.get("validEnd"));
        card.put("familyCardEnabled", Boolean.TRUE.equals(familyCardAccess.get("enabled")));
        card.put("familyCardLimit", ((Number) row.getOrDefault("familyCardLimit", 0)).intValue());
        result.put("hasTalentCard", true);
        result.put("card", card);
        advisor(level).ifPresent(advisor -> {
            String consultUrl = value(advisor.get("consultUrl"), "");
            advisor.put("consultUrl", isAllowedExternalWebUrl(consultUrl) ? consultUrl : "");
            result.put("advisor", advisor);
        });
        return result;
    }
 
    @Transactional(readOnly = true)
    public Map<String, Object> familyCardAccess(Long talentId) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("enabled", false);
        result.put("effectiveScope", "仅限卓越人才卡用户");
        result.put("cardLevel", "");
        result.put("familyCardLimit", 0);
        if (talentId == null) {
            result.put("unavailableReason", "暂未获取到人才卡信息");
            return result;
        }
        List<Map<String, Object>> cards = query("""
                select c.card_level, c.status as card_status, c.valid_end, c.is_family_card_enabled, c.family_card_limit,
                       t.status as talent_status
                from talent_card c
                join talent_user t on t.id = c.talent_id and t.deleted = 0
                where c.deleted = 0 and c.talent_id = ? and c.card_type = 'PERSONAL'
                order by c.id desc
                limit 1
                """, talentId);
        if (cards.isEmpty()) {
            result.put("unavailableReason", "暂未开通人才卡,暂不能使用家庭附属卡服务");
            return result;
        }
        Map<String, Object> card = cards.get(0);
        String level = value(card.get("cardLevel"), "人才卡");
        result.put("cardLevel", level);
        result.put("familyCardLimit", ((Number) card.getOrDefault("familyCardLimit", 0)).intValue());
        if (!isExcellenceCardLevel(level)) {
            result.put("unavailableReason", "家庭附属卡仅向卓越人才卡用户开放");
            return result;
        }
        if (!"ACTIVE".equals(cardStatus(card))) {
            result.put("unavailableReason", cardStatusText(cardStatus(card)) + ",暂不能使用家庭附属卡服务");
            return result;
        }
        if (((Number) card.getOrDefault("isFamilyCardEnabled", 0)).intValue() != 1) {
            result.put("unavailableReason", "当前人才卡暂未开通家庭附属卡服务");
            return result;
        }
        if (((Number) card.getOrDefault("familyCardLimit", 0)).intValue() < 1) {
            result.put("unavailableReason", "当前人才卡暂未配置家庭附属卡名额");
            return result;
        }
        result.put("enabled", true);
        result.put("unavailableReason", "");
        return result;
    }
 
    @Transactional
    public Map<String, Object> issueDynamicCode(Long talentId) {
        return issueDynamicCode(talentId, null);
    }
 
    @Transactional
    public Map<String, Object> issueDynamicCode(Long talentId, TalentDynamicCodeRequest request) {
        Map<String, Object> card = activeCard(talentId);
        String scene = scene(request == null ? null : request.scene());
        Long expectedMerchantId = request == null ? null : request.merchantId();
        if (expectedMerchantId != null && expectedMerchantId > 0) ensureMerchant(expectedMerchantId);
        if (expectedMerchantId != null && expectedMerchantId <= 0) expectedMerchantId = null;
        jdbcTemplate.update("update talent_dynamic_code set status = 2 where card_id = ? and status = 0", card.get("cardId"));
        Instant expiresAt = Instant.now().plusSeconds(DYNAMIC_CODE_SECONDS);
        String token = nextToken();
        jdbcTemplate.update("""
                insert into talent_dynamic_code(code_token, card_id, talent_id, expected_merchant_id, scene, expires_at, status)
                values (?, ?, ?, ?, ?, ?, 0)
                """, token, card.get("cardId"), talentId, expectedMerchantId, scene, Timestamp.from(expiresAt));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("imagePath", "/api/app/talent-cards/dynamic-codes/" + token + "/image");
        result.put("expiresAt", expiresAt.toEpochMilli());
        result.put("refreshAfterSeconds", DYNAMIC_CODE_SECONDS);
        result.put("scene", scene);
        return result;
    }
 
    @Transactional(readOnly = true)
    public byte[] dynamicCodeImage(String token) {
        List<Map<String, Object>> rows = query("""
                select id from talent_dynamic_code
                where code_token = ? and status = 0 and expires_at > now()
                """, token);
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "动态人才码已失效");
        }
        try {
            BitMatrix matrix = new QRCodeWriter().encode(token, BarcodeFormat.QR_CODE, 360, 360);
            BufferedImage image = MatrixToImageWriter.toBufferedImage(matrix);
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            ImageIO.write(image, "PNG", output);
            return output.toByteArray();
        } catch (Exception exception) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "动态人才码生成失败");
        }
    }
 
    @Transactional
    public Map<String, Object> verifyDynamicCode(String token, Long merchantId) {
        if (merchantId == null) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "核销商户不能为空");
        }
        List<Map<String, Object>> rows = query("""
                select d.id, d.card_id, d.talent_id, d.expected_merchant_id, d.scene, c.card_no, c.card_level, c.status as card_status, c.valid_end
                from talent_dynamic_code d
                join talent_card c on c.id = d.card_id and c.deleted = 0
                where d.code_token = ? and d.status = 0 and d.expires_at > now()
                """, token);
        if (rows.isEmpty() || !"ACTIVE".equals(cardStatus(rows.get(0)))) {
            throw new BusinessException(ErrorCode.CONFLICT, "动态人才码已失效或不可核销");
        }
        Map<String, Object> code = rows.get(0);
        Long expectedMerchantId = code.get("expectedMerchantId") instanceof Number number ? number.longValue() : null;
        if (expectedMerchantId != null && !expectedMerchantId.equals(merchantId)) {
            throw new BusinessException(ErrorCode.CONFLICT, "该人才码不可在当前商户核验");
        }
        int updated = jdbcTemplate.update("""
                update talent_dynamic_code
                set status = 1, merchant_id = ?, used_at = now()
                where id = ? and status = 0 and expires_at > now()
                """, merchantId, code.get("id"));
        if (updated == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, "动态人才码已被使用");
        }
        return Map.of(
                "verified", true,
                "cardNo", maskCardNo((String) code.get("cardNo")),
                "cardLevel", value(code.get("cardLevel"), "人才卡")
        );
    }
 
    private void ensureMerchant(Long merchantId) {
        Integer count = jdbcTemplate.queryForObject("select count(*) from merchant where id = ? and deleted = 0 and status = 1", Integer.class, merchantId);
        if (count == null || count == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "亮码商户不存在或不可用");
    }
 
    private String scene(String value) {
        String scene = value == null ? "general" : value.trim();
        return switch (scene) {
            case "merchant", "parking", "children", "general" -> scene;
            default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的亮码场景");
        };
    }
 
    private Map<String, Object> activeCard(Long talentId) {
        if (talentId == null) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "人才信息不能为空");
        }
        List<Map<String, Object>> cards = query("""
                select c.id as card_id, c.card_no, c.card_level, c.valid_end, c.status as card_status, t.status as talent_status
                from talent_card c
                join talent_user t on t.id = c.talent_id and t.deleted = 0
                where c.deleted = 0 and c.talent_id = ? and c.card_type = 'PERSONAL'
                order by c.id desc
                limit 1
                """, talentId);
        if (cards.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "未找到人才卡");
        }
        Map<String, Object> card = cards.get(0);
        if (!"ACTIVE".equals(cardStatus(card))) {
            throw new BusinessException(ErrorCode.CONFLICT, cardStatusText(cardStatus(card)) + ",无法生成动态人才码");
        }
        return card;
    }
 
    private java.util.Optional<Map<String, Object>> advisor(String cardLevel) {
        List<Map<String, Object>> advisors = query("""
                select advisor_name, department, mobile, consult_url
                from talent_card_advisor
                where card_level = ? and status = 1
                limit 1
                """, cardLevel);
        if (advisors.isEmpty()) return java.util.Optional.empty();
        Map<String, Object> row = advisors.get(0);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("name", row.get("advisorName"));
        result.put("department", row.get("department"));
        result.put("mobile", row.get("mobile"));
        result.put("consultUrl", row.get("consultUrl"));
        return java.util.Optional.of(result);
    }
 
    private boolean isExcellenceCardLevel(String level) {
        if (level == null) return false;
        String normalized = level.trim().toUpperCase(Locale.ROOT);
        return "EXCELLENCE".equals(normalized) || level.contains("卓越") || level.contains("鍗撹秺");
    }
 
    private String cardStatus(Map<String, Object> card) {
        if (((Number) card.getOrDefault("talentStatus", 1)).intValue() != 1) return "FROZEN";
        if (((Number) card.getOrDefault("cardStatus", 0)).intValue() == 0) return "FROZEN";
        if (((Number) card.getOrDefault("cardStatus", 0)).intValue() == 2) return "EXPIRED";
        Object validEnd = card.get("validEnd");
        if (validEnd instanceof LocalDate date && date.isBefore(LocalDate.now())) return "EXPIRED";
        return "ACTIVE";
    }
 
    private String cardStatusText(String status) {
        return switch (status) {
            case "FROZEN" -> "人才卡已冻结";
            case "EXPIRED" -> "人才卡已过期";
            default -> "人才卡有效";
        };
    }
 
    private String cardTheme(String level) {
        if (level.contains("杰出")) return "OUTSTANDING";
        if (level.contains("菁英") || level.contains("精英")) return "ELITE";
        return "EXCELLENCE";
    }
 
    private String maskName(String name) {
        if (name == null || name.isBlank()) return "*";
        return name.length() == 1 ? name + "*" : name.substring(0, 1) + "*".repeat(name.length() - 1);
    }
 
    private String maskCardNo(String cardNo) {
        if (cardNo == null || cardNo.isBlank()) return "-";
        return cardNo.length() <= 6 ? cardNo : cardNo.substring(0, 2) + "****" + cardNo.substring(cardNo.length() - 4);
    }
 
    private String maskWorkUnit(String workUnit) {
        if (workUnit == null || workUnit.isBlank()) return "未填写";
        return workUnit.length() <= 4 ? workUnit.substring(0, 1) + "***" : workUnit.substring(0, 2) + "****" + workUnit.substring(workUnit.length() - 2);
    }
 
    private String genderText(Object gender) {
        if (gender instanceof Number value) return value.intValue() == 1 ? "男" : value.intValue() == 2 ? "女" : "-";
        return "-";
    }
 
    private String value(Object source, String fallback) {
        return source == null || String.valueOf(source).isBlank() ? fallback : String.valueOf(source);
    }
 
    private String nextToken() {
        byte[] bytes = new byte[24];
        java.security.SecureRandom random = new java.security.SecureRandom();
        random.nextBytes(bytes);
        return "TC1" + Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
 
    private boolean isAllowedExternalWebUrl(String value) {
        if (value.isBlank() || allowedExternalWebHosts.isEmpty()) return false;
        try {
            URI uri = URI.create(value);
            String host = uri.getHost();
            return "https".equalsIgnoreCase(uri.getScheme()) && host != null && uri.getUserInfo() == null
                    && allowedExternalWebHosts.contains(host.toLowerCase(Locale.ROOT));
        } catch (IllegalArgumentException ignored) {
            return false;
        }
    }
 
    private List<Map<String, Object>> query(String sql, Object... args) {
        return DatabaseRows.normalize(jdbcTemplate.queryForList(sql, args));
    }
}