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
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
package com.talentcard.server.service;
 
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
 
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.request.ServiceAppointmentSubmitRequest;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import com.talentcard.server.common.SensitiveDataCrypto;
import com.talentcard.server.entity.ChildInformation;
import com.talentcard.server.entity.ServiceApplication;
import com.talentcard.server.entity.ServiceApplicationOperationLog;
import com.talentcard.server.mapper.ChildInformationMapper;
import com.talentcard.server.mapper.ServiceApplicationMapper;
import com.talentcard.server.mapper.ServiceApplicationOperationLogMapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class ServiceAppointmentApplicationService {
    public static final String TYPE_MEDICAL = "MEDICAL_RESERVATION";
    public static final String TYPE_FAST_TRACK = "FAST_TRACK";
    public static final String TYPE_HOTEL = "HOTEL_RESERVATION";
    public static final String TYPE_SPOUSE = "SPOUSE_EMPLOYMENT";
    public static final String TYPE_CHILD = "CHILD_ENTRUSTMENT";
 
    private final ServiceApplicationMapper applicationMapper;
    private final ChildInformationMapper childInformationMapper;
    private final ServiceApplicationOperationLogMapper operationLogMapper;
    private final SensitiveDataCrypto sensitiveDataCrypto;
 
    public ServiceAppointmentApplicationService(
            ServiceApplicationMapper applicationMapper,
            ChildInformationMapper childInformationMapper,
            ServiceApplicationOperationLogMapper operationLogMapper,
            SensitiveDataCrypto sensitiveDataCrypto
    ) {
        this.applicationMapper = applicationMapper;
        this.childInformationMapper = childInformationMapper;
        this.operationLogMapper = operationLogMapper;
        this.sensitiveDataCrypto = sensitiveDataCrypto;
    }
 
    @Transactional
    public Map<String, Object> submit(ServiceAppointmentSubmitRequest request, String idempotencyKey) {
        String key = idempotencyKey(idempotencyKey);
        ServiceApplication duplicate = applicationByIdempotencyKey(key);
        if (duplicate != null) {
            return appResult(duplicate);
        }
 
        String applicationType = normalizeType(request.applicationType());
        Map<String, Object> card = activeCard(request.talentId(), request.cardId());
        ServiceApplication application = baseApplication(applicationType, key, card, request.remark());
 
        switch (applicationType) {
            case TYPE_MEDICAL -> fillMedical(application, request);
            case TYPE_FAST_TRACK -> fillFastTrack(application, request);
            case TYPE_HOTEL -> fillHotel(application, request);
            case TYPE_SPOUSE -> fillSpouseEmployment(application, request);
            case TYPE_CHILD -> fillChildEntrustment(application, request, key);
            default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "预约类型不正确");
        }
 
        try {
            applicationMapper.insert(application);
        } catch (DuplicateKeyException exception) {
            ServiceApplication existing = applicationByIdempotencyKey(key);
            if (existing != null) {
                return appResult(existing);
            }
            throw exception;
        }
        ServiceApplication saved = applicationMapper.selectById(application.getId());
        if (saved == null) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "预约申请保存失败");
        }
        ServiceApplicationOperationLog log = new ServiceApplicationOperationLog();
        log.setApplicationId(saved.getId());
        log.setApplicationNo(saved.getApplicationNo());
        log.setOperationType("SUBMIT");
        log.setAfterStatus(saved.getStatus());
        log.setOperatorType("TALENT");
        log.setOperatorId(saved.getTalentId());
        log.setOperatorName(saved.getTalentName());
        log.setRemark("提交预约申请");
        log.setCreatedAt(saved.getSubmittedAt());
        if (operationLogMapper.insert(log) != 1) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "预约申请操作日志保存失败");
        }
        return appResult(saved);
    }
 
    private ServiceApplication baseApplication(String applicationType, String idempotencyKey, Map<String, Object> card, String remark) {
        ServiceApplication application = new ServiceApplication();
        application.setApplicationNo(BusinessIdGenerator.next("SA"));
        application.setApplicationType(applicationType);
        application.setIdempotencyKey(idempotencyKey);
        application.setTalentId(longValue(card, "talentId", "talent_id"));
        application.setCardId(longValue(card, "cardId", "card_id"));
        application.setTalentName(text(card, "talentName", "talent_name"));
        application.setTalentMobile(text(card, "talentMobile", "talent_mobile"));
        application.setTalentLevel(text(card, "talentLevel", "talent_level"));
        application.setCardNo(text(card, "cardNo", "card_no"));
        application.setApplicantIdCardNo(blankToNull(text(card, "talentIdCardNo", "talent_id_card_no")));
        application.setSubmittedAt(LocalDateTime.now());
        application.setStatus(TYPE_CHILD.equals(applicationType) ? "待审核" : "待处理");
        application.setRejectReason(blankToNull(clean(remark)));
        application.setDeleted(0);
        return application;
    }
 
    private void fillMedical(ServiceApplication application, ServiceAppointmentSubmitRequest request) {
        Map<String, Object> merchant = optionalMerchant(request.merchantId(), "medical", "medical", "体检医院不存在或暂未开放");
        applyInstitution(application, merchant, request.hospitalName(), request.contactMobile(), true);
        application.setApplicantName(first(clean(request.applicantName()), application.getTalentName()));
        application.setApplicantMobile(first(clean(request.applicantMobile()), application.getTalentMobile()));
        application.setApplicantIdCardNo(blankToNull(first(request.applicantIdCardNo(), application.getApplicantIdCardNo())));
        applyMedicalPackage(application, request.packageId(), request.packageName());
        String expected = first(clean(request.expectedTime()), clean(request.expectedDate()));
        if (expected.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择期望预约日期");
        }
        application.setExpectedTime(expected);
        application.setEstimatedProcessingTime("提交后1个工作日内由医院确认");
    }
 
    private void fillFastTrack(ServiceApplication application, ServiceAppointmentSubmitRequest request) {
        Map<String, Object> merchant = optionalMerchant(request.merchantId(), "medical", "medical", "医院快捷通道服务不存在或暂未开放");
        applyInstitution(application, merchant, request.hospitalName(), request.contactMobile(), true);
        application.setPackageName(first(clean(request.packageName()), "医院快捷通道"));
        application.setProductName(first(clean(request.productName()), "快捷通道预约"));
        application.setApplicantName(first(clean(request.applicantName()), application.getTalentName()));
        application.setApplicantMobile(first(clean(request.applicantMobile()), application.getTalentMobile()));
        application.setApplicantIdCardNo(blankToNull(first(request.applicantIdCardNo(), application.getApplicantIdCardNo())));
        String expected = first(clean(request.expectedTime()), clean(request.expectedDate()));
        if (expected.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择期望预约日期");
        }
        application.setExpectedTime(expected);
        application.setEstimatedProcessingTime("提交后1个工作日内由服务专员联系确认");
    }
 
    private void fillHotel(ServiceApplication application, ServiceAppointmentSubmitRequest request) {
        Map<String, Object> merchant = optionalMerchant(request.merchantId(), "hotel", null, "酒店不存在或暂未开放");
        applyInstitution(application, merchant, request.merchantName(), request.contactMobile(), false);
        application.setApplicantName(first(clean(request.applicantName()), application.getTalentName()));
        application.setApplicantMobile(first(clean(request.applicantMobile()), application.getTalentMobile()));
        application.setGuestCount(request.guestCount() == null ? 1 : request.guestCount());
        application.setSubsidy(blankToNull(clean(request.subsidy())));
        applyHotelProduct(application, request.productId(), request.productName());
        LocalDate checkIn = date(request.checkInDate(), "入住日期格式不正确", false);
        LocalDate checkOut = date(request.checkOutDate(), "离店日期格式不正确", false);
        if (checkIn == null || checkOut == null) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择入住日期和离店日期");
        }
        if (!checkOut.isAfter(checkIn)) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "离店日期必须晚于入住日期");
        }
        application.setCheckInDate(checkIn);
        application.setCheckOutDate(checkOut);
        application.setExpectedTime(checkIn + " 至 " + checkOut);
        application.setEstimatedProcessingTime("提交后1个工作日内由酒店确认");
    }
 
    private void fillSpouseEmployment(ServiceApplication application, ServiceAppointmentSubmitRequest request) {
        Map<String, Object> merchant = requiredPremiumMerchant(request.merchantId(), "employment", "配偶推荐就业服务不存在或暂未开放");
        applyInstitution(application, merchant, null, null, false);
        application.setProductName("配偶推荐就业");
        application.setSpouseName(requiredText(request.spouseName(), "请填写配偶姓名"));
        application.setSpouseMobile(sensitiveDataCrypto.encrypt(requiredText(request.spouseMobile(), "请填写配偶手机号")));
        application.setSpouseIdCardNo(sensitiveDataCrypto.encrypt(requiredText(request.spouseIdCardNo(), "请填写配偶证件号").toUpperCase(Locale.ROOT)));
        application.setApplicantIdCardNo(application.getSpouseIdCardNo());
        application.setSpousePoliticalStatus(requiredText(request.spousePoliticalStatus(), "请填写政治面貌"));
        application.setSpouseEducation(requiredText(request.spouseEducation(), "请选择学历"));
        application.setSpouseSchoolMajor(requiredText(request.spouseSchoolMajor(), "请填写毕业院校及专业"));
        application.setSpouseWorkUnit(requiredText(request.spouseWorkUnit(), "请填写现工作单位及职务"));
        application.setSpouseInInstitution(requiredText(request.spouseInInstitution(), "请选择是否为机关事业单位在编人员"));
        application.setSpouseWorkExperience(requiredText(request.spouseWorkExperience(), "请填写工作经历"));
        application.setApplicantName(application.getSpouseName());
        application.setApplicantMobile(sensitiveDataCrypto.decrypt(application.getSpouseMobile()));
        application.setExpectedTime(first(clean(request.expectedTime()), "提交后由服务专员联系"));
        application.setEstimatedProcessingTime("提交后5个工作日内由服务专员联系确认");
    }
 
    private void fillChildEntrustment(ServiceApplication application, ServiceAppointmentSubmitRequest request, String idempotencyKey) {
        Map<String, Object> merchant = request.merchantId() == null
                ? Map.of()
                : requiredPremiumMerchant(request.merchantId(), "children", "学生托管服务不存在或暂未开放");
        applyInstitution(application, merchant, first(clean(request.merchantName()), "学生托管服务"), request.contactMobile(), false);
        applyChildrenSchool(application, request.schoolId());
        ChildInformation child = childInformation(application.getTalentId(), request, idempotencyKey);
        application.setChildId(child.getId());
        application.setChildName(child.getChildName());
        application.setChildGender(child.getGender());
        application.setChildBirthDate(child.getBirthDate());
        application.setProofFileIds(child.getProofFileIds());
        application.setApplicantName(child.getChildName());
        application.setApplicantMobile(null);
        application.setApplicantIdCardNo(null);
        application.setProductName(first(application.getProductName(), "学生托管"));
        application.setExpectedTime(first(clean(request.expectedTime()), clean(request.expectedDate()), "审核通过后由服务专员联系"));
        application.setEstimatedProcessingTime("提交后3个工作日内完成审核");
        application.setStatus("待审核");
    }
 
    private void applyMedicalPackage(ServiceApplication application, Long packageId, String packageName) {
        if (packageId == null) {
            application.setPackageName(first(clean(packageName), "体检预约"));
            return;
        }
        Map<String, Object> row = applicationMapper.selectMedicalPackage(packageId, application.getMerchantId());
        if (row == null || row.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "体检套餐不存在");
        }
        if (intValue(row, "status") != 1 || intValue(row, "stock") == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, first(text(row, "unavailableReason"), "体检套餐当前不可预约"));
        }
        Long merchantId = longValue(row, "merchantId", "merchant_id");
        if (application.getMerchantId() == null && merchantId != null) {
            Map<String, Object> merchant = optionalMerchant(merchantId, "medical", "medical", "体检医院不存在或暂未开放");
            applyInstitution(application, merchant, null, null, true);
        }
        application.setPackageName(text(row, "packageName", "package_name"));
    }
 
    private void applyHotelProduct(ServiceApplication application, Long productId, String productName) {
        if (productId == null) {
            application.setProductName(requiredText(productName, "请选择酒店房型或套餐"));
            return;
        }
        Map<String, Object> row = applicationMapper.selectMerchantProduct(productId, application.getMerchantId());
        if (row == null || row.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "酒店房型或套餐不存在");
        }
        if (intValue(row, "status") != 1 || intValue(row, "stock") == 0) {
            throw new BusinessException(ErrorCode.CONFLICT, "酒店房型或套餐当前不可预约");
        }
        Long merchantId = longValue(row, "merchantId", "merchant_id");
        if (application.getMerchantId() == null && merchantId != null) {
            Map<String, Object> merchant = optionalMerchant(merchantId, "hotel", null, "酒店不存在或暂未开放");
            applyInstitution(application, merchant, null, null, false);
        }
        application.setProductName(text(row, "productName", "product_name"));
    }
 
    private void applyChildrenSchool(ServiceApplication application, Long schoolId) {
        if (schoolId == null) {
            return;
        }
        Map<String, Object> row = applicationMapper.selectChildrenSchool(schoolId);
        if (row == null || row.isEmpty() || intValue(row, "enabled") != 1) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "托管学校不存在或暂未开放");
        }
        application.setProductName(text(row, "schoolName", "school_name"));
        application.setContactMobile(first(application.getContactMobile(), text(row, "contactMobile", "contact_mobile")));
        application.setInstitutionAddressSnapshot(first(application.getInstitutionAddressSnapshot(), text(row, "address")));
        application.setInstitutionLatitudeSnapshot(decimalValue(row, "latitude"));
        application.setInstitutionLongitudeSnapshot(decimalValue(row, "longitude"));
    }
 
    private ChildInformation childInformation(Long talentId, ServiceAppointmentSubmitRequest request, String idempotencyKey) {
        if (request.childId() != null) {
            ChildInformation existing = childInformationMapper.selectOne(new QueryWrapper<ChildInformation>()
                    .eq("id", request.childId())
                    .eq("talent_id", talentId)
                    .eq("deleted", 0)
                    .last("limit 1"));
            if (existing == null) {
                throw new BusinessException(ErrorCode.NOT_FOUND, "子女信息不存在或无权使用");
            }
            return existing;
        }
 
        ChildInformation duplicate = childInformationMapper.selectOne(new QueryWrapper<ChildInformation>()
                .eq("talent_id", talentId)
                .eq("idempotency_key", idempotencyKey)
                .eq("deleted", 0)
                .last("limit 1"));
        if (duplicate != null) {
            return duplicate;
        }
 
        List<String> proofIds = proofFileIds(request.proofFileIds());
        if (proofIds.isEmpty()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传亲属关系证明材料");
        }
        ChildInformation child = new ChildInformation();
        child.setTalentId(talentId);
        child.setChildName(requiredText(request.childName(), "请填写子女姓名"));
        child.setGender(gender(request.childGender()));
        child.setBirthDate(childBirthDate(request.childBirthDate()));
        child.setProofFileIds(String.join(",", proofIds));
        child.setAuditStatus("待审核");
        child.setIdempotencyKey(idempotencyKey);
        child.setSubmittedAt(LocalDateTime.now());
        child.setDeleted(0);
        try {
            childInformationMapper.insert(child);
        } catch (DuplicateKeyException exception) {
            ChildInformation existing = childInformationMapper.selectOne(new QueryWrapper<ChildInformation>()
                    .eq("talent_id", talentId)
                    .eq("idempotency_key", idempotencyKey)
                    .eq("deleted", 0)
                    .last("limit 1"));
            if (existing != null) {
                return existing;
            }
            throw exception;
        }
        return child;
    }
 
    private void applyInstitution(ServiceApplication application, Map<String, Object> merchant, String fallbackName, String fallbackMobile, boolean hospital) {
        if (merchant != null && !merchant.isEmpty()) {
            application.setMerchantId(longValue(merchant, "merchantId", "merchant_id", "id"));
            application.setMerchantName(text(merchant, "merchantName", "merchant_name"));
            application.setContactMobile(text(merchant, "contactMobile", "contact_mobile"));
            application.setInstitutionAddressSnapshot(text(merchant, "address"));
            application.setInstitutionLongitudeSnapshot(decimalValue(merchant, "longitude"));
            application.setInstitutionLatitudeSnapshot(decimalValue(merchant, "latitude"));
            if (hospital) {
                application.setHospitalName(application.getMerchantName());
            }
            return;
        }
        String name = clean(fallbackName);
        if (name.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, hospital ? "请选择预约医院" : "请选择服务机构");
        }
        if (hospital) {
            application.setHospitalName(name);
        } else {
            application.setMerchantName(name);
        }
        application.setContactMobile(blankToNull(clean(fallbackMobile)));
    }
 
    private Map<String, Object> optionalMerchant(Long merchantId, String expectedType, String categoryKey, String message) {
        if (merchantId == null) {
            return Map.of();
        }
        Map<String, Object> merchant = categoryKey == null
                ? applicationMapper.selectMerchantSnapshot(merchantId)
                : applicationMapper.selectPremiumMerchantSnapshot(merchantId, categoryKey);
        if (merchant == null || merchant.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, message);
        }
        String actualType = text(merchant, "merchantType", "merchant_type");
        boolean medicalType = "medical".equalsIgnoreCase(expectedType)
                && ("medical".equalsIgnoreCase(actualType) || "healthCheck".equalsIgnoreCase(actualType) || "免费体检".equals(actualType));
        boolean hotelType = "hotel".equalsIgnoreCase(expectedType)
                && ("hotel".equalsIgnoreCase(actualType) || "酒店".equals(actualType) || "酒店服务".equals(actualType));
        if (expectedType != null && !medicalType && !hotelType && !expectedType.equalsIgnoreCase(actualType)) {
            throw new BusinessException(ErrorCode.CONFLICT, message);
        }
        return merchant;
    }
 
    private Map<String, Object> requiredPremiumMerchant(Long merchantId, String categoryKey, String message) {
        if (merchantId == null || merchantId <= 0) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择服务机构");
        }
        Map<String, Object> merchant = applicationMapper.selectPremiumMerchantSnapshot(merchantId, categoryKey);
        if (merchant == null || merchant.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, message);
        }
        return merchant;
    }
 
    private Map<String, Object> activeCard(Long talentId, Long cardId) {
        if (talentId == null && cardId == null) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "人才用户ID或人才卡ID不能为空");
        }
        Map<String, Object> card = applicationMapper.selectActiveCardSnapshot(talentId, cardId);
        if (card == null || card.isEmpty()) {
            throw new BusinessException(ErrorCode.CONFLICT, "未找到有效人才卡");
        }
        return card;
    }
 
    private ServiceApplication applicationByIdempotencyKey(String idempotencyKey) {
        return applicationMapper.selectOne(new QueryWrapper<ServiceApplication>()
                .eq("idempotency_key", idempotencyKey)
                .eq("deleted", 0)
                .last("limit 1"));
    }
 
    private Map<String, Object> appResult(ServiceApplication application) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("applicationId", application.getId());
        result.put("applicationNo", application.getApplicationNo());
        result.put("applicationType", application.getApplicationType());
        result.put("applicationTypeName", typeName(application.getApplicationType()));
        result.put("status", application.getStatus());
        result.put("reviewRequired", TYPE_CHILD.equals(application.getApplicationType()));
        result.put("submittedAt", application.getSubmittedAt());
        result.put("serviceName", first(application.getHospitalName(), application.getMerchantName(), application.getProductName(), "人才服务"));
        result.put("appointmentContent", first(application.getProductName(), application.getPackageName(), typeName(application.getApplicationType())));
        result.put("appointmentTime", first(application.getExpectedTime(), "以专员通知为准"));
        result.put("estimatedProcessingTime", first(application.getEstimatedProcessingTime(), "提交后由服务专员联系确认"));
        result.put("contactMobile", first(application.getContactMobile(), ""));
        result.put("childId", application.getChildId());
        return result;
    }
 
    private String normalizeType(String raw) {
        String value = clean(raw).toUpperCase(Locale.ROOT).replace('-', '_');
        return switch (value) {
            case "MEDICAL", "MEDICAL_CHECKUP", "MEDICAL_RESERVATION", "PHYSICAL_EXAM", "HEALTH_CHECK" -> TYPE_MEDICAL;
            case "FAST_TRACK", "QUICK_CHANNEL", "GREEN_CHANNEL", "MEDICAL_APPLICATION", "HOSPITAL", "HOSPITAL_APPOINTMENT" -> TYPE_FAST_TRACK;
            case "HOTEL", "HOTEL_RESERVATION", "HOTEL_BOOKING" -> TYPE_HOTEL;
            case "SPOUSE", "SPOUSE_EMPLOYMENT", "EMPLOYMENT" -> TYPE_SPOUSE;
            case "CHILD", "CHILDREN", "CHILD_ENTRUSTMENT", "CHILDREN_ENTRUSTMENT", "STUDENT_CARE", "STUDENT_ENTRUSTMENT" -> TYPE_CHILD;
            default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "预约类型仅支持体检、医院快捷通道、酒店、配偶就业、学生托管");
        };
    }
 
    private String typeName(String type) {
        return switch (type) {
            case TYPE_MEDICAL -> "体检预约";
            case TYPE_FAST_TRACK -> "医院快捷通道";
            case TYPE_HOTEL -> "酒店预约";
            case TYPE_SPOUSE -> "配偶推荐就业";
            case TYPE_CHILD -> "学生托管";
            default -> "预约申请";
        };
    }
 
    private String idempotencyKey(String raw) {
        String key = clean(raw);
        if (key.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "提交凭证不能为空");
        }
        if (key.length() > 128) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "提交凭证长度不能超过128");
        }
        return key;
    }
 
    private List<String> proofFileIds(List<String> values) {
        if (values == null) {
            return List.of();
        }
        if (values.size() > 3) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "证明材料最多上传3个");
        }
        return values.stream()
                .map(this::clean)
                .filter(item -> !item.isBlank())
                .peek(item -> {
                    if (item.length() > 255) {
                        throw new BusinessException(ErrorCode.BAD_REQUEST, "证明材料文件ID长度不能超过255");
                    }
                })
                .collect(Collectors.toList());
    }
 
    private String gender(String value) {
        String gender = requiredText(value, "请选择子女性别");
        if (!List.of("男", "女").contains(gender)) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择有效的子女性别");
        }
        return gender;
    }
 
    private LocalDate childBirthDate(String value) {
        LocalDate date = date(value, "子女出生日期格式不正确", true);
        if (date == null) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择子女出生日期");
        }
        if (date.isAfter(LocalDate.now())) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "子女出生日期不能晚于今天");
        }
        return date;
    }
 
    private LocalDate date(String value, String message, boolean required) {
        String text = clean(value);
        if (text.isBlank()) {
            if (required) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, message);
            }
            return null;
        }
        try {
            return LocalDate.parse(text);
        } catch (DateTimeParseException exception) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, message);
        }
    }
 
    private String requiredText(String value, String message) {
        String text = clean(value);
        if (text.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, message);
        }
        return text;
    }
 
    private String first(String... values) {
        for (String value : values) {
            String text = clean(value);
            if (!text.isBlank()) {
                return text;
            }
        }
        return "";
    }
 
    private String text(Map<String, Object> row, String... keys) {
        Object value = value(row, keys);
        return value == null ? "" : String.valueOf(value).trim();
    }
 
    private Long longValue(Map<String, Object> row, String... keys) {
        Object value = value(row, keys);
        if (value instanceof Number number) {
            return number.longValue();
        }
        if (value == null || String.valueOf(value).isBlank()) {
            return null;
        }
        return Long.parseLong(String.valueOf(value));
    }
 
    private int intValue(Map<String, Object> row, String... keys) {
        Object value = value(row, keys);
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value == null || String.valueOf(value).isBlank()) {
            return 0;
        }
        return Integer.parseInt(String.valueOf(value));
    }
 
    private BigDecimal decimalValue(Map<String, Object> row, String... keys) {
        Object value = value(row, keys);
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return BigDecimal.valueOf(number.doubleValue());
        }
        if (value == null || String.valueOf(value).isBlank()) {
            return null;
        }
        return new BigDecimal(String.valueOf(value));
    }
 
    private Object value(Map<String, Object> row, String... keys) {
        if (row == null || row.isEmpty()) {
            return null;
        }
        for (String key : keys) {
            if (row.containsKey(key)) {
                return row.get(key);
            }
            String lower = key.toLowerCase(Locale.ROOT);
            String upper = key.toUpperCase(Locale.ROOT);
            if (row.containsKey(lower)) {
                return row.get(lower);
            }
            if (row.containsKey(upper)) {
                return row.get(upper);
            }
        }
        return row.entrySet().stream()
                .filter(entry -> keysMatch(entry.getKey(), keys))
                .map(Map.Entry::getValue)
                .filter(Objects::nonNull)
                .findFirst()
                .orElse(null);
    }
 
    private boolean keysMatch(String actual, String[] keys) {
        String normalizedActual = actual.replace("_", "").toLowerCase(Locale.ROOT);
        for (String key : keys) {
            if (normalizedActual.equals(key.replace("_", "").toLowerCase(Locale.ROOT))) {
                return true;
            }
        }
        return false;
    }
 
    private String blankToNull(String value) {
        String text = clean(value);
        return text.isBlank() ? null : text;
    }
 
    private String clean(String value) {
        return value == null ? "" : value.trim();
    }
}