package com.talentcard.server.business.portal; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import com.alibaba.fastjson.JSON; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.common.SensitiveDataCrypto; import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class SpouseEmploymentPortalService { private final JdbcTemplate jdbc; private final SensitiveDataCrypto sensitiveDataCrypto; public SpouseEmploymentPortalService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; } @Transactional(readOnly = true) public Map detail(Long merchantId, Long talentId) { Map benefit = benefit(merchantId); Map result = new LinkedHashMap<>(); result.put("benefitId", benefit.get("id") == null ? merchantId : benefit.get("id")); result.put("serviceId", benefit.get("merchantId")); Map matchedRight = matchedRight(benefit.get("merchantRightsConfig"), activeCardLevel(talentId)); result.put("title", benefit.get("title")); result.put("heroImage", benefit.get("coverUrl")); result.put("freeLabel", benefit.get("freeLabel")); result.put("applicableCards", text(matchedRight.get("tag"), "")); result.put("rulesText", text(matchedRight.get("detail"), text(benefit.get("rulesText"), "请咨询服务单位了解配偶推荐就业权益详情"))); result.put("consultationPhone", benefit.get("consultationPhone")); result.put("available", number(benefit.get("enabled")) == 1); result.put("unavailableReason", text(benefit.get("unavailableReason"), "该权益暂未开放")); result.put("formConfig", Map.of( "educationOptions", options(benefit.get("educationOptions")), "institutionOptions", options(benefit.get("institutionOptions")), "requiredFields", List.of("name", "mobile", "idCard", "politicalStatus", "education", "schoolMajor", "workUnit", "inInstitution", "workExperience"), "maxLengths", Map.of("name", 64, "politicalStatus", 64, "schoolMajor", 255, "workUnit", 255, "workExperience", 4000) )); return result; } @Transactional public Map submit(Long merchantId, SpouseEmploymentApplicationRequest request, String idempotencyKey) { if (idempotencyKey == null || idempotencyKey.isBlank() || idempotencyKey.length() > 128) { throw new BusinessException(ErrorCode.BAD_REQUEST, "提交凭证不能为空"); } List> duplicate = query("select id from service_application where idempotency_key = ? and deleted = 0", idempotencyKey); if (!duplicate.isEmpty()) return application(((Number) duplicate.get(0).get("id")).longValue()); Map benefit = benefit(merchantId); if (number(benefit.get("enabled")) != 1) { throw new BusinessException(ErrorCode.CONFLICT, text(benefit.get("unavailableReason"), "该权益暂未开放")); } validateOptions(request, benefit); Map card = activeCard(request.talentId()); String applicationNo = BusinessIdGenerator.next("SE"); Long applicationId; try { jdbc.update(""" insert into service_application( application_no, application_type, idempotency_key, talent_name, talent_mobile, talent_level, card_no, merchant_name, contact_mobile, product_name, expected_time, spouse_name, spouse_mobile, spouse_id_card_no, spouse_political_status, spouse_education, spouse_school_major, spouse_work_unit, spouse_in_institution, spouse_work_experience, submitted_at, status ) values (?, 'SPOUSE_EMPLOYMENT', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), '待处理') """, applicationNo, idempotencyKey, card.get("talentName"), card.get("talentMobile"), card.get("cardLevel"), card.get("cardNo"), benefit.get("merchantName"), benefit.get("consultationPhone"), benefit.get("title"), benefit.get("expectedTime"), clean(request.name()), sensitiveDataCrypto.encrypt(clean(request.mobile())), sensitiveDataCrypto.encrypt(clean(request.idCard()).toUpperCase()), clean(request.politicalStatus()), clean(request.education()), clean(request.schoolMajor()), clean(request.workUnit()), clean(request.inInstitution()), clean(request.workExperience())); applicationId = jdbc.queryForObject("select id from service_application where idempotency_key = ?", Long.class, idempotencyKey); } catch (DuplicateKeyException exception) { List> existing = query("select id from service_application where idempotency_key = ? and deleted = 0 for update", idempotencyKey); if (!existing.isEmpty()) return application(((Number) existing.get(0).get("id")).longValue()); throw exception; } if (applicationId == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "申请创建失败"); return application(applicationId); } private void validateOptions(SpouseEmploymentApplicationRequest request, Map benefit) { if (!options(benefit.get("educationOptions")).contains(clean(request.education()))) { throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择有效的学历"); } if (!options(benefit.get("institutionOptions")).contains(clean(request.inInstitution()))) { throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择是否在编"); } } private Map benefit(Long merchantId) { if (merchantId == null || merchantId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "权益信息不能为空"); List> rows = query(""" select b.id, m.id as merchant_id, m.merchant_name, m.cover_url, coalesce(nullif(b.title, ''), nullif(m.merchant_name, ''), '配偶推荐就业') as title, coalesce(nullif(b.free_label, ''), '免费') as free_label, coalesce(nullif(b.rules_text, ''), nullif(m.rights_text, ''), '请咨询服务单位了解配偶推荐就业权益详情') as rules_text, coalesce(nullif(b.consultation_phone, ''), m.contact_mobile, '') as consultation_phone, coalesce(nullif(b.expected_time, ''), '提交后 5 个工作日内由服务专员联系确认') as expected_time, coalesce(nullif(b.education_options, ''), '高中及以下,大专,本科,硕士研究生,博士研究生') as education_options, coalesce(nullif(b.institution_options, ''), '是,否') as institution_options, coalesce(b.enabled, m.status, 0) as enabled, coalesce(nullif(b.unavailable_reason, ''), '该权益暂未开放') as unavailable_reason, m.merchant_rights_config from merchant m left join spouse_employment_benefit b on b.merchant_id = m.id and b.deleted = 0 where m.id = ? and m.deleted = 0 and m.merchant_type_code = 'employment' """, merchantId); if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "配偶推荐就业权益不存在"); return rows.get(0); } private Map matchedRight(Object config, String cardLevel) { if (cardLevel.isBlank() || config == null) return Map.of(); String raw = text(config, ""); if (raw.isBlank()) return Map.of(); try { List> rights = JSON.parseObject(raw, new com.alibaba.fastjson.TypeReference<>() {}); if (rights == null) return Map.of(); for (Map right : rights) { if (booleanValue(right.get("enabled")) && sameCardLevel(right.get("level"), cardLevel)) return right; } } catch (RuntimeException ignored) { // Fall back to the merchant-level rights description below. } return Map.of(); } private boolean sameCardLevel(Object configured, String actual) { String left = normalizedCardLevel(configured); return !left.isBlank() && left.equals(normalizedCardLevel(actual)); } private String normalizedCardLevel(Object value) { String level = value == null ? "" : String.valueOf(value).trim().toUpperCase(Locale.ROOT); if (level.contains("卓越") || "EXCELLENT".equals(level)) return "EXCELLENT"; if (level.contains("杰出") || "OUTSTANDING".equals(level)) return "OUTSTANDING"; if (level.contains("菁英") || "ELITE".equals(level)) return "ELITE"; return ""; } private String activeCardLevel(Long talentId) { if (talentId == null || talentId <= 0) return ""; List> cards = query(""" select card_level from talent_card where talent_id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1 and (valid_end is null or valid_end >= current_date) order by id desc limit 1 """, talentId); return cards.isEmpty() ? "" : text(cards.get(0).get("cardLevel"), ""); } private boolean booleanValue(Object value) { if (value instanceof Boolean bool) return bool; if (value instanceof Number number) return number.intValue() != 0; String result = text(value, ""); return "true".equalsIgnoreCase(result) || "1".equals(result) || "yes".equalsIgnoreCase(result); } private Map activeCard(Long talentId) { if (talentId == null || talentId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "请先登录后提交"); List> cards = query(""" select c.card_no, c.card_level, u.name as talent_name, u.mobile as talent_mobile from talent_card c join talent_user u on u.id = c.talent_id and u.deleted = 0 where c.talent_id = ? and c.deleted = 0 and c.card_type = 'PERSONAL' and c.status = 1 and (c.valid_end is null or c.valid_end >= current_date) order by c.id desc limit 1 """, talentId); if (cards.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前用户未持有有效人才卡"); return cards.get(0); } private Map application(Long applicationId) { List> rows = query(""" select s.id, s.application_no, s.status, s.submitted_at, s.expected_time, s.merchant_name, s.contact_mobile from service_application s where s.id = ? and s.deleted = 0 and s.application_type = 'SPOUSE_EMPLOYMENT' """, applicationId); if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "就业申请不存在"); Map row = rows.get(0); Map result = new LinkedHashMap<>(); result.put("applicationId", row.get("id")); result.put("applicationNo", row.get("applicationNo")); result.put("status", status(String.valueOf(row.get("status")))); result.put("submittedAt", row.get("submittedAt")); result.put("estimatedProcessingTime", row.get("expectedTime")); result.put("serviceName", row.get("merchantName")); result.put("consultationPhone", row.get("contactMobile")); return result; } private String status(String status) { if ("已处理".equals(status) || "已完成".equals(status)) return "completed"; if ("已驳回".equals(status) || "已取消".equals(status)) return "rejected"; return "processing"; } private List options(Object value) { return Arrays.stream(String.valueOf(value).split(",")) .map(String::trim).filter(item -> !item.isBlank()).toList(); } private List> query(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); } private int number(Object value) { return value instanceof Number number ? number.intValue() : Integer.parseInt(String.valueOf(value)); } private String text(Object value, String fallback) { String result = value == null ? "" : String.valueOf(value).trim(); return result.isBlank() ? fallback : result; } private String clean(String value) { return value == null ? "" : value.trim(); } }