From ddc84625dcdb22a1ec2407f6adc7eb2e7a839ef0 Mon Sep 17 00:00:00 2001
From: wenminjie <1036699586@qq.com>
Date: 星期一, 03 八月 2026 18:11:46 +0800
Subject: [PATCH] 小程序一些接口,加了mybatisplus、swagger

---
 talent-card-server/src/main/java/com/talentcard/server/config/SwaggerConfig.java                                    |   18 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitTalentCard.java                                |   25 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrder.java                                     |   29 
 talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationMapper.java                         |  120 +
 talent-card-server/src/main/java/com/talentcard/server/business/portal/BenefitVerificationCodeController.java       |   35 
 talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java                |   50 
 talent-card-server/src/main/java/com/talentcard/server/entity/ServiceApplication.java                               |   70 
 talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyResponse.java                     |   46 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMovieTicketRuleMapper.java                     |    9 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponClaimMapper.java                   |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/ChildInformation.java                                 |   28 
 talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java                             |  459 +++++
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMovieTicketRule.java                           |   17 
 talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java            |  583 +++++++
 talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyItemResponse.java                 |   52 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitVerifyRecord.java                              |   30 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchant.java                                  |   17 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponMapper.java                        |    9 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderDynamicVerificationCodeMapper.java        |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApplyStatusLog.java                      |   54 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java        |   74 
 talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentSubmitRequest.java                 |  156 +
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantMapper.java                            |    9 
 talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java                       |  192 ++
 talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApply.java                               |  108 +
 talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java                    |    5 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCoupon.java                              |   24 
 talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java                  |  185 ++
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVoucherMapper.java                     |    9 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java        |   93 +
 talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationService.java                      |  508 ++++++
 talent-card-server/src/main/java/com/talentcard/server/common/SensitiveDataCrypto.java                              |   24 
 talent-card-server/src/main/java/com/talentcard/server/business/portal/ServiceAppointmentApplicationController.java |   35 
 talent-card-server/src/main/java/com/talentcard/server/config/ServiceAppointmentExceptionHandler.java               |   82 +
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitTalentCardMapper.java                          |    9 
 talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyMapper.java                         |   35 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrderDynamicVerificationCode.java              |   21 
 talent-card-server/src/main/java/com/talentcard/server/request/DependentCardAuditRequest.java                       |   16 
 talent-card-server/src/main/java/com/talentcard/server/request/DependentCardApplyRequest.java                       |   36 
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/BenefitVerificationController.java         |   39 
 talent-card-server/src/main/java/com/talentcard/server/mapper/ChildInformationMapper.java                           |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVoucher.java                           |   23 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantProductMapper.java                     |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantBenefitRule.java                       |   21 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantBenefitRuleMapper.java                 |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantProduct.java                           |   25 
 talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationCodeRequest.java                  |   29 
 talent-card-server/pom.xml                                                                                          |   10 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitVerifyRecordMapper.java                        |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCouponClaim.java                         |   31 
 talent-card-server/src/main/java/com/talentcard/server/request/DependentCardUnbindRequest.java                      |   14 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVerificationCode.java                  |   19 
 talent-card-server/src/main/java/com/talentcard/server/TalentCardServerApplication.java                             |    5 
 talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java                  |  457 +++++
 talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyStatusLogMapper.java                |    9 
 talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentStatusUpdateRequest.java           |   26 
 talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java                  |  407 +++++
 talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java                |    5 
 talent-card-server/src/main/java/com/talentcard/server/config/BenefitVerificationExceptionHandler.java              |   76 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVerificationCodeMapper.java            |    9 
 talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFamilyCardApplication.java                     |   18 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderMapper.java                               |    9 
 talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFamilyCardApplicationMapper.java               |    9 
 talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationRequest.java                      |   23 
 talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java            |   73 
 talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyStatusLogResponse.java            |   34 
 66 files changed, 4,691 insertions(+), 6 deletions(-)

diff --git a/talent-card-server/pom.xml b/talent-card-server/pom.xml
index 07f77b4..782211b 100644
--- a/talent-card-server/pom.xml
+++ b/talent-card-server/pom.xml
@@ -39,6 +39,16 @@
 			<artifactId>spring-boot-starter-data-jpa</artifactId>
 		</dependency>
 		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+			<version>3.5.17</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springdoc</groupId>
+			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+			<version>3.1.0</version>
+		</dependency>
+		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-data-redis</artifactId>
 		</dependency>
diff --git a/talent-card-server/src/main/java/com/talentcard/server/TalentCardServerApplication.java b/talent-card-server/src/main/java/com/talentcard/server/TalentCardServerApplication.java
index 00b3601..12a7252 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/TalentCardServerApplication.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/TalentCardServerApplication.java
@@ -3,6 +3,7 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
@@ -11,7 +12,9 @@
 public class TalentCardServerApplication {
 
 	public static void main(String[] args) {
-		SpringApplication.run(TalentCardServerApplication.class, args);
+		ConfigurableApplicationContext context = SpringApplication.run(TalentCardServerApplication.class, args);
+		String port = context.getEnvironment().getProperty("local.server.port", "8080");
+		System.out.printf("Swagger UI: http://localhost:%s/swagger-ui/index.html%n", port);
 	}
 
 }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java
new file mode 100644
index 0000000..fe9100c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java
@@ -0,0 +1,74 @@
+package com.talentcard.server.business.admin;
+
+import java.util.List;
+
+import com.talentcard.server.business.PageResult;
+import com.talentcard.server.business.admin.auth.AdminAuthInterceptor;
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.DependentCardAuditRequest;
+import com.talentcard.server.response.DependentCardApplyAdminResponse;
+import com.talentcard.server.response.DependentCardApplyStatusLogResponse;
+import com.talentcard.server.service.DependentCardApplyAdminService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "管理后台-家属附属卡审核", description = "家属附属卡申请查询、审核和状态记录")
+@RestController
+@RequestMapping("/api/admin/dependent-card-applies")
+public class DependentCardApplyAdminController {
+
+	private final DependentCardApplyAdminService service;
+
+	public DependentCardApplyAdminController(DependentCardApplyAdminService service) {
+		this.service = service;
+	}
+
+	@Operation(summary = "分页查询家属附属卡申请")
+	@GetMapping
+	public ApiResponse<PageResult<DependentCardApplyAdminResponse>> page(
+			@RequestParam(required = false) Long talentCardId,
+			@RequestParam(required = false) Integer status,
+			@RequestParam(defaultValue = "1") int page,
+			@RequestParam(defaultValue = "10") int size
+	) {
+		return ApiResponse.success(service.page(talentCardId, status, page, size));
+	}
+
+	@Operation(summary = "查询家属附属卡申请详情")
+	@GetMapping("/{id}")
+	public ApiResponse<DependentCardApplyAdminResponse> detail(@PathVariable Long id) {
+		return ApiResponse.success(service.detail(id));
+	}
+
+	@Operation(summary = "审核家属附属卡申请", description = "仅待审核申请可以变更为通过或驳回")
+	@ApiResponses({
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "审核成功"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "审核参数不正确"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "申请不存在"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "状态冲突或绑定名额已满")
+	})
+	@PatchMapping("/{id}/audit")
+	public ApiResponse<DependentCardApplyAdminResponse> audit(
+			@PathVariable Long id,
+			@Valid @RequestBody DependentCardAuditRequest request,
+			@RequestAttribute(AdminAuthInterceptor.OPERATOR_ID_ATTRIBUTE) Long operatorId
+	) {
+		return ApiResponse.success(service.audit(id, request, operatorId));
+	}
+
+	@Operation(summary = "查询家属附属卡申请状态记录")
+	@GetMapping("/{id}/status-logs")
+	public ApiResponse<List<DependentCardApplyStatusLogResponse>> statusLogs(@PathVariable Long id) {
+		return ApiResponse.success(service.statusLogs(id));
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java
new file mode 100644
index 0000000..d21294b
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java
@@ -0,0 +1,93 @@
+package com.talentcard.server.business.admin;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import com.talentcard.server.business.PageResult;
+import com.talentcard.server.business.admin.auth.AdminAuthInterceptor;
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.ServiceAppointmentStatusUpdateRequest;
+import com.talentcard.server.service.ServiceAppointmentAdminService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "后台-预约申请", description = "后台查看、处理、导出体检、医院快捷通道、酒店、配偶就业、学生托管预约申请")
+@RestController
+@RequestMapping("/api/admin/service-appointment-applications")
+public class ServiceAppointmentAdminController {
+    private final ServiceAppointmentAdminService service;
+
+    public ServiceAppointmentAdminController(ServiceAppointmentAdminService service) {
+        this.service = service;
+    }
+
+    @Operation(summary = "预约申请列表", description = "分页查看各类预约申请,支持类型、状态、人才、服务机构和提交时间筛选")
+    @GetMapping
+    public ApiResponse<PageResult<Map<String, Object>>> page(
+            @Parameter(description = "预约类型") @RequestParam(required = false) String applicationType,
+            @Parameter(description = "人才/申请人/手机号/卡号/申请编号关键词") @RequestParam(required = false) String keyword,
+            @Parameter(description = "服务机构关键词") @RequestParam(required = false) String serviceKeyword,
+            @Parameter(description = "人才卡等级") @RequestParam(required = false) String cardLevel,
+            @Parameter(description = "处理状态") @RequestParam(required = false) String status,
+            @Parameter(description = "提交开始日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedStart,
+            @Parameter(description = "提交结束日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedEnd,
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
+            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
+    ) {
+        return ApiResponse.success(service.page(applicationType, keyword, serviceKeyword, cardLevel, status, submittedStart, submittedEnd, page, size));
+    }
+
+    @Operation(summary = "预约申请详情", description = "查看预约申请完整信息,包含服务快照、申请人、配偶或子女信息")
+    @GetMapping("/{id}")
+    public ApiResponse<Map<String, Object>> detail(@PathVariable Long id) {
+        return ApiResponse.success(service.detail(id));
+    }
+
+    @Operation(summary = "处理预约申请", description = "维护处理状态和处理备注;学生托管审核通过/不通过时同步更新子女信息审核状态")
+    @PatchMapping("/{id}/status")
+    public ApiResponse<Map<String, Object>> updateStatus(
+            @PathVariable Long id,
+            @Valid @RequestBody ServiceAppointmentStatusUpdateRequest request,
+            @RequestAttribute(value = AdminAuthInterceptor.OPERATOR_ID_ATTRIBUTE, required = false) Long operatorId
+    ) {
+        return ApiResponse.success(service.updateStatus(id, request, operatorId));
+    }
+
+    @Operation(summary = "导出预约申请", description = "按当前筛选条件导出预约申请Excel")
+    @GetMapping(value = "/export", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+    public void export(
+            @Parameter(description = "预约类型") @RequestParam(required = false) String applicationType,
+            @Parameter(description = "人才/申请人/手机号/卡号/申请编号关键词") @RequestParam(required = false) String keyword,
+            @Parameter(description = "服务机构关键词") @RequestParam(required = false) String serviceKeyword,
+            @Parameter(description = "人才卡等级") @RequestParam(required = false) String cardLevel,
+            @Parameter(description = "处理状态") @RequestParam(required = false) String status,
+            @Parameter(description = "提交开始日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedStart,
+            @Parameter(description = "提交结束日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedEnd,
+            HttpServletResponse response
+    ) throws IOException {
+        byte[] data = service.exportExcel(applicationType, keyword, serviceKeyword, cardLevel, status, submittedStart, submittedEnd);
+        String filename = URLEncoder.encode("预约申请列表.xlsx", StandardCharsets.UTF_8).replace("+", "%20");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet").toString());
+        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + filename);
+        response.setContentLength(data.length);
+        response.getOutputStream().write(data);
+        response.flushBuffer();
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
index 8c9d041..0e0cbd8 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
@@ -8,6 +8,8 @@
 
 @Component
 public class AdminAuthInterceptor implements HandlerInterceptor {
+    public static final String OPERATOR_ID_ATTRIBUTE = "authenticatedAdminOperatorId";
+
     private final AdminAuthService service;
 
     public AdminAuthInterceptor(AdminAuthService service) {
@@ -16,7 +18,8 @@
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
+        Long operatorId = service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
+        request.setAttribute(OPERATOR_ID_ATTRIBUTE, operatorId);
         return true;
     }
 }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
index 997d156..0b9a58e 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthService.java
@@ -80,11 +80,11 @@
     }
 
     @Transactional
-    public void authenticate(String authorization) {
+    public Long authenticate(String authorization) {
         String token = bearer(authorization);
         if (token.isBlank()) throw new BusinessException(ErrorCode.UNAUTHORIZED, "请先登录管理后台");
         List<Map<String, Object>> rows = rows("""
-                select s.id session_id, o.status
+                select s.id session_id, s.operator_id, o.status
                 from admin_session s join admin_operator o on o.id = s.operator_id
                 where s.access_token_hash = ? and s.revoked_at is null and s.access_expires_at > now()
                   and o.deleted = 0
@@ -95,6 +95,7 @@
             throw new BusinessException(ErrorCode.FORBIDDEN, "当前管理员账号已停用");
         }
         jdbc.update("update admin_session set last_used_at = now() where id = ?", number(rows.get(0).get("sessionId")));
+        return number(rows.get(0).get("operatorId"));
     }
 
     private Map<String, Object> issueSession(Map<String, Object> operator) {
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/BenefitVerificationController.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/BenefitVerificationController.java
new file mode 100644
index 0000000..e991956
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/BenefitVerificationController.java
@@ -0,0 +1,39 @@
+package com.talentcard.server.business.merchant;
+
+import java.util.Map;
+
+import com.talentcard.server.business.merchant.auth.MerchantAuthService;
+import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.BenefitVerificationRequest;
+import com.talentcard.server.service.BenefitVerificationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "商户端-权益核销", description = "商户核销电影票、酒店权益5分钟核销码")
+@RestController
+@RequestMapping("/api/merchant/benefit-verifications")
+public class BenefitVerificationController {
+    private final BenefitVerificationService service;
+
+    public BenefitVerificationController(BenefitVerificationService service) {
+        this.service = service;
+    }
+
+    @Operation(summary = "核销权益码", description = "商户核销小程序生成的电影票/酒店5分钟核销码,并自动校验主卡共享额度、库存和核销状态")
+    @PostMapping("/verify")
+    public ApiResponse<Map<String, Object>> verify(
+            @RequestAttribute(MerchantAuthService.PRINCIPAL_ATTRIBUTE) MerchantPrincipal principal,
+            @Valid @RequestBody BenefitVerificationRequest request,
+            @RequestHeader(value = "X-Idempotency-Key", required = false) String idempotencyKey
+    ) {
+        return ApiResponse.success(service.verify(principal, request, idempotencyKey));
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/BenefitVerificationCodeController.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/BenefitVerificationCodeController.java
new file mode 100644
index 0000000..ba864ab
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/BenefitVerificationCodeController.java
@@ -0,0 +1,35 @@
+package com.talentcard.server.business.portal;
+
+import java.util.Map;
+
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.BenefitVerificationCodeRequest;
+import com.talentcard.server.service.BenefitVerificationCodeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "小程序-权益核销码", description = "小程序生成电影票、酒店权益5分钟核销码")
+@RestController
+@RequestMapping("/api/app/benefit-verification-codes")
+public class BenefitVerificationCodeController {
+    private final BenefitVerificationCodeService service;
+
+    public BenefitVerificationCodeController(BenefitVerificationCodeService service) {
+        this.service = service;
+    }
+
+    @Operation(summary = "生成权益核销码", description = "根据主卡/亲属附属卡共享额度、商户剩余券或酒店库存生成5分钟有效核销码")
+    @PostMapping
+    public ApiResponse<Map<String, Object>> create(
+            @Valid @RequestBody BenefitVerificationCodeRequest request,
+            @RequestHeader(value = "X-Idempotency-Key", required = false) String idempotencyKey
+    ) {
+        return ApiResponse.success(service.generate(request, idempotencyKey));
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java
new file mode 100644
index 0000000..fd881b7
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java
@@ -0,0 +1,73 @@
+package com.talentcard.server.business.portal;
+
+import java.util.List;
+
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.DependentCardApplyRequest;
+import com.talentcard.server.request.DependentCardUnbindRequest;
+import com.talentcard.server.response.DependentCardApplyItemResponse;
+import com.talentcard.server.service.DependentCardApplyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "家属附属卡申请", description = "小程序家属附属卡申请、查询与解绑")
+@RestController
+@RequestMapping("/api/app/dependent-card-applies")
+public class DependentCardApplyController {
+
+	private final DependentCardApplyService service;
+
+	public DependentCardApplyController(DependentCardApplyService service) {
+		this.service = service;
+	}
+
+	@Operation(
+			summary = "提交家属附属卡申请",
+			description = "仅已开通权限的卓越人才卡可申请;每张主卡最多绑定2个家属,状态0/1/2的申请累计最多3个"
+	)
+	@ApiResponses({
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "申请提交成功"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数不正确"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "主卡不存在或已失效"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "无申请权限、绑定名额已满或累计申请已达到3个"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "申请保存失败")
+	})
+	@PostMapping
+	public ApiResponse<Void> submit(
+			@Valid @RequestBody DependentCardApplyRequest request
+	) {
+		service.submit(request);
+		return ApiResponse.success();
+	}
+
+	@Operation(summary = "查询家属附属卡申请", description = "手机号中间4位统一显示为****")
+	@GetMapping
+	public ApiResponse<List<DependentCardApplyItemResponse>> list(
+			@RequestParam Long talentCardId
+	) {
+		return ApiResponse.success(service.list(talentCardId));
+	}
+
+	@Operation(summary = "解绑家属附属卡", description = "仅已通过审核的家属附属卡可以解绑")
+	@ApiResponses({
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "解绑成功"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "申请不存在"),
+			@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "当前状态不允许解绑")
+	})
+	@PostMapping("/{id}/unbind")
+	public ApiResponse<DependentCardApplyItemResponse> unbind(
+			@PathVariable Long id,
+			@Valid @RequestBody DependentCardUnbindRequest request
+	) {
+		return ApiResponse.success(service.unbind(id, request));
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/ServiceAppointmentApplicationController.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ServiceAppointmentApplicationController.java
new file mode 100644
index 0000000..dc66062
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ServiceAppointmentApplicationController.java
@@ -0,0 +1,35 @@
+package com.talentcard.server.business.portal;
+
+import java.util.Map;
+
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.request.ServiceAppointmentSubmitRequest;
+import com.talentcard.server.service.ServiceAppointmentApplicationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "小程序-预约申请", description = "小程序提交体检、医院快捷通道、酒店、配偶就业、学生托管预约申请")
+@RestController
+@RequestMapping("/api/app/service-appointment-applications")
+public class ServiceAppointmentApplicationController {
+    private final ServiceAppointmentApplicationService service;
+
+    public ServiceAppointmentApplicationController(ServiceAppointmentApplicationService service) {
+        this.service = service;
+    }
+
+    @Operation(summary = "提交预约申请", description = "按预约类型保存预约信息;学生托管默认进入待审核状态,其他服务默认待处理")
+    @PostMapping
+    public ApiResponse<Map<String, Object>> submit(
+            @Valid @RequestBody ServiceAppointmentSubmitRequest request,
+            @RequestHeader(value = "X-Idempotency-Key", required = false) String idempotencyKey
+    ) {
+        return ApiResponse.success(service.submit(request, idempotencyKey));
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/common/SensitiveDataCrypto.java b/talent-card-server/src/main/java/com/talentcard/server/common/SensitiveDataCrypto.java
index 03c9b17..5ecc238 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/common/SensitiveDataCrypto.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/common/SensitiveDataCrypto.java
@@ -3,6 +3,7 @@
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.SecureRandom;
+import java.util.Arrays;
 import java.util.Base64;
 
 import javax.crypto.Cipher;
@@ -16,6 +17,7 @@
 public class SensitiveDataCrypto {
 
     private static final SecureRandom RANDOM = new SecureRandom();
+    private static final String VERSION_PREFIX = "v1:";
     private final byte[] key;
 
     public SensitiveDataCrypto(@Value("${app.privacy.encryption-key}") String encryptionKey) {
@@ -37,9 +39,27 @@
             byte[] payload = new byte[iv.length + encrypted.length];
             System.arraycopy(iv, 0, payload, 0, iv.length);
             System.arraycopy(encrypted, 0, payload, iv.length, encrypted.length);
-            return "v1:" + Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
+            return VERSION_PREFIX + Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
         } catch (Exception exception) {
             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "敏感信息加密失败");
         }
     }
-}
+
+    public String decrypt(String cipherText) {
+        if (cipherText == null || cipherText.isBlank()) return "";
+        if (!cipherText.startsWith(VERSION_PREFIX)) return cipherText;
+        try {
+            byte[] payload = Base64.getUrlDecoder().decode(cipherText.substring(VERSION_PREFIX.length()));
+            if (payload.length <= 12) {
+                throw new IllegalArgumentException("Invalid encrypted payload");
+            }
+            byte[] iv = Arrays.copyOfRange(payload, 0, 12);
+            byte[] encrypted = Arrays.copyOfRange(payload, 12, payload.length);
+            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
+            return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
+        } catch (Exception exception) {
+            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "敏感信息解密失败");
+        }
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/config/BenefitVerificationExceptionHandler.java b/talent-card-server/src/main/java/com/talentcard/server/config/BenefitVerificationExceptionHandler.java
new file mode 100644
index 0000000..29950d2
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/config/BenefitVerificationExceptionHandler.java
@@ -0,0 +1,76 @@
+package com.talentcard.server.config;
+
+import java.util.stream.Collectors;
+
+import com.talentcard.server.business.merchant.BenefitVerificationController;
+import com.talentcard.server.business.portal.BenefitVerificationCodeController;
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.common.BusinessException;
+import jakarta.validation.ConstraintViolationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@RestControllerAdvice(assignableTypes = {
+        BenefitVerificationCodeController.class,
+        BenefitVerificationController.class
+})
+public class BenefitVerificationExceptionHandler {
+    private static final Logger log = LoggerFactory.getLogger(BenefitVerificationExceptionHandler.class);
+    private static final int ERROR_CODE = 500;
+
+    @ExceptionHandler(BusinessException.class)
+    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException exception) {
+        return error(exception.getMessage());
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
+        String message = exception.getBindingResult().getFieldErrors().stream()
+                .map(error -> error.getField() + " " + error.getDefaultMessage())
+                .collect(Collectors.joining("; "));
+        return error(message);
+    }
+
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<ApiResponse<Void>> handleBindException(BindException exception) {
+        String message = exception.getBindingResult().getFieldErrors().stream()
+                .map(error -> error.getField() + " " + error.getDefaultMessage())
+                .collect(Collectors.joining("; "));
+        return error(message);
+    }
+
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException exception) {
+        String message = exception.getConstraintViolations().stream()
+                .map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
+                .collect(Collectors.joining("; "));
+        return error(message);
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<ApiResponse<Void>> handleUnreadableMessage(HttpMessageNotReadableException exception) {
+        return error("请求内容格式不正确");
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ApiResponse<Void>> handleException(Exception exception) {
+        log.error("Benefit verification request exception", exception);
+        return error("服务器内部错误");
+    }
+
+    private ResponseEntity<ApiResponse<Void>> error(String message) {
+        String safeMessage = message == null || message.isBlank() ? "服务器内部错误" : message;
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                .body(ApiResponse.failure(ERROR_CODE, safeMessage));
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/config/ServiceAppointmentExceptionHandler.java b/talent-card-server/src/main/java/com/talentcard/server/config/ServiceAppointmentExceptionHandler.java
new file mode 100644
index 0000000..48a1be0
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/config/ServiceAppointmentExceptionHandler.java
@@ -0,0 +1,82 @@
+package com.talentcard.server.config;
+
+import java.util.stream.Collectors;
+
+import com.talentcard.server.business.admin.ServiceAppointmentAdminController;
+import com.talentcard.server.business.portal.ServiceAppointmentApplicationController;
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.common.BusinessException;
+import jakarta.validation.ConstraintViolationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@RestControllerAdvice(assignableTypes = {
+        ServiceAppointmentApplicationController.class,
+        ServiceAppointmentAdminController.class
+})
+public class ServiceAppointmentExceptionHandler {
+    private static final Logger log = LoggerFactory.getLogger(ServiceAppointmentExceptionHandler.class);
+    private static final int ERROR_CODE = 500;
+
+    @ExceptionHandler(BusinessException.class)
+    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException exception) {
+        return error(exception.getMessage());
+    }
+
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
+        String message = exception.getBindingResult().getFieldErrors().stream()
+                .map(error -> error.getDefaultMessage() == null ? error.getField() + "参数不正确" : error.getDefaultMessage())
+                .collect(Collectors.joining(";"));
+        return error(message);
+    }
+
+    @ExceptionHandler(BindException.class)
+    public ResponseEntity<ApiResponse<Void>> handleBindException(BindException exception) {
+        String message = exception.getBindingResult().getFieldErrors().stream()
+                .map(error -> error.getDefaultMessage() == null ? error.getField() + "参数不正确" : error.getDefaultMessage())
+                .collect(Collectors.joining(";"));
+        return error(message);
+    }
+
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException exception) {
+        String message = exception.getConstraintViolations().stream()
+                .map(violation -> violation.getMessage())
+                .collect(Collectors.joining(";"));
+        return error(message);
+    }
+
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    public ResponseEntity<ApiResponse<Void>> handleMissingRequestParameter(MissingServletRequestParameterException exception) {
+        return error("请求参数不能为空:" + exception.getParameterName());
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<ApiResponse<Void>> handleUnreadableMessage(HttpMessageNotReadableException exception) {
+        return error("请求内容格式不正确");
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ApiResponse<Void>> handleException(Exception exception) {
+        log.error("Service appointment request exception", exception);
+        return error("服务器内部错误");
+    }
+
+    private ResponseEntity<ApiResponse<Void>> error(String message) {
+        String safeMessage = message == null || message.isBlank() ? "服务器内部错误" : message;
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                .body(ApiResponse.failure(ERROR_CODE, safeMessage));
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/config/SwaggerConfig.java b/talent-card-server/src/main/java/com/talentcard/server/config/SwaggerConfig.java
new file mode 100644
index 0000000..d3d684f
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/config/SwaggerConfig.java
@@ -0,0 +1,18 @@
+package com.talentcard.server.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+	@Bean
+	public OpenAPI talentCardOpenApi() {
+		return new OpenAPI().info(new Info()
+				.title("人才卡服务接口")
+				.description("人才卡后端服务 OpenAPI 文档,启动后访问 /swagger-ui/index.html 或 /swagger-ui.html")
+				.version("v1"));
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCoupon.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCoupon.java
new file mode 100644
index 0000000..642ac7c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCoupon.java
@@ -0,0 +1,24 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDate;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("cinema_coupon")
+public class BenefitCinemaCoupon {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long merchantId;
+    private String couponName;
+    private Integer stock;
+    private Integer perUserLimit;
+    private LocalDate validEnd;
+    private String talentLevels;
+    private Integer status;
+    private Integer sortNo;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCouponClaim.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCouponClaim.java
new file mode 100644
index 0000000..ec4bced
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitCinemaCouponClaim.java
@@ -0,0 +1,31 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("cinema_coupon_claim")
+public class BenefitCinemaCouponClaim {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String claimNo;
+    private Long couponId;
+    private Long merchantId;
+    private Long talentId;
+    private Long cardId;
+    private String cardNo;
+    private LocalDate validEnd;
+    private String status;
+    private String idempotencyKey;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    private Integer deleted;
+    private String exchangeCode;
+    private String codeStatus;
+    private LocalDateTime usedAt;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFamilyCardApplication.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFamilyCardApplication.java
new file mode 100644
index 0000000..2d572ab
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFamilyCardApplication.java
@@ -0,0 +1,18 @@
+package com.talentcard.server.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("family_card_application")
+public class BenefitFamilyCardApplication {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long talentId;
+    private String status;
+    private Long familyTalentId;
+    private Long familyCardId;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVerificationCode.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVerificationCode.java
new file mode 100644
index 0000000..8299adf
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVerificationCode.java
@@ -0,0 +1,19 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("free_stay_verification_code")
+public class BenefitFreeStayVerificationCode {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String codeToken;
+    private Long voucherId;
+    private LocalDateTime expiresAt;
+    private Integer status;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVoucher.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVoucher.java
new file mode 100644
index 0000000..90451f7
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitFreeStayVoucher.java
@@ -0,0 +1,23 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDate;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("talent_free_stay_voucher")
+public class BenefitFreeStayVoucher {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String voucherNo;
+    private Long talentId;
+    private Long merchantId;
+    private Long cardId;
+    private Integer totalCount;
+    private Integer usedCount;
+    private LocalDate validEnd;
+    private Integer status;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchant.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchant.java
new file mode 100644
index 0000000..d0b714d
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchant.java
@@ -0,0 +1,17 @@
+package com.talentcard.server.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("merchant")
+public class BenefitMerchant {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String merchantName;
+    private String merchantType;
+    private Integer status;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantBenefitRule.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantBenefitRule.java
new file mode 100644
index 0000000..f0f4f48
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantBenefitRule.java
@@ -0,0 +1,21 @@
+package com.talentcard.server.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("merchant_benefit_rule")
+public class BenefitMerchantBenefitRule {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long merchantId;
+    private Long productId;
+    private String cardLevel;
+    private Integer freeCount;
+    private String resetCycle;
+    private Integer canFamilyUse;
+    private Integer status;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantProduct.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantProduct.java
new file mode 100644
index 0000000..8222eb0
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMerchantProduct.java
@@ -0,0 +1,25 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("merchant_product")
+public class BenefitMerchantProduct {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long merchantId;
+    private String productNo;
+    private String productType;
+    private String productName;
+    private Integer stock;
+    private Integer salesCount;
+    private LocalDateTime validStartTime;
+    private LocalDateTime validEndTime;
+    private Integer status;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMovieTicketRule.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMovieTicketRule.java
new file mode 100644
index 0000000..ef9ae3c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitMovieTicketRule.java
@@ -0,0 +1,17 @@
+package com.talentcard.server.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("movie_ticket_rule")
+public class BenefitMovieTicketRule {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String talentLevel;
+    private Integer enabled;
+    private Integer claimableCount;
+    private String resetCycle;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrder.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrder.java
new file mode 100644
index 0000000..5549274
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrder.java
@@ -0,0 +1,29 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("benefit_order")
+public class BenefitOrder {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String orderNo;
+    private Long talentId;
+    private Long cardId;
+    private Long benefitId;
+    private Long merchantId;
+    private Long storeId;
+    private Long productId;
+    private Integer orderStatus;
+    private Integer verifyStatus;
+    private Long payAmount;
+    private Long subsidyAmount;
+    private String productNameSnapshot;
+    private LocalDateTime updatedAt;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrderDynamicVerificationCode.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrderDynamicVerificationCode.java
new file mode 100644
index 0000000..29457e6
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitOrderDynamicVerificationCode.java
@@ -0,0 +1,21 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("order_dynamic_verification_code")
+public class BenefitOrderDynamicVerificationCode {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String codeToken;
+    private String displayCode;
+    private Long orderId;
+    private Long talentId;
+    private LocalDateTime expiresAt;
+    private Integer status;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitTalentCard.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitTalentCard.java
new file mode 100644
index 0000000..1f3476c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitTalentCard.java
@@ -0,0 +1,25 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("talent_card")
+public class BenefitTalentCard {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long talentId;
+    private String cardNo;
+    private String cardType;
+    private String cardLevel;
+    private LocalDate validStart;
+    private LocalDate validEnd;
+    private Integer status;
+    private LocalDateTime expireTime;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitVerifyRecord.java b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitVerifyRecord.java
new file mode 100644
index 0000000..90cdef6
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/BenefitVerifyRecord.java
@@ -0,0 +1,30 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("verify_record")
+public class BenefitVerifyRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String verifyNo;
+    private Long orderId;
+    private Long talentId;
+    private Long cardId;
+    private Long benefitId;
+    private Long merchantId;
+    private Long storeId;
+    private Long staffId;
+    private String verifyType;
+    private Long verifyAmount;
+    private Long subsidyAmount;
+    private LocalDateTime verifyTime;
+    private String remark;
+    private Integer status;
+    private String idempotencyKey;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/ChildInformation.java b/talent-card-server/src/main/java/com/talentcard/server/entity/ChildInformation.java
new file mode 100644
index 0000000..d631d7c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/ChildInformation.java
@@ -0,0 +1,28 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("child_information")
+public class ChildInformation {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long talentId;
+    private String childName;
+    private String gender;
+    private LocalDate birthDate;
+    private String proofFileIds;
+    private String auditStatus;
+    private String rejectReason;
+    private String idempotencyKey;
+    private LocalDateTime submittedAt;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApply.java b/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApply.java
new file mode 100644
index 0000000..f111f7b
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApply.java
@@ -0,0 +1,108 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+@TableName("dependent_card_apply")
+public class DependentCardApply {
+
+	public static final int STATUS_PENDING_REVIEW = 0;
+	public static final int STATUS_APPROVED = 1;
+	public static final int STATUS_REJECTED = 2;
+	public static final int STATUS_CANCELLED = 3;
+
+	@TableId(type = IdType.AUTO)
+	private Long id;
+	private String applyNo;
+	private Long talentId;
+	private Long talentCardId;
+	private String talentName;
+	private String talentMobile;
+	private String talentCardNo;
+	private String cardLevel;
+	@TableField(exist = false)
+	private Integer isFamilyCardEnabled;
+	@TableField(exist = false)
+	private Integer familyCardLimit;
+	private String dependentName;
+	private String dependentMobile;
+	private String dependentIdCardNo;
+	private String relation;
+	private String proofFiles;
+	private String applyType;
+	private LocalDateTime applyTime;
+	private Integer status;
+	private Long auditBy;
+	private LocalDateTime auditTime;
+	private String auditRemark;
+	private LocalDateTime createdAt;
+	private LocalDateTime updatedAt;
+	@TableLogic
+	private Integer deleted;
+
+	public DependentCardApply() {
+	}
+
+	public DependentCardApply(
+			String applyNo,
+			Long talentId,
+			Long talentCardId,
+			String talentName,
+			String talentMobile,
+			String talentCardNo,
+			String cardLevel,
+			String dependentName,
+			String dependentMobile,
+			String dependentIdCardNo,
+			String relation
+	) {
+		LocalDateTime now = LocalDateTime.now();
+		this.applyNo = applyNo;
+		this.talentId = talentId;
+		this.talentCardId = talentCardId;
+		this.talentName = talentName;
+		this.talentMobile = talentMobile;
+		this.talentCardNo = talentCardNo;
+		this.cardLevel = cardLevel;
+		this.dependentName = dependentName;
+		this.dependentMobile = dependentMobile;
+		this.dependentIdCardNo = dependentIdCardNo;
+		this.relation = relation;
+		this.applyType = "FAMILY";
+		this.applyTime = now;
+		this.status = STATUS_PENDING_REVIEW;
+		this.createdAt = now;
+		this.updatedAt = now;
+		this.deleted = 0;
+	}
+
+	public Long getId() { return id; }
+	public String getApplyNo() { return applyNo; }
+	public Long getTalentId() { return talentId; }
+	public Long getTalentCardId() { return talentCardId; }
+	public String getTalentName() { return talentName; }
+	public String getTalentMobile() { return talentMobile; }
+	public String getTalentCardNo() { return talentCardNo; }
+	public String getCardLevel() { return cardLevel; }
+	public Integer getIsFamilyCardEnabled() { return isFamilyCardEnabled; }
+	public Integer getFamilyCardLimit() { return familyCardLimit; }
+	public String getDependentName() { return dependentName; }
+	public String getDependentMobile() { return dependentMobile; }
+	public String getDependentIdCardNo() { return dependentIdCardNo; }
+	public String getRelation() { return relation; }
+	public String getProofFiles() { return proofFiles; }
+	public String getApplyType() { return applyType; }
+	public LocalDateTime getApplyTime() { return applyTime; }
+	public Integer getStatus() { return status; }
+	public Long getAuditBy() { return auditBy; }
+	public LocalDateTime getAuditTime() { return auditTime; }
+	public String getAuditRemark() { return auditRemark; }
+	public LocalDateTime getCreatedAt() { return createdAt; }
+	public LocalDateTime getUpdatedAt() { return updatedAt; }
+	public Integer getDeleted() { return deleted; }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApplyStatusLog.java b/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApplyStatusLog.java
new file mode 100644
index 0000000..7e8e9f4
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/DependentCardApplyStatusLog.java
@@ -0,0 +1,54 @@
+package com.talentcard.server.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+@TableName("dependent_card_apply_status_log")
+public class DependentCardApplyStatusLog {
+
+	@TableId(type = IdType.AUTO)
+	private Long id;
+	private Long applyId;
+	private String applyNo;
+	private Integer fromStatus;
+	private Integer toStatus;
+	private String operatorType;
+	private Long operatorId;
+	private String remark;
+	private LocalDateTime createdAt;
+
+	public DependentCardApplyStatusLog() {
+	}
+
+	public DependentCardApplyStatusLog(
+			Long applyId,
+			String applyNo,
+			Integer fromStatus,
+			Integer toStatus,
+			String operatorType,
+			Long operatorId,
+			String remark
+	) {
+		this.applyId = applyId;
+		this.applyNo = applyNo;
+		this.fromStatus = fromStatus;
+		this.toStatus = toStatus;
+		this.operatorType = operatorType;
+		this.operatorId = operatorId;
+		this.remark = remark;
+		this.createdAt = LocalDateTime.now();
+	}
+
+	public Long getId() { return id; }
+	public Long getApplyId() { return applyId; }
+	public String getApplyNo() { return applyNo; }
+	public Integer getFromStatus() { return fromStatus; }
+	public Integer getToStatus() { return toStatus; }
+	public String getOperatorType() { return operatorType; }
+	public Long getOperatorId() { return operatorId; }
+	public String getRemark() { return remark; }
+	public LocalDateTime getCreatedAt() { return createdAt; }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/entity/ServiceApplication.java b/talent-card-server/src/main/java/com/talentcard/server/entity/ServiceApplication.java
new file mode 100644
index 0000000..906cd73
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/entity/ServiceApplication.java
@@ -0,0 +1,70 @@
+package com.talentcard.server.entity;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("service_application")
+public class ServiceApplication {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String applicationNo;
+    private String applicationType;
+    private Long talentId;
+    private Long cardId;
+    private Long merchantId;
+    private String talentName;
+    private String talentMobile;
+    private String talentLevel;
+    private String cardNo;
+    private String merchantName;
+    private String hospitalName;
+    private String contactMobile;
+    private String packageName;
+    private String productName;
+    private String applicantName;
+    private String applicantMobile;
+    private String applicantIdCardNo;
+    private String expectedTime;
+    private LocalDate checkInDate;
+    private LocalDate checkOutDate;
+    private LocalDateTime submittedAt;
+    private String submissionType;
+    private String plateNo;
+    private String parkingLot;
+    private String alipayMobile;
+    private String spouseName;
+    private String spouseMobile;
+    private String spouseIdCardNo;
+    private String spousePoliticalStatus;
+    private String spouseEducation;
+    private String spouseSchoolMajor;
+    private String spouseWorkUnit;
+    private String spouseInInstitution;
+    private String spouseWorkExperience;
+    private Long childId;
+    private String childName;
+    private String childGender;
+    private LocalDate childBirthDate;
+    private String proofFileIds;
+    private Integer guestCount;
+    private String subsidy;
+    private String status;
+    private LocalDateTime handledAt;
+    private String handlerName;
+    private String rejectReason;
+    private String institutionAddressSnapshot;
+    private BigDecimal institutionLongitudeSnapshot;
+    private BigDecimal institutionLatitudeSnapshot;
+    private String estimatedProcessingTime;
+    private String idempotencyKey;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    private Integer deleted;
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponClaimMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponClaimMapper.java
new file mode 100644
index 0000000..26c53de
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponClaimMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitCinemaCouponClaim;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitCinemaCouponClaimMapper extends BaseMapper<BenefitCinemaCouponClaim> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponMapper.java
new file mode 100644
index 0000000..4de34a4
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitCinemaCouponMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitCinemaCoupon;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitCinemaCouponMapper extends BaseMapper<BenefitCinemaCoupon> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFamilyCardApplicationMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFamilyCardApplicationMapper.java
new file mode 100644
index 0000000..4c27dc9
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFamilyCardApplicationMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitFamilyCardApplication;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitFamilyCardApplicationMapper extends BaseMapper<BenefitFamilyCardApplication> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVerificationCodeMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVerificationCodeMapper.java
new file mode 100644
index 0000000..a41b440
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVerificationCodeMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitFreeStayVerificationCode;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitFreeStayVerificationCodeMapper extends BaseMapper<BenefitFreeStayVerificationCode> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVoucherMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVoucherMapper.java
new file mode 100644
index 0000000..3666936
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitFreeStayVoucherMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitFreeStayVoucher;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitFreeStayVoucherMapper extends BaseMapper<BenefitFreeStayVoucher> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantBenefitRuleMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantBenefitRuleMapper.java
new file mode 100644
index 0000000..fe2a690
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantBenefitRuleMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitMerchantBenefitRule;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitMerchantBenefitRuleMapper extends BaseMapper<BenefitMerchantBenefitRule> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantMapper.java
new file mode 100644
index 0000000..422202a
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitMerchant;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitMerchantMapper extends BaseMapper<BenefitMerchant> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantProductMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantProductMapper.java
new file mode 100644
index 0000000..4d8528e
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMerchantProductMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitMerchantProduct;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitMerchantProductMapper extends BaseMapper<BenefitMerchantProduct> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMovieTicketRuleMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMovieTicketRuleMapper.java
new file mode 100644
index 0000000..e42f5a7
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitMovieTicketRuleMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitMovieTicketRule;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitMovieTicketRuleMapper extends BaseMapper<BenefitMovieTicketRule> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderDynamicVerificationCodeMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderDynamicVerificationCodeMapper.java
new file mode 100644
index 0000000..b5d0e8f
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderDynamicVerificationCodeMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitOrderDynamicVerificationCode;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitOrderDynamicVerificationCodeMapper extends BaseMapper<BenefitOrderDynamicVerificationCode> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderMapper.java
new file mode 100644
index 0000000..cccca77
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitOrderMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitOrderMapper extends BaseMapper<BenefitOrder> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitTalentCardMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitTalentCardMapper.java
new file mode 100644
index 0000000..de8d6b7
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitTalentCardMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitTalentCard;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitTalentCardMapper extends BaseMapper<BenefitTalentCard> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitVerifyRecordMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitVerifyRecordMapper.java
new file mode 100644
index 0000000..c69d47d
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/BenefitVerifyRecordMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.BenefitVerifyRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface BenefitVerifyRecordMapper extends BaseMapper<BenefitVerifyRecord> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/ChildInformationMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/ChildInformationMapper.java
new file mode 100644
index 0000000..e6d990c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/ChildInformationMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.ChildInformation;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ChildInformationMapper extends BaseMapper<ChildInformation> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyMapper.java
new file mode 100644
index 0000000..c21a15e
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyMapper.java
@@ -0,0 +1,35 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.DependentCardApply;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface DependentCardApplyMapper extends BaseMapper<DependentCardApply> {
+
+	@Select("""
+			select c.id as talentCardId, c.talent_id as talentId, c.card_no as talentCardNo,
+			       c.card_level as cardLevel, c.is_family_card_enabled as isFamilyCardEnabled,
+			       c.family_card_limit as familyCardLimit, u.name as talentName, u.mobile as talentMobile
+			from talent_card c
+			join talent_user u on u.id = c.talent_id and u.deleted = 0
+			where c.id = #{talentCardId}
+			  and c.deleted = 0
+			  and c.card_type = 'PERSONAL'
+			  and c.status = 1
+			limit 1
+			for update
+			""")
+	DependentCardApply selectOwnerCard(@Param("talentCardId") Long talentCardId);
+
+	@Select("""
+			select *
+			from dependent_card_apply
+			where id = #{id} and deleted = 0
+			limit 1
+			for update
+			""")
+	DependentCardApply selectByIdForUpdate(@Param("id") Long id);
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyStatusLogMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyStatusLogMapper.java
new file mode 100644
index 0000000..2c37e37
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/DependentCardApplyStatusLogMapper.java
@@ -0,0 +1,9 @@
+package com.talentcard.server.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.DependentCardApplyStatusLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DependentCardApplyStatusLogMapper extends BaseMapper<DependentCardApplyStatusLog> {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationMapper.java
new file mode 100644
index 0000000..bf191b6
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationMapper.java
@@ -0,0 +1,120 @@
+package com.talentcard.server.mapper;
+
+import java.util.Map;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.talentcard.server.entity.ServiceApplication;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface ServiceApplicationMapper extends BaseMapper<ServiceApplication> {
+
+    @Select("""
+            select c.id as cardId,
+                   c.talent_id as talentId,
+                   c.card_no as cardNo,
+                   c.card_level as talentLevel,
+                   c.card_type as cardType,
+                   u.name as talentName,
+                   u.mobile as talentMobile,
+                   u.id_card_no as talentIdCardNo
+            from talent_card c
+            join talent_user u on u.id = c.talent_id and u.deleted = 0 and u.status = 1
+            where c.deleted = 0
+              and c.status = 1
+              and (c.valid_start is null or c.valid_start <= current_date)
+              and (c.valid_end is null or c.valid_end >= current_date)
+              and (c.expire_time is null or c.expire_time > now())
+              and ((#{cardId} is not null and c.id = #{cardId}) or (#{cardId} is null and c.talent_id = #{talentId}))
+              and (#{talentId} is null or c.talent_id = #{talentId})
+            order by case when c.card_type = 'PERSONAL' then 0 else 1 end, c.id desc
+            limit 1
+            """)
+    Map<String, Object> selectActiveCardSnapshot(@Param("talentId") Long talentId, @Param("cardId") Long cardId);
+
+    @Select("""
+            select id as merchantId,
+                   merchant_name as merchantName,
+                   merchant_type as merchantType,
+                   contact_mobile as contactMobile,
+                   address,
+                   longitude,
+                   latitude
+            from merchant
+            where id = #{merchantId} and deleted = 0 and status = 1
+            limit 1
+            """)
+    Map<String, Object> selectMerchantSnapshot(@Param("merchantId") Long merchantId);
+
+    @Select("""
+            select m.id as merchantId,
+                   m.merchant_name as merchantName,
+                   m.merchant_type as merchantType,
+                   m.contact_mobile as contactMobile,
+                   m.address,
+                   m.longitude,
+                   m.latitude
+            from merchant m
+            join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0
+            where m.id = #{merchantId}
+              and mapping.category_key = #{categoryKey}
+              and m.deleted = 0
+              and m.status = 1
+            limit 1
+            """)
+    Map<String, Object> selectPremiumMerchantSnapshot(@Param("merchantId") Long merchantId, @Param("categoryKey") String categoryKey);
+
+    @Select("""
+            select p.id as packageId,
+                   p.merchant_id as merchantId,
+                   p.package_name as packageName,
+                   p.stock,
+                   p.status,
+                   p.unavailable_reason as unavailableReason
+            from medical_package p
+            where p.id = #{packageId}
+              and (#{merchantId} is null or p.merchant_id = #{merchantId})
+              and p.deleted = 0
+            limit 1
+            """)
+    Map<String, Object> selectMedicalPackage(@Param("packageId") Long packageId, @Param("merchantId") Long merchantId);
+
+    @Select("""
+            select id as productId,
+                   merchant_id as merchantId,
+                   product_name as productName,
+                   stock,
+                   status
+            from merchant_product
+            where id = #{productId}
+              and (#{merchantId} is null or merchant_id = #{merchantId})
+              and deleted = 0
+            limit 1
+            """)
+    Map<String, Object> selectMerchantProduct(@Param("productId") Long productId, @Param("merchantId") Long merchantId);
+
+    @Select("""
+            select id as schoolId,
+                   school_name as schoolName,
+                   address,
+                   latitude,
+                   longitude,
+                   contact_mobile as contactMobile,
+                   enabled
+            from children_entrustment_school
+            where id = #{schoolId} and deleted = 0
+            limit 1
+            """)
+    Map<String, Object> selectChildrenSchool(@Param("schoolId") Long schoolId);
+
+    @Select("""
+            select *
+            from service_application
+            where id = #{id} and deleted = 0
+            limit 1
+            for update
+            """)
+    ServiceApplication selectByIdForUpdate(@Param("id") Long id);
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationCodeRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationCodeRequest.java
new file mode 100644
index 0000000..9f77351
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationCodeRequest.java
@@ -0,0 +1,29 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "生成权益核销码请求", description = "生成电影票/酒店权益5分钟核销码请求")
+public record BenefitVerificationCodeRequest(
+        @Schema(description = "权益类型:MOVIE_TICKET电影票、HOTEL酒店", example = "MOVIE_TICKET", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotBlank(message = "权益类型不能为空")
+        @Size(max = 32, message = "权益类型长度不能超过32")
+        String benefitType,
+
+        @Schema(description = "人才用户ID;cardId未传时必填,系统自动选择有效人才卡", example = "2")
+        @Positive(message = "人才用户ID必须大于0")
+        Long talentId,
+
+        @Schema(description = "人才卡ID;支持主卡或已审核通过的亲属附属卡", example = "2")
+        @Positive(message = "人才卡ID必须大于0")
+        Long cardId,
+
+        @Schema(description = "商户ID", example = "22", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotNull(message = "商户ID不能为空")
+        @Positive(message = "商户ID必须大于0")
+        Long merchantId
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationRequest.java
new file mode 100644
index 0000000..90eb2ad
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/BenefitVerificationRequest.java
@@ -0,0 +1,23 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "权益核销请求", description = "电影票/酒店权益核销请求")
+public record BenefitVerificationRequest(
+        @Schema(description = "权益类型:MOVIE_TICKET电影票、HOTEL酒店", example = "MOVIE_TICKET", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotBlank
+        @Size(max = 32)
+        String benefitType,
+
+        @Schema(description = "核销码;由小程序生成码接口返回,5分钟内有效", example = "MV2A1B2C3D4E5F6", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotBlank
+        @Size(max = 128)
+        String verifyCode,
+
+        @Schema(description = "核销备注", example = "商户确认用户到店使用")
+        @Size(max = 512)
+        String remark
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardApplyRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardApplyRequest.java
new file mode 100644
index 0000000..f78c041
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardApplyRequest.java
@@ -0,0 +1,36 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "家属附属卡申请提交参数", description = "家属附属卡申请提交参数")
+public record DependentCardApplyRequest(
+		@Schema(description = "主卡ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
+		@NotNull(message = "主卡ID不能为空")
+		Long talentCardId,
+
+		@Schema(description = "家属姓名", example = "张三", maxLength = 64, requiredMode = Schema.RequiredMode.REQUIRED)
+		@NotBlank(message = "家属姓名不能为空")
+		@Size(max = 64, message = "家属姓名不能超过64个字符")
+		String dependentName,
+
+		@Schema(description = "家属身份证号", example = "510105199001011234", maxLength = 32, requiredMode = Schema.RequiredMode.REQUIRED)
+		@NotBlank(message = "家属证件号不能为空")
+		@Size(max = 32, message = "家属证件号不能超过32个字符")
+		@Pattern(regexp = "^(?:[0-9]{15}|[0-9]{17}[0-9Xx])$", message = "家属证件号格式不正确")
+		String dependentIdCardNo,
+
+		@Schema(description = "家属手机号", example = "13800138000", pattern = "^1[3-9][0-9]{9}$", requiredMode = Schema.RequiredMode.REQUIRED)
+		@NotBlank(message = "家属手机号不能为空")
+		@Pattern(regexp = "^1[3-9][0-9]{9}$", message = "家属手机号格式不正确")
+		String dependentMobile,
+
+		@Schema(description = "与主卡持有人的关系", example = "配偶", maxLength = 32, requiredMode = Schema.RequiredMode.REQUIRED)
+		@NotBlank(message = "家属关系不能为空")
+		@Size(max = 32, message = "家属关系不能超过32个字符")
+		String relation
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardAuditRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardAuditRequest.java
new file mode 100644
index 0000000..7e7ee8e
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardAuditRequest.java
@@ -0,0 +1,16 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "家属附属卡审核参数", description = "家属附属卡审核参数")
+public record DependentCardAuditRequest(
+		@NotNull
+		@Schema(description = "审核结果:1通过、2驳回", example = "1", allowableValues = {"1", "2"})
+		Integer status,
+		@Size(max = 500)
+		@Schema(description = "审核备注;驳回时必填", example = "证明材料不完整")
+		String auditRemark
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardUnbindRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardUnbindRequest.java
new file mode 100644
index 0000000..fe5a77d
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/DependentCardUnbindRequest.java
@@ -0,0 +1,14 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+
+@Schema(name = "家属附属卡解绑参数", description = "家属附属卡解绑参数")
+public record DependentCardUnbindRequest(
+		@NotNull
+		@Positive
+		@Schema(description = "主卡ID", example = "1")
+		Long talentCardId
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentStatusUpdateRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentStatusUpdateRequest.java
new file mode 100644
index 0000000..84816dd
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentStatusUpdateRequest.java
@@ -0,0 +1,26 @@
+package com.talentcard.server.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "预约申请处理请求", description = "后台处理预约申请状态")
+public record ServiceAppointmentStatusUpdateRequest(
+        @Schema(description = "处理状态:待处理、处理中、已处理、已驳回、已取消、待审核、审核通过、审核不通过;也支持通过/拒绝别名", example = "已处理", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotBlank(message = "处理状态不能为空")
+        @Size(max = 32, message = "处理状态长度不能超过32")
+        String status,
+
+        @Schema(description = "处理人名称;未传默认管理员", example = "管理员")
+        @Size(max = 64, message = "处理人名称长度不能超过64")
+        String handlerName,
+
+        @Schema(description = "处理/审核备注;拒绝或驳回时必填", example = "资料不完整,请补充后重新提交")
+        @Size(max = 500, message = "处理备注长度不能超过500")
+        String remark,
+
+        @Schema(description = "驳回原因;兼容字段,未传remark时使用", example = "资料不完整")
+        @Size(max = 500, message = "驳回原因长度不能超过500")
+        String rejectReason
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentSubmitRequest.java b/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentSubmitRequest.java
new file mode 100644
index 0000000..2103257
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/request/ServiceAppointmentSubmitRequest.java
@@ -0,0 +1,156 @@
+package com.talentcard.server.request;
+
+import java.util.List;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+
+@Schema(name = "预约申请提交请求", description = "小程序提交体检、医院快捷通道、酒店、配偶就业、学生托管预约申请")
+public record ServiceAppointmentSubmitRequest(
+        @Schema(description = "预约类型:MEDICAL_RESERVATION体检、FAST_TRACK快捷通道、HOTEL_RESERVATION酒店、SPOUSE_EMPLOYMENT配偶就业、CHILD_ENTRUSTMENT学生托管", example = "MEDICAL_RESERVATION", requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotBlank(message = "预约类型不能为空")
+        @Size(max = 64, message = "预约类型长度不能超过64")
+        String applicationType,
+
+        @Schema(description = "人才用户ID;cardId未传时必填", example = "2")
+        @Positive(message = "人才用户ID必须大于0")
+        Long talentId,
+
+        @Schema(description = "人才卡ID;传入后按该卡保存预约快照", example = "8")
+        @Positive(message = "人才卡ID必须大于0")
+        Long cardId,
+
+        @Schema(description = "服务商户/医院/酒店ID;体检、酒店、配偶就业建议传入", example = "12")
+        @Positive(message = "商户ID必须大于0")
+        Long merchantId,
+
+        @Schema(description = "体检套餐ID;体检预约可传,不传则使用packageName", example = "1")
+        @Positive(message = "体检套餐ID必须大于0")
+        Long packageId,
+
+        @Schema(description = "酒店房型/套餐ID;酒店预约可传,不传则使用productName", example = "20")
+        @Positive(message = "酒店房型/套餐ID必须大于0")
+        Long productId,
+
+        @Schema(description = "学生托管学校ID;可选", example = "3")
+        @Positive(message = "学校ID必须大于0")
+        Long schoolId,
+
+        @Schema(description = "已存在的子女信息ID;学生托管可传,未传则按子女字段新建审核资料", example = "5")
+        @Positive(message = "子女信息ID必须大于0")
+        Long childId,
+
+        @Schema(description = "申请人姓名;体检/快捷通道可传,默认使用持卡人姓名", example = "张某某")
+        @Size(max = 64, message = "申请人姓名长度不能超过64")
+        String applicantName,
+
+        @Schema(description = "申请人手机号;体检/快捷通道可传,默认使用持卡人手机号", example = "13800001688")
+        @Size(max = 20, message = "申请人手机号长度不能超过20")
+        String applicantMobile,
+
+        @Schema(description = "申请人证件号;体检/快捷通道可传,默认使用持卡人证件号", example = "510402199001011688")
+        @Size(max = 64, message = "申请人证件号长度不能超过64")
+        String applicantIdCardNo,
+
+        @Schema(description = "医院名称;未传merchantId时必填", example = "攀枝花市中心医院")
+        @Size(max = 128, message = "医院名称长度不能超过128")
+        String hospitalName,
+
+        @Schema(description = "商户/酒店/服务机构名称;未传merchantId时可传", example = "全季酒店")
+        @Size(max = 128, message = "服务机构名称长度不能超过128")
+        String merchantName,
+
+        @Schema(description = "联系电话;未传merchantId时可传", example = "0812-3509201")
+        @Size(max = 20, message = "联系电话长度不能超过20")
+        String contactMobile,
+
+        @Schema(description = "预约套餐/体检套餐名称", example = "A套餐")
+        @Size(max = 128, message = "套餐名称长度不能超过128")
+        String packageName,
+
+        @Schema(description = "预约项目/房型/服务内容", example = "商务大床房")
+        @Size(max = 128, message = "预约项目长度不能超过128")
+        String productName,
+
+        @Schema(description = "期望预约时间;如2026-05-28或2026-05-22 至 2026-05-23", example = "2026-05-28")
+        @Size(max = 128, message = "期望预约时间长度不能超过128")
+        String expectedTime,
+
+        @Schema(description = "期望预约日期;格式yyyy-MM-dd,未传expectedTime时使用", example = "2026-05-28")
+        @Size(max = 32, message = "期望预约日期长度不能超过32")
+        String expectedDate,
+
+        @Schema(description = "入住日期;酒店预约使用,格式yyyy-MM-dd", example = "2026-05-22")
+        @Size(max = 32, message = "入住日期长度不能超过32")
+        String checkInDate,
+
+        @Schema(description = "离店日期;酒店预约使用,格式yyyy-MM-dd", example = "2026-05-23")
+        @Size(max = 32, message = "离店日期长度不能超过32")
+        String checkOutDate,
+
+        @Schema(description = "入住人数;酒店预约可传", example = "2")
+        @Positive(message = "入住人数必须大于0")
+        Integer guestCount,
+
+        @Schema(description = "是否使用补贴/权益;酒店预约可传", example = "是")
+        @Size(max = 16, message = "补贴标识长度不能超过16")
+        String subsidy,
+
+        @Schema(description = "配偶姓名;配偶就业必填", example = "李四")
+        @Size(max = 64, message = "配偶姓名长度不能超过64")
+        String spouseName,
+
+        @Schema(description = "配偶手机号;配偶就业必填", example = "13600006666")
+        @Size(max = 20, message = "配偶手机号长度不能超过20")
+        String spouseMobile,
+
+        @Schema(description = "配偶证件号;配偶就业必填", example = "510402199003033456")
+        @Size(max = 64, message = "配偶证件号长度不能超过64")
+        String spouseIdCardNo,
+
+        @Schema(description = "政治面貌", example = "群众")
+        @Size(max = 64, message = "政治面貌长度不能超过64")
+        String spousePoliticalStatus,
+
+        @Schema(description = "学历", example = "本科")
+        @Size(max = 64, message = "学历长度不能超过64")
+        String spouseEducation,
+
+        @Schema(description = "毕业院校及专业", example = "四川大学计算机专业")
+        @Size(max = 255, message = "毕业院校及专业长度不能超过255")
+        String spouseSchoolMajor,
+
+        @Schema(description = "现工作单位及职务", example = "待就业")
+        @Size(max = 255, message = "现工作单位及职务长度不能超过255")
+        String spouseWorkUnit,
+
+        @Schema(description = "是否为机关事业单位在编人员", example = "否")
+        @Size(max = 16, message = "是否在编长度不能超过16")
+        String spouseInInstitution,
+
+        @Schema(description = "工作经历", example = "具有软件项目实施、客户服务相关经验。")
+        @Size(max = 4000, message = "工作经历长度不能超过4000")
+        String spouseWorkExperience,
+
+        @Schema(description = "子女姓名;学生托管必填", example = "陈娇娇")
+        @Size(max = 64, message = "子女姓名长度不能超过64")
+        String childName,
+
+        @Schema(description = "子女性别;学生托管必填:男、女", example = "男")
+        @Size(max = 8, message = "子女性别长度不能超过8")
+        String childGender,
+
+        @Schema(description = "子女出生年月/出生日期;格式yyyy-MM-dd", example = "2020-05-23")
+        @Size(max = 32, message = "出生日期长度不能超过32")
+        String childBirthDate,
+
+        @Schema(description = "亲属关系证明材料文件ID列表;学生托管最多3个", example = "[\"file-1\",\"file-2\"]")
+        List<String> proofFileIds,
+
+        @Schema(description = "提交备注", example = "请尽快联系")
+        @Size(max = 500, message = "备注长度不能超过500")
+        String remark
+) {
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java
new file mode 100644
index 0000000..bda036c
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java
@@ -0,0 +1,50 @@
+package com.talentcard.server.response;
+
+import java.time.LocalDateTime;
+
+import com.talentcard.server.entity.DependentCardApply;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(name = "DependentCardApplyAdminResponse", description = "管理后台家属附属卡申请信息")
+public record DependentCardApplyAdminResponse(
+		Long id,
+		String applyNo,
+		Long talentId,
+		Long talentCardId,
+		String talentName,
+		String talentMobile,
+		String talentCardNo,
+		String cardLevel,
+		String dependentName,
+		String dependentMobile,
+		String dependentIdCardNo,
+		String relation,
+		Integer status,
+		LocalDateTime applyTime,
+		Long auditBy,
+		LocalDateTime auditTime,
+		String auditRemark
+) {
+
+	public static DependentCardApplyAdminResponse from(DependentCardApply apply) {
+		return new DependentCardApplyAdminResponse(
+				apply.getId(),
+				apply.getApplyNo(),
+				apply.getTalentId(),
+				apply.getTalentCardId(),
+				apply.getTalentName(),
+				apply.getTalentMobile(),
+				apply.getTalentCardNo(),
+				apply.getCardLevel(),
+				apply.getDependentName(),
+				apply.getDependentMobile(),
+				apply.getDependentIdCardNo(),
+				apply.getRelation(),
+				apply.getStatus(),
+				apply.getApplyTime(),
+				apply.getAuditBy(),
+				apply.getAuditTime(),
+				apply.getAuditRemark()
+		);
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyItemResponse.java b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyItemResponse.java
new file mode 100644
index 0000000..6940495
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyItemResponse.java
@@ -0,0 +1,52 @@
+package com.talentcard.server.response;
+
+import com.talentcard.server.entity.DependentCardApply;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(name = "DependentCardApplyItemResponse", description = "小程序家属附属卡申请信息")
+public record DependentCardApplyItemResponse(
+		@Schema(description = "数据ID", example = "1001")
+		Long id,
+		@Schema(description = "亲属姓名", example = "张三")
+		String dependentName,
+		@Schema(description = "脱敏手机号", example = "138****8000")
+		String dependentMobile,
+		@Schema(description = "审核状态:0待审核、1已通过、2已驳回、3已解绑", example = "1")
+		Integer status,
+		@Schema(description = "审核状态文本", example = "已通过")
+		String statusText,
+		@Schema(description = "亲属关系", example = "配偶")
+		String relation
+) {
+
+	public static DependentCardApplyItemResponse from(DependentCardApply apply) {
+		return new DependentCardApplyItemResponse(
+				apply.getId(),
+				apply.getDependentName(),
+				maskMobile(apply.getDependentMobile()),
+				apply.getStatus(),
+				statusText(apply.getStatus()),
+				apply.getRelation()
+		);
+	}
+
+	private static String maskMobile(String mobile) {
+		if (mobile == null || mobile.length() < 7) {
+			return "";
+		}
+		return mobile.substring(0, 3) + "****" + mobile.substring(7);
+	}
+
+	private static String statusText(Integer status) {
+		if (status == null) {
+			return "";
+		}
+		return switch (status) {
+			case DependentCardApply.STATUS_PENDING_REVIEW -> "待审核";
+			case DependentCardApply.STATUS_APPROVED -> "已通过";
+			case DependentCardApply.STATUS_REJECTED -> "已驳回";
+			case DependentCardApply.STATUS_CANCELLED -> "已解绑";
+			default -> "未知";
+		};
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyResponse.java b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyResponse.java
new file mode 100644
index 0000000..0d91d94
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyResponse.java
@@ -0,0 +1,46 @@
+package com.talentcard.server.response;
+
+import java.time.LocalDateTime;
+
+import com.talentcard.server.entity.DependentCardApply;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(name = "DependentCardApplyResponse", description = "家属附属卡申请提交结果")
+public record DependentCardApplyResponse(
+		@Schema(description = "申请记录ID", example = "1001")
+		Long id,
+		@Schema(description = "申请编号", example = "DCA202608031100001234")
+		String applyNo,
+		@Schema(description = "主卡持有人ID", example = "2")
+		Long talentId,
+		@Schema(description = "主卡ID", example = "1")
+		Long talentCardId,
+		@Schema(description = "家属姓名", example = "张三")
+		String dependentName,
+		@Schema(description = "家属身份证号", example = "510105199001011234")
+		String dependentIdCardNo,
+		@Schema(description = "家属手机号", example = "13800138000")
+		String dependentMobile,
+		@Schema(description = "家属关系", example = "配偶")
+		String relation,
+		@Schema(description = "审核状态:0待审核、1通过、2驳回、3取消", example = "0", allowableValues = {"0", "1", "2", "3"})
+		Integer status,
+		@Schema(description = "申请时间", example = "2026-08-03T11:00:00")
+		LocalDateTime applyTime
+) {
+
+	public static DependentCardApplyResponse from(DependentCardApply apply) {
+		return new DependentCardApplyResponse(
+				apply.getId(),
+				apply.getApplyNo(),
+				apply.getTalentId(),
+				apply.getTalentCardId(),
+				apply.getDependentName(),
+				apply.getDependentIdCardNo(),
+				apply.getDependentMobile(),
+				apply.getRelation(),
+				apply.getStatus(),
+				apply.getApplyTime()
+		);
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyStatusLogResponse.java b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyStatusLogResponse.java
new file mode 100644
index 0000000..1a83610
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyStatusLogResponse.java
@@ -0,0 +1,34 @@
+package com.talentcard.server.response;
+
+import java.time.LocalDateTime;
+
+import com.talentcard.server.entity.DependentCardApplyStatusLog;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(name = "DependentCardApplyStatusLogResponse", description = "家属附属卡申请状态变更记录")
+public record DependentCardApplyStatusLogResponse(
+		Long id,
+		Long applyId,
+		String applyNo,
+		Integer fromStatus,
+		Integer toStatus,
+		String operatorType,
+		Long operatorId,
+		String remark,
+		LocalDateTime createdAt
+) {
+
+	public static DependentCardApplyStatusLogResponse from(DependentCardApplyStatusLog log) {
+		return new DependentCardApplyStatusLogResponse(
+				log.getId(),
+				log.getApplyId(),
+				log.getApplyNo(),
+				log.getFromStatus(),
+				log.getToStatus(),
+				log.getOperatorType(),
+				log.getOperatorId(),
+				log.getRemark(),
+				log.getCreatedAt()
+		);
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java
new file mode 100644
index 0000000..fa8f4b3
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java
@@ -0,0 +1,459 @@
+package com.talentcard.server.service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.TemporalAdjusters;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import com.talentcard.server.entity.BenefitCinemaCouponClaim;
+import com.talentcard.server.entity.BenefitFamilyCardApplication;
+import com.talentcard.server.entity.BenefitFreeStayVoucher;
+import com.talentcard.server.entity.BenefitMerchantBenefitRule;
+import com.talentcard.server.entity.BenefitMovieTicketRule;
+import com.talentcard.server.entity.BenefitOrder;
+import com.talentcard.server.entity.BenefitTalentCard;
+import com.talentcard.server.entity.BenefitVerifyRecord;
+import com.talentcard.server.mapper.BenefitCinemaCouponClaimMapper;
+import com.talentcard.server.mapper.BenefitFamilyCardApplicationMapper;
+import com.talentcard.server.mapper.BenefitFreeStayVoucherMapper;
+import com.talentcard.server.mapper.BenefitMerchantBenefitRuleMapper;
+import com.talentcard.server.mapper.BenefitMovieTicketRuleMapper;
+import com.talentcard.server.mapper.BenefitOrderMapper;
+import com.talentcard.server.mapper.BenefitTalentCardMapper;
+import com.talentcard.server.mapper.BenefitVerifyRecordMapper;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BenefitQuotaService {
+    public static final String MOVIE_TICKET = "MOVIE_TICKET";
+    public static final String HOTEL = "HOTEL";
+
+    private static final int DEFAULT_HOTEL_TOTAL = 5;
+    private static final List<String> HOTEL_VERIFY_TYPES = List.of(
+            "HOTEL", "HOTEL_FREE", "FREE_STAY", "HOTEL_ORDER", "HOTEL_RESERVATION"
+    );
+
+    private final BenefitTalentCardMapper cardMapper;
+    private final BenefitFamilyCardApplicationMapper familyMapper;
+    private final BenefitMovieTicketRuleMapper movieRuleMapper;
+    private final BenefitCinemaCouponClaimMapper claimMapper;
+    private final BenefitFreeStayVoucherMapper voucherMapper;
+    private final BenefitMerchantBenefitRuleMapper merchantBenefitRuleMapper;
+    private final BenefitVerifyRecordMapper verifyRecordMapper;
+    private final BenefitOrderMapper orderMapper;
+
+    public BenefitQuotaService(
+            BenefitTalentCardMapper cardMapper,
+            BenefitFamilyCardApplicationMapper familyMapper,
+            BenefitMovieTicketRuleMapper movieRuleMapper,
+            BenefitCinemaCouponClaimMapper claimMapper,
+            BenefitFreeStayVoucherMapper voucherMapper,
+            BenefitMerchantBenefitRuleMapper merchantBenefitRuleMapper,
+            BenefitVerifyRecordMapper verifyRecordMapper,
+            BenefitOrderMapper orderMapper
+    ) {
+        this.cardMapper = cardMapper;
+        this.familyMapper = familyMapper;
+        this.movieRuleMapper = movieRuleMapper;
+        this.claimMapper = claimMapper;
+        this.voucherMapper = voucherMapper;
+        this.merchantBenefitRuleMapper = merchantBenefitRuleMapper;
+        this.verifyRecordMapper = verifyRecordMapper;
+        this.orderMapper = orderMapper;
+    }
+
+    public QuotaSnapshot requireAvailable(String rawBenefitType, Long cardId, Long talentId) {
+        return requireAvailable(rawBenefitType, cardId, talentId, null, null, 1);
+    }
+
+    public QuotaSnapshot requireAvailable(String rawBenefitType, Long cardId, Long talentId,
+                                          Long merchantId, Long productId, int quantity) {
+        if (quantity < 1) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "核销数量必须大于0");
+        }
+        QuotaSnapshot snapshot = current(rawBenefitType, cardId, talentId, merchantId, productId);
+        if (snapshot.remaining() < quantity) {
+            throw new BusinessException(ErrorCode.CONFLICT, "主卡权益剩余次数不足");
+        }
+        return snapshot;
+    }
+
+    public QuotaSnapshot current(String rawBenefitType, Long cardId, Long talentId) {
+        return current(rawBenefitType, cardId, talentId, null, null);
+    }
+
+    public QuotaSnapshot current(String rawBenefitType, Long cardId, Long talentId, Long merchantId, Long productId) {
+        String benefitType = normalizeBenefitType(rawBenefitType);
+        PrimaryCard primary = lockPrimaryCard(cardId, talentId);
+        QuotaPolicy policy = policy(benefitType, primary, merchantId, productId);
+        int used = usedQuota(benefitType, primary.sharedCardIds(), policy.windowStart());
+        return new QuotaSnapshot(
+                benefitType,
+                primary.sourceTalentId(),
+                primary.sourceCardId(),
+                primary.sourceCardNo(),
+                primary.sourceCardType(),
+                primary.primaryTalentId(),
+                primary.primaryCardId(),
+                primary.primaryCardNo(),
+                primary.cardLevel(),
+                policy.total(),
+                used,
+                Math.max(policy.total() - used, 0),
+                policy.resetCycle(),
+                policy.windowStart(),
+                primary.familyCard(),
+                primary.sharedCardIds()
+        );
+    }
+
+    public String normalizeBenefitType(String value) {
+        String type = value == null ? "" : value.trim().toUpperCase(Locale.ROOT).replace('-', '_');
+        return switch (type) {
+            case "MOVIE", "MOVIE_TICKET", "CINEMA", "CINEMA_TICKET" -> MOVIE_TICKET;
+            case "HOTEL", "FREE_STAY", "HOTEL_RESERVATION", "STAY" -> HOTEL;
+            default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "权益类型仅支持MOVIE_TICKET电影票或HOTEL酒店");
+        };
+    }
+
+    public List<String> cardLevelAliases(String cardLevel) {
+        Set<String> aliases = new LinkedHashSet<>();
+        String raw = cardLevel == null ? "" : cardLevel.trim();
+        if (!raw.isBlank()) {
+            aliases.add(raw);
+        }
+        String upper = raw.toUpperCase(Locale.ROOT);
+        if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) {
+            aliases.add("EXCELLENT");
+            aliases.add("\u5353\u8d8a\u4eba\u624d\u5361");
+            aliases.add("\u5353\u8d8a");
+        }
+        if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) {
+            aliases.add("OUTSTANDING");
+            aliases.add("\u6770\u51fa\u4eba\u624d\u5361");
+            aliases.add("\u6770\u51fa");
+        }
+        if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1")) {
+            aliases.add("ELITE");
+            aliases.add("\u83c1\u82f1\u4eba\u624d\u5361");
+            aliases.add("\u83c1\u82f1");
+        }
+        return aliases.isEmpty() ? List.of(raw) : List.copyOf(aliases);
+    }
+
+    private PrimaryCard lockPrimaryCard(Long cardId, Long talentId) {
+        if (cardId == null && talentId == null) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡ID或人才用户ID不能为空");
+        }
+        BenefitTalentCard sourceCard = cardId == null ? activeCardForTalent(talentId) : card(cardId);
+        Long sourceCardId = sourceCard.getId();
+        Long sourceTalentId = sourceCard.getTalentId();
+        String sourceCardType = text(sourceCard.getCardType()).toUpperCase(Locale.ROOT);
+        if ("PERSONAL".equals(sourceCardType) || sourceCardType.isBlank()) {
+            return primaryCard(sourceCardId, sourceTalentId, sourceCard.getCardNo(), sourceCardType, sourceCardId, sourceTalentId);
+        }
+
+        BenefitFamilyCardApplication owner = familyMapper.selectOne(new QueryWrapper<BenefitFamilyCardApplication>()
+                .eq("deleted", 0)
+                .eq("family_card_id", sourceCardId)
+                .isNotNull("talent_id")
+                .orderByDesc("id")
+                .last("limit 1"));
+        if (owner == null) {
+            throw new BusinessException(ErrorCode.CONFLICT, "亲属附属卡未找到绑定的主卡");
+        }
+        BenefitTalentCard primary = activePersonalCard(owner.getTalentId());
+        return primaryCard(sourceCardId, sourceTalentId, sourceCard.getCardNo(), sourceCardType,
+                primary.getId(), primary.getTalentId());
+    }
+
+    private PrimaryCard primaryCard(Long sourceCardId, Long sourceTalentId, String sourceCardNo, String sourceCardType,
+                                    Long primaryCardId, Long primaryTalentId) {
+        BenefitTalentCard primary = cardMapper.selectOne(new QueryWrapper<BenefitTalentCard>()
+                .eq("id", primaryCardId)
+                .eq("talent_id", primaryTalentId)
+                .eq("deleted", 0)
+                .last("limit 1 for update"));
+        if (primary == null) {
+            throw new BusinessException(ErrorCode.CONFLICT, "主卡不存在");
+        }
+        if (intValue(primary.getStatus()) != 1 || notStarted(primary.getValidStart())
+                || expiredDate(primary.getValidEnd()) || expiredDateTime(primary.getExpireTime())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "主卡当前不可用");
+        }
+        return new PrimaryCard(
+                sourceTalentId,
+                sourceCardId,
+                sourceCardNo,
+                sourceCardType,
+                primaryTalentId,
+                primaryCardId,
+                primary.getCardNo(),
+                text(primary.getCardLevel()),
+                !"PERSONAL".equals(sourceCardType),
+                sharedCardIds(primaryCardId, primaryTalentId)
+        );
+    }
+
+    private List<Long> sharedCardIds(Long primaryCardId, Long primaryTalentId) {
+        Set<Long> ids = new LinkedHashSet<>();
+        ids.add(primaryCardId);
+        List<BenefitFamilyCardApplication> applications = familyMapper.selectList(new QueryWrapper<BenefitFamilyCardApplication>()
+                .eq("deleted", 0)
+                .eq("talent_id", primaryTalentId)
+                .isNotNull("family_card_id"));
+        for (BenefitFamilyCardApplication application : applications) {
+            if (application.getFamilyCardId() != null) {
+                ids.add(application.getFamilyCardId());
+            }
+        }
+        if (ids.size() == 1) {
+            return List.copyOf(ids);
+        }
+        List<BenefitTalentCard> activeCards = cardMapper.selectList(new QueryWrapper<BenefitTalentCard>()
+                .select("id")
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .in("id", ids));
+        Set<Long> activeIds = new LinkedHashSet<>();
+        activeIds.add(primaryCardId);
+        for (BenefitTalentCard card : activeCards) {
+            activeIds.add(card.getId());
+        }
+        return List.copyOf(activeIds);
+    }
+
+    private BenefitTalentCard card(Long cardId) {
+        BenefitTalentCard card = cardMapper.selectOne(new QueryWrapper<BenefitTalentCard>()
+                .eq("id", cardId)
+                .eq("deleted", 0)
+                .last("limit 1"));
+        if (card == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "人才卡不存在");
+        }
+        if (intValue(card.getStatus()) != 1 || notStarted(card.getValidStart())
+                || expiredDate(card.getValidEnd()) || expiredDateTime(card.getExpireTime())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "人才卡当前不可用");
+        }
+        return card;
+    }
+
+    private BenefitTalentCard activeCardForTalent(Long talentId) {
+        BenefitTalentCard card = cardMapper.selectOne(new QueryWrapper<BenefitTalentCard>()
+                .eq("talent_id", talentId)
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .and(wrapper -> wrapper.isNull("valid_start").or().le("valid_start", LocalDate.now()))
+                .and(wrapper -> wrapper.isNull("valid_end").or().ge("valid_end", LocalDate.now()))
+                .and(wrapper -> wrapper.isNull("expire_time").or().gt("expire_time", LocalDateTime.now()))
+                .last("order by case when card_type = 'PERSONAL' then 0 else 1 end, id desc limit 1"));
+        if (card == null) {
+            throw new BusinessException(ErrorCode.CONFLICT, "未找到有效人才卡");
+        }
+        return card;
+    }
+
+    private BenefitTalentCard activePersonalCard(Long talentId) {
+        BenefitTalentCard card = cardMapper.selectOne(new QueryWrapper<BenefitTalentCard>()
+                .eq("talent_id", talentId)
+                .eq("card_type", "PERSONAL")
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .and(wrapper -> wrapper.isNull("valid_start").or().le("valid_start", LocalDate.now()))
+                .and(wrapper -> wrapper.isNull("valid_end").or().ge("valid_end", LocalDate.now()))
+                .and(wrapper -> wrapper.isNull("expire_time").or().gt("expire_time", LocalDateTime.now()))
+                .orderByDesc("id")
+                .last("limit 1"));
+        if (card == null) {
+            throw new BusinessException(ErrorCode.CONFLICT, "未找到有效主卡");
+        }
+        return card;
+    }
+
+    private QuotaPolicy policy(String benefitType, PrimaryCard primary, Long merchantId, Long productId) {
+        if (MOVIE_TICKET.equals(benefitType)) {
+            return moviePolicy(primary);
+        }
+        return hotelPolicy(primary, merchantId, productId);
+    }
+
+    private QuotaPolicy moviePolicy(PrimaryCard primary) {
+        List<String> aliases = cardLevelAliases(primary.cardLevel());
+        BenefitMovieTicketRule rule = movieRuleMapper.selectOne(new QueryWrapper<BenefitMovieTicketRule>()
+                .in("talent_level", aliases)
+                .last("limit 1"));
+        if (rule == null || intValue(rule.getEnabled()) != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型未启用电影票权益");
+        }
+        int total = intValue(rule.getClaimableCount());
+        if (total < 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型未配置电影票权益次数");
+        }
+        String resetCycle = text(rule.getResetCycle());
+        return new QuotaPolicy(total, resetCycle, windowStart(resetCycle, true));
+    }
+
+    private QuotaPolicy hotelPolicy(PrimaryCard primary, Long merchantId, Long productId) {
+        if (merchantId != null) {
+            List<String> aliases = cardLevelAliases(primary.cardLevel());
+            QueryWrapper<BenefitMerchantBenefitRule> query = new QueryWrapper<BenefitMerchantBenefitRule>()
+                    .eq("deleted", 0)
+                    .eq("status", 1)
+                    .eq("merchant_id", merchantId)
+                    .in("card_level", aliases)
+                    .isNotNull("free_count")
+                    .gt("free_count", 0);
+            if (productId == null) {
+                query.isNull("product_id").orderByDesc("id").last("limit 1");
+            } else {
+                query.and(wrapper -> wrapper.eq("product_id", productId).or().isNull("product_id"))
+                        .last("order by case when product_id = " + productId + " then 0 else 1 end, id desc limit 1");
+            }
+            BenefitMerchantBenefitRule rule = merchantBenefitRuleMapper.selectOne(query);
+            if (rule != null) {
+                if (primary.familyCard() && intValue(rule.getCanFamilyUse()) != 1) {
+                    throw new BusinessException(ErrorCode.CONFLICT, "亲属附属卡不可使用该酒店权益");
+                }
+                String resetCycle = text(rule.getResetCycle());
+                return new QuotaPolicy(intValue(rule.getFreeCount()), resetCycle, windowStart(resetCycle, false));
+            }
+        }
+
+        QueryWrapper<BenefitFreeStayVoucher> voucherQuery = new QueryWrapper<BenefitFreeStayVoucher>()
+                .select("total_count")
+                .eq("talent_id", primary.primaryTalentId())
+                .eq("status", 1)
+                .ge("valid_end", LocalDate.now())
+                .orderByDesc("total_count")
+                .last("limit 1");
+        if (merchantId != null) {
+            voucherQuery.eq("merchant_id", merchantId);
+        }
+        BenefitFreeStayVoucher voucher = voucherMapper.selectOne(voucherQuery);
+        int configured = voucher == null || intValue(voucher.getTotalCount()) < 1 ? DEFAULT_HOTEL_TOTAL : voucher.getTotalCount();
+        return new QuotaPolicy(configured, "YEARLY", windowStart("YEARLY", false));
+    }
+
+    private int usedQuota(String benefitType, List<Long> sharedCardIds, LocalDateTime windowStart) {
+        if (MOVIE_TICKET.equals(benefitType)) {
+            return usedMovieQuota(sharedCardIds, windowStart);
+        }
+        return usedHotelQuota(sharedCardIds, windowStart);
+    }
+
+    private int usedMovieQuota(List<Long> sharedCardIds, LocalDateTime windowStart) {
+        Long used = claimMapper.selectCount(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .in("card_id", sharedCardIds)
+                .apply("((upper(coalesce(code_status, 'ACTIVE')) = 'USED' and used_at >= {0}) or "
+                        + "(upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE' and updated_at >= {1} "
+                        + "and updated_at > date_sub(now(), interval 300 second)))", windowStart, windowStart));
+        return intValue(used);
+    }
+
+    private int usedHotelQuota(List<Long> sharedCardIds, LocalDateTime windowStart) {
+        Long records = verifyRecordMapper.selectCount(new QueryWrapper<BenefitVerifyRecord>()
+                .in("card_id", sharedCardIds)
+                .eq("status", 1)
+                .ge("verify_time", windowStart)
+                .and(wrapper -> wrapper.in("verify_type", HOTEL_VERIFY_TYPES)
+                        .or()
+                        .apply("exists (select 1 from benefit_order o "
+                                + "join merchant m on m.id = o.merchant_id and m.deleted = 0 and m.merchant_type = 'hotel' "
+                                + "where o.id = verify_record.order_id and o.deleted = 0)")));
+
+        Long ordersWithoutRecord = orderMapper.selectCount(new QueryWrapper<BenefitOrder>()
+                .eq("deleted", 0)
+                .in("card_id", sharedCardIds)
+                .eq("verify_status", 1)
+                .ge("updated_at", windowStart)
+                .apply("exists (select 1 from merchant m where m.id = benefit_order.merchant_id "
+                        + "and m.deleted = 0 and m.merchant_type = 'hotel')")
+                .apply("not exists (select 1 from verify_record r where r.order_id = benefit_order.id and r.status = 1)"));
+        return intValue(records) + intValue(ordersWithoutRecord);
+    }
+
+    private LocalDateTime windowStart(String resetCycle, boolean monthlyDefault) {
+        String cycle = resetCycle == null ? "" : resetCycle.trim().toUpperCase(Locale.ROOT);
+        LocalDate today = LocalDate.now();
+        if (cycle.isBlank() || "NONE".equals(cycle)) {
+            return LocalDate.of(1970, 1, 1).atStartOfDay();
+        }
+        if (cycle.contains("DAY") || cycle.contains("DAILY") || cycle.contains("\u5929") || cycle.contains("\u65e5")) {
+            return today.atStartOfDay();
+        }
+        if (cycle.contains("WEEK") || cycle.contains("\u5468") || cycle.contains("\u661f\u671f")) {
+            return today.minusDays(today.getDayOfWeek().getValue() - 1L).atStartOfDay();
+        }
+        if (cycle.contains("YEAR") || cycle.contains("ANNUAL") || cycle.contains("\u5e74")) {
+            return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
+        }
+        if (cycle.contains("MONTH") || cycle.contains("\u6708") || monthlyDefault) {
+            return today.withDayOfMonth(1).atStartOfDay();
+        }
+        return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
+    }
+
+    private boolean notStarted(LocalDate value) {
+        return value != null && value.isAfter(LocalDate.now());
+    }
+
+    private boolean expiredDate(LocalDate value) {
+        return value != null && value.isBefore(LocalDate.now());
+    }
+
+    private boolean expiredDateTime(LocalDateTime value) {
+        return value != null && !value.isAfter(LocalDateTime.now());
+    }
+
+    private String text(Object value) {
+        return value == null ? "" : String.valueOf(value).trim();
+    }
+
+    private int intValue(Object value) {
+        return value instanceof Number number ? number.intValue() : 0;
+    }
+
+    private record PrimaryCard(
+            Long sourceTalentId,
+            Long sourceCardId,
+            String sourceCardNo,
+            String sourceCardType,
+            Long primaryTalentId,
+            Long primaryCardId,
+            String primaryCardNo,
+            String cardLevel,
+            boolean familyCard,
+            List<Long> sharedCardIds
+    ) {
+    }
+
+    private record QuotaPolicy(int total, String resetCycle, LocalDateTime windowStart) {
+    }
+
+    public record QuotaSnapshot(
+            String benefitType,
+            Long talentId,
+            Long cardId,
+            String cardNo,
+            String cardType,
+            Long primaryTalentId,
+            Long primaryCardId,
+            String primaryCardNo,
+            String cardLevel,
+            int total,
+            int used,
+            int remaining,
+            String resetCycle,
+            LocalDateTime windowStart,
+            boolean familyCard,
+            List<Long> sharedCardIds
+    ) {
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
new file mode 100644
index 0000000..a229c44
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
@@ -0,0 +1,457 @@
+package com.talentcard.server.service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.talentcard.server.business.BusinessIdGenerator;
+import com.talentcard.server.request.BenefitVerificationCodeRequest;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import com.talentcard.server.entity.BenefitCinemaCoupon;
+import com.talentcard.server.entity.BenefitCinemaCouponClaim;
+import com.talentcard.server.entity.BenefitFreeStayVerificationCode;
+import com.talentcard.server.entity.BenefitFreeStayVoucher;
+import com.talentcard.server.entity.BenefitMerchant;
+import com.talentcard.server.mapper.BenefitCinemaCouponClaimMapper;
+import com.talentcard.server.mapper.BenefitCinemaCouponMapper;
+import com.talentcard.server.mapper.BenefitFreeStayVerificationCodeMapper;
+import com.talentcard.server.mapper.BenefitFreeStayVoucherMapper;
+import com.talentcard.server.mapper.BenefitMerchantMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class BenefitVerificationCodeService {
+    public static final int CODE_SECONDS = 300;
+    public static final String MOVIE_TOKEN_PREFIX = "MV2";
+    public static final String HOTEL_TOKEN_PREFIX = "HV2";
+
+    private final BenefitQuotaService quotaService;
+    private final BenefitMerchantMapper merchantMapper;
+    private final BenefitCinemaCouponMapper couponMapper;
+    private final BenefitCinemaCouponClaimMapper claimMapper;
+    private final BenefitFreeStayVoucherMapper voucherMapper;
+    private final BenefitFreeStayVerificationCodeMapper freeStayCodeMapper;
+
+    public BenefitVerificationCodeService(
+            BenefitQuotaService quotaService,
+            BenefitMerchantMapper merchantMapper,
+            BenefitCinemaCouponMapper couponMapper,
+            BenefitCinemaCouponClaimMapper claimMapper,
+            BenefitFreeStayVoucherMapper voucherMapper,
+            BenefitFreeStayVerificationCodeMapper freeStayCodeMapper
+    ) {
+        this.quotaService = quotaService;
+        this.merchantMapper = merchantMapper;
+        this.couponMapper = couponMapper;
+        this.claimMapper = claimMapper;
+        this.voucherMapper = voucherMapper;
+        this.freeStayCodeMapper = freeStayCodeMapper;
+    }
+
+    @Transactional
+    public Map<String, Object> generate(BenefitVerificationCodeRequest request, String idempotencyKey) {
+        String benefitType = quotaService.normalizeBenefitType(request.benefitType());
+        if (BenefitQuotaService.MOVIE_TICKET.equals(benefitType)) {
+            return generateMovieCode(request, idempotencyKey);
+        }
+        return generateHotelFreeStayCode(request);
+    }
+
+    private Map<String, Object> generateMovieCode(BenefitVerificationCodeRequest request, String idempotencyKey) {
+        Long merchantId = requirePositive(request.merchantId(), "商户ID不能为空");
+        expireExpiredMovieCodes(merchantId);
+
+        BenefitQuotaService.QuotaSnapshot quota = quotaService.requireAvailable(
+                BenefitQuotaService.MOVIE_TICKET, request.cardId(), request.talentId(), merchantId, null, 1);
+        MovieCouponChoice choice = movieCouponForUpdate(merchantId, quota.cardLevel(), quota.sharedCardIds());
+        BenefitCinemaCoupon coupon = choice.coupon();
+        Long couponId = coupon.getId();
+        int dailyLimit = choice.dailyLimit();
+        int dailyUsed = choice.dailyUsed();
+
+        String token = MOVIE_TOKEN_PREFIX + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
+        String claimKey = claimIdempotencyKey(idempotencyKey, token);
+        BenefitCinemaCouponClaim idempotent = movieClaimByIdempotencyKey(claimKey);
+        if (idempotent != null) {
+            ensureSameMovieClaim(idempotent, couponId, merchantId, quota.cardId());
+            if (activeMovieCode(idempotent) && startsWithMoviePrefix(idempotent.getExchangeCode())) {
+                return movieCodeResponse(idempotent, coupon, quota, dailyLimit, dailyUsed, false);
+            }
+            throw new BusinessException(ErrorCode.CONFLICT, "幂等键对应的电影票核销码已过期或已使用");
+        }
+
+        BenefitCinemaCouponClaim existing = movieClaimForUpdate(couponId, quota.talentId());
+        Long claimId;
+        if (existing != null) {
+            String status = upper(existing.getCodeStatus());
+            if ("USED".equals(status)) {
+                throw new BusinessException(ErrorCode.CONFLICT, "电影票已核销");
+            }
+            if (activeMovieCode(existing) && startsWithMoviePrefix(existing.getExchangeCode())) {
+                return movieCodeResponse(existing, coupon, quota, dailyLimit, dailyUsed, false);
+            }
+            boolean stockAlreadyReserved = "ACTIVE".equals(status) && !startsWithMoviePrefix(existing.getExchangeCode());
+            if (!stockAlreadyReserved) {
+                decrementMovieStock(couponId, merchantId);
+            }
+            BenefitCinemaCouponClaim update = new BenefitCinemaCouponClaim();
+            update.setId(existing.getId());
+            update.setExchangeCode(token);
+            update.setCodeStatus("ACTIVE");
+            update.setStatus("RECEIVED");
+            update.setUsedAt(null);
+            update.setValidEnd(coupon.getValidEnd());
+            update.setIdempotencyKey(claimKey);
+            claimMapper.updateById(update);
+            claimId = existing.getId();
+        } else {
+            decrementMovieStock(couponId, merchantId);
+            BenefitCinemaCouponClaim claim = new BenefitCinemaCouponClaim();
+            claim.setClaimNo(BusinessIdGenerator.next("MC"));
+            claim.setCouponId(couponId);
+            claim.setMerchantId(merchantId);
+            claim.setTalentId(quota.talentId());
+            claim.setCardId(quota.cardId());
+            claim.setCardNo(quota.cardNo());
+            claim.setValidEnd(coupon.getValidEnd());
+            claim.setStatus("RECEIVED");
+            claim.setExchangeCode(token);
+            claim.setCodeStatus("ACTIVE");
+            claim.setIdempotencyKey(claimKey);
+            claim.setDeleted(0);
+            claimMapper.insert(claim);
+            claimId = claim.getId();
+        }
+        BenefitCinemaCouponClaim claim = movieClaimById(claimId);
+        return movieCodeResponse(claim, couponMapper.selectById(couponId), quota, dailyLimit, dailyUsed + 1, true);
+    }
+
+    private Map<String, Object> generateHotelFreeStayCode(BenefitVerificationCodeRequest request) {
+        Long merchantId = requirePositive(request.merchantId(), "商户ID不能为空");
+        ensureHotelMerchant(merchantId);
+        BenefitQuotaService.QuotaSnapshot quota = quotaService.requireAvailable(
+                BenefitQuotaService.HOTEL, request.cardId(), request.talentId(), merchantId, null, 1);
+        BenefitFreeStayVoucher voucher = voucherForUpdate(quota.primaryTalentId(), merchantId, quota.primaryCardId(), quota.total());
+        if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店权益券当前不可用");
+        }
+        if (intValue(voucher.getUsedCount()) >= Math.min(intValue(voucher.getTotalCount()), quota.total())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店权益次数已用完");
+        }
+        freeStayCodeMapper.update(null, new UpdateWrapper<BenefitFreeStayVerificationCode>()
+                .set("status", 2)
+                .eq("voucher_id", voucher.getId())
+                .eq("status", 0));
+
+        BenefitFreeStayVerificationCode code = new BenefitFreeStayVerificationCode();
+        code.setCodeToken(hotelFreeStayToken(quota.cardId()));
+        code.setVoucherId(voucher.getId());
+        code.setExpiresAt(LocalDateTime.now().plusSeconds(CODE_SECONDS));
+        code.setStatus(0);
+        freeStayCodeMapper.insert(code);
+        return hotelCodeResponse(code, voucher, quota);
+    }
+
+    private MovieCouponChoice movieCouponForUpdate(Long merchantId, String cardLevel, List<Long> sharedCardIds) {
+        ensureCinemaMerchant(merchantId);
+        List<BenefitCinemaCoupon> coupons = couponMapper.selectList(new QueryWrapper<BenefitCinemaCoupon>()
+                .eq("merchant_id", merchantId)
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .gt("stock", 0)
+                .and(wrapper -> wrapper.isNull("valid_end").or().ge("valid_end", LocalDate.now()))
+                .orderByAsc("sort_no", "id")
+                .last("for update"));
+        if (coupons.isEmpty()) {
+            throw new BusinessException(ErrorCode.CONFLICT, "未找到可用电影票权益券");
+        }
+        List<String> aliases = quotaService.cardLevelAliases(cardLevel);
+        boolean cardLevelSupported = false;
+        for (BenefitCinemaCoupon coupon : coupons) {
+            if (!supportsCard(coupon.getTalentLevels(), aliases)) {
+                continue;
+            }
+            cardLevelSupported = true;
+            int dailyLimit = Math.max(1, intValue(coupon.getPerUserLimit()));
+            int dailyUsed = dailyMovieClaims(sharedCardIds, coupon.getId());
+            if (dailyUsed < dailyLimit) {
+                return new MovieCouponChoice(coupon, dailyLimit, dailyUsed);
+            }
+        }
+        if (!cardLevelSupported) {
+            throw new BusinessException(ErrorCode.CONFLICT, "当前卡类型不适用该电影票权益券");
+        }
+        throw new BusinessException(ErrorCode.CONFLICT, "当天电影票领取次数已达上限");
+    }
+
+    private void decrementMovieStock(Long couponId, Long merchantId) {
+        int updated = couponMapper.update(null, new UpdateWrapper<BenefitCinemaCoupon>()
+                .setSql("stock = stock - 1")
+                .eq("id", couponId)
+                .eq("merchant_id", merchantId)
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .gt("stock", 0));
+        if (updated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票权益券库存不足");
+        }
+    }
+
+    private int dailyMovieClaims(List<Long> sharedCardIds, Long couponId) {
+        Long count = claimMapper.selectCount(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .in("card_id", sharedCardIds)
+                .eq("coupon_id", couponId)
+                .apply("((upper(coalesce(code_status, 'ACTIVE')) = 'USED' and used_at >= current_date) or "
+                        + "(upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE' and updated_at >= current_date "
+                        + "and updated_at > date_sub(now(), interval 300 second)))"));
+        return intValue(count);
+    }
+
+    private void expireExpiredMovieCodes(Long merchantId) {
+        List<BenefitCinemaCouponClaim> expired = claimMapper.selectList(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .eq("merchant_id", merchantId)
+                .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'")
+                .likeRight("exchange_code", MOVIE_TOKEN_PREFIX)
+                .isNull("used_at")
+                .apply("updated_at <= date_sub(now(), interval 300 second)")
+                .last("for update"));
+        for (BenefitCinemaCouponClaim claim : expired) {
+            int updated = claimMapper.update(null, new UpdateWrapper<BenefitCinemaCouponClaim>()
+                    .set("code_status", "EXPIRED")
+                    .set("status", "EXPIRED")
+                    .eq("id", claim.getId())
+                    .eq("deleted", 0)
+                    .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'"));
+            if (updated == 1) {
+                couponMapper.update(null, new UpdateWrapper<BenefitCinemaCoupon>()
+                        .setSql("stock = stock + 1")
+                        .eq("id", claim.getCouponId())
+                        .eq("deleted", 0));
+            }
+        }
+    }
+
+    private BenefitCinemaCouponClaim movieClaimByIdempotencyKey(String key) {
+        return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .eq("idempotency_key", key)
+                .last("limit 1 for update"));
+    }
+
+    private BenefitCinemaCouponClaim movieClaimForUpdate(Long couponId, Long talentId) {
+        return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .eq("coupon_id", couponId)
+                .eq("talent_id", talentId)
+                .last("limit 1 for update"));
+    }
+
+    private BenefitCinemaCouponClaim movieClaimById(Long claimId) {
+        BenefitCinemaCouponClaim claim = claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .eq("id", claimId)
+                .last("limit 1"));
+        if (claim == null) {
+            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "电影票领取记录保存失败");
+        }
+        return claim;
+    }
+
+    private void ensureSameMovieClaim(BenefitCinemaCouponClaim claim, Long couponId, Long merchantId, Long cardId) {
+        if (!couponId.equals(claim.getCouponId()) || !merchantId.equals(claim.getMerchantId()) || !cardId.equals(claim.getCardId())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "幂等键已被其他电影票核销码请求使用");
+        }
+    }
+
+    private void ensureCinemaMerchant(Long merchantId) {
+        BenefitMerchant merchant = merchantMapper.selectById(merchantId);
+        if (merchant == null || intValue(merchant.getDeleted()) != 0 || intValue(merchant.getStatus()) != 1
+                || !"cinema".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) {
+            throw new BusinessException(ErrorCode.CONFLICT, "未找到有效影院商户");
+        }
+    }
+
+    private void ensureHotelMerchant(Long merchantId) {
+        BenefitMerchant merchant = merchantMapper.selectById(merchantId);
+        if (merchant == null || intValue(merchant.getDeleted()) != 0 || intValue(merchant.getStatus()) != 1
+                || !"hotel".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) {
+            throw new BusinessException(ErrorCode.CONFLICT, "未找到有效酒店商户");
+        }
+    }
+
+    private BenefitFreeStayVoucher voucherForUpdate(Long primaryTalentId, Long merchantId, Long primaryCardId, int totalCount) {
+        BenefitFreeStayVoucher voucher = voucherMapper.selectOne(new QueryWrapper<BenefitFreeStayVoucher>()
+                .eq("talent_id", primaryTalentId)
+                .eq("merchant_id", merchantId)
+                .last("limit 1 for update"));
+        LocalDate validEnd = LocalDate.now().plusYears(1);
+        if (voucher == null) {
+            voucher = new BenefitFreeStayVoucher();
+            voucher.setVoucherNo(BusinessIdGenerator.next("FSV"));
+            voucher.setTalentId(primaryTalentId);
+            voucher.setMerchantId(merchantId);
+            voucher.setCardId(primaryCardId);
+            voucher.setTotalCount(totalCount);
+            voucher.setUsedCount(0);
+            voucher.setValidEnd(validEnd);
+            voucher.setStatus(1);
+            voucherMapper.insert(voucher);
+            return voucher;
+        }
+        if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd())) {
+            voucher.setTotalCount(totalCount);
+            voucher.setUsedCount(0);
+            voucher.setValidEnd(validEnd);
+            voucher.setStatus(1);
+            voucherMapper.updateById(voucher);
+        } else if (intValue(voucher.getTotalCount()) < totalCount) {
+            voucher.setTotalCount(totalCount);
+            voucherMapper.updateById(voucher);
+        }
+        return voucher;
+    }
+
+    private Map<String, Object> movieCodeResponse(BenefitCinemaCouponClaim claim, BenefitCinemaCoupon coupon,
+                                                  BenefitQuotaService.QuotaSnapshot quota,
+                                                  int dailyLimit, int dailyUsed, boolean newlyReserved) {
+        LocalDateTime updatedAt = claim.getUpdatedAt() == null ? LocalDateTime.now() : claim.getUpdatedAt();
+        LocalDateTime expiresAt = updatedAt.plusSeconds(CODE_SECONDS);
+        BenefitMerchant merchant = merchantMapper.selectById(claim.getMerchantId());
+        Map<String, Object> result = baseResponse(BenefitQuotaService.MOVIE_TICKET, claim.getExchangeCode(), expiresAt, quota);
+        result.put("displayCode", claim.getExchangeCode());
+        result.put("sourceType", "CINEMA_COUPON_CLAIM");
+        result.put("sourceId", claim.getId());
+        result.put("merchantId", claim.getMerchantId());
+        result.put("merchantName", merchant == null ? "" : merchant.getMerchantName());
+        result.put("couponId", claim.getCouponId());
+        result.put("couponName", coupon == null ? "" : coupon.getCouponName());
+        result.put("stockRemaining", coupon == null ? null : coupon.getStock());
+        result.put("dailyLimit", dailyLimit);
+        result.put("dailyUsed", dailyUsed);
+        result.put("dailyRemaining", Math.max(dailyLimit - dailyUsed, 0));
+        result.put("quotaEffect", newlyReserved ? "RESERVED_ON_CODE_GENERATION" : "EXISTING_ACTIVE_CODE");
+        return result;
+    }
+
+    private Map<String, Object> hotelCodeResponse(BenefitFreeStayVerificationCode code, BenefitFreeStayVoucher voucher,
+                                                  BenefitQuotaService.QuotaSnapshot quota) {
+        Map<String, Object> result = baseResponse(BenefitQuotaService.HOTEL, code.getCodeToken(), code.getExpiresAt(), quota);
+        result.put("displayCode", shortDisplayCode(code.getCodeToken()));
+        result.put("sourceType", "FREE_STAY_VERIFICATION_CODE");
+        result.put("sourceId", code.getId());
+        result.put("merchantId", voucher.getMerchantId());
+        result.put("voucherId", voucher.getId());
+        result.put("voucherNo", voucher.getVoucherNo());
+        result.put("voucherTotal", voucher.getTotalCount());
+        result.put("voucherUsed", voucher.getUsedCount());
+        result.put("quotaEffect", "DEDUCT_ON_VERIFICATION");
+        return result;
+    }
+
+    private Map<String, Object> baseResponse(String benefitType, Object codeToken, LocalDateTime expiresAt,
+                                             BenefitQuotaService.QuotaSnapshot quota) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("benefitType", benefitType);
+        result.put("verifyCode", codeToken);
+        result.put("expiresAt", expiresAt);
+        result.put("expiresAtEpochMillis", expiresAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
+        result.put("expiresInSeconds", Math.max(0, java.time.Duration.between(LocalDateTime.now(), expiresAt).toSeconds()));
+        result.put("refreshAfterSeconds", CODE_SECONDS);
+        result.put("sharedQuotaMode", "AUTO_DEDUCT");
+        result.put("sourceCardId", quota.cardId());
+        result.put("sourceTalentId", quota.talentId());
+        result.put("sourceCardType", quota.cardType());
+        result.put("familyCard", quota.familyCard());
+        result.put("primaryCardId", quota.primaryCardId());
+        result.put("primaryTalentId", quota.primaryTalentId());
+        result.put("primaryCardNo", quota.primaryCardNo());
+        result.put("cardLevel", quota.cardLevel());
+        result.put("quotaTotal", quota.total());
+        result.put("quotaUsed", quota.used());
+        result.put("quotaRemainingBefore", quota.remaining());
+        result.put("quotaResetCycle", quota.resetCycle());
+        result.put("quotaWindowStart", quota.windowStart());
+        return result;
+    }
+
+    private String hotelFreeStayToken(Long sourceCardId) {
+        long card = sourceCardId == null ? 0 : sourceCardId;
+        return HOTEL_TOKEN_PREFIX + "C" + card + "P0_" + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
+    }
+
+    private boolean activeMovieCode(BenefitCinemaCouponClaim claim) {
+        return "ACTIVE".equals(upper(claim.getCodeStatus())) && !movieCodeExpired(claim.getUpdatedAt());
+    }
+
+    private boolean movieCodeExpired(LocalDateTime updatedAt) {
+        return updatedAt != null && !updatedAt.plusSeconds(CODE_SECONDS).isAfter(LocalDateTime.now());
+    }
+
+    private boolean startsWithMoviePrefix(String code) {
+        return code != null && code.startsWith(MOVIE_TOKEN_PREFIX);
+    }
+
+    private String claimIdempotencyKey(String raw, String token) {
+        if (raw == null || raw.isBlank()) {
+            return "VCODE-" + token;
+        }
+        String key = raw.trim();
+        if (key.length() > 100) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "幂等键长度不能超过限制");
+        }
+        return "VCODE-" + key;
+    }
+
+    private boolean supportsCard(String levels, List<String> aliases) {
+        String configured = levels == null ? "" : levels.trim();
+        if (configured.isBlank()) {
+            return true;
+        }
+        for (String alias : aliases) {
+            if (alias != null && !alias.isBlank() && configured.contains(alias)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String shortDisplayCode(String token) {
+        if (token == null || token.length() <= 8) {
+            return token;
+        }
+        return token.substring(token.length() - 8);
+    }
+
+    private Long requirePositive(Long value, String message) {
+        if (value == null || value <= 0) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, message);
+        }
+        return value;
+    }
+
+    private boolean expiredDate(LocalDate value) {
+        return value != null && value.isBefore(LocalDate.now());
+    }
+
+    private String upper(String value) {
+        return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
+    }
+
+    private int intValue(Object value) {
+        return value instanceof Number number ? number.intValue() : 0;
+    }
+
+    private record MovieCouponChoice(BenefitCinemaCoupon coupon, int dailyLimit, int dailyUsed) {
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationService.java b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationService.java
new file mode 100644
index 0000000..d9d7ee7
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationService.java
@@ -0,0 +1,508 @@
+package com.talentcard.server.service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.talentcard.server.business.BusinessIdGenerator;
+import com.talentcard.server.request.BenefitVerificationRequest;
+import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import com.talentcard.server.entity.BenefitCinemaCoupon;
+import com.talentcard.server.entity.BenefitCinemaCouponClaim;
+import com.talentcard.server.entity.BenefitFreeStayVerificationCode;
+import com.talentcard.server.entity.BenefitFreeStayVoucher;
+import com.talentcard.server.entity.BenefitMerchant;
+import com.talentcard.server.entity.BenefitMerchantProduct;
+import com.talentcard.server.entity.BenefitOrder;
+import com.talentcard.server.entity.BenefitOrderDynamicVerificationCode;
+import com.talentcard.server.entity.BenefitVerifyRecord;
+import com.talentcard.server.mapper.BenefitCinemaCouponClaimMapper;
+import com.talentcard.server.mapper.BenefitCinemaCouponMapper;
+import com.talentcard.server.mapper.BenefitFreeStayVerificationCodeMapper;
+import com.talentcard.server.mapper.BenefitFreeStayVoucherMapper;
+import com.talentcard.server.mapper.BenefitMerchantMapper;
+import com.talentcard.server.mapper.BenefitMerchantProductMapper;
+import com.talentcard.server.mapper.BenefitOrderDynamicVerificationCodeMapper;
+import com.talentcard.server.mapper.BenefitOrderMapper;
+import com.talentcard.server.mapper.BenefitVerifyRecordMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class BenefitVerificationService {
+    private final BenefitQuotaService quotaService;
+    private final BenefitVerifyRecordMapper verifyRecordMapper;
+    private final BenefitCinemaCouponClaimMapper claimMapper;
+    private final BenefitCinemaCouponMapper couponMapper;
+    private final BenefitOrderDynamicVerificationCodeMapper orderCodeMapper;
+    private final BenefitOrderMapper orderMapper;
+    private final BenefitFreeStayVerificationCodeMapper freeStayCodeMapper;
+    private final BenefitFreeStayVoucherMapper voucherMapper;
+    private final BenefitMerchantMapper merchantMapper;
+    private final BenefitMerchantProductMapper productMapper;
+
+    public BenefitVerificationService(
+            BenefitQuotaService quotaService,
+            BenefitVerifyRecordMapper verifyRecordMapper,
+            BenefitCinemaCouponClaimMapper claimMapper,
+            BenefitCinemaCouponMapper couponMapper,
+            BenefitOrderDynamicVerificationCodeMapper orderCodeMapper,
+            BenefitOrderMapper orderMapper,
+            BenefitFreeStayVerificationCodeMapper freeStayCodeMapper,
+            BenefitFreeStayVoucherMapper voucherMapper,
+            BenefitMerchantMapper merchantMapper,
+            BenefitMerchantProductMapper productMapper
+    ) {
+        this.quotaService = quotaService;
+        this.verifyRecordMapper = verifyRecordMapper;
+        this.claimMapper = claimMapper;
+        this.couponMapper = couponMapper;
+        this.orderCodeMapper = orderCodeMapper;
+        this.orderMapper = orderMapper;
+        this.freeStayCodeMapper = freeStayCodeMapper;
+        this.voucherMapper = voucherMapper;
+        this.merchantMapper = merchantMapper;
+        this.productMapper = productMapper;
+    }
+
+    @Transactional
+    public Map<String, Object> verify(MerchantPrincipal principal, BenefitVerificationRequest request, String idempotencyKey) {
+        String key = normalizeIdempotencyKey(idempotencyKey);
+        BenefitVerifyRecord existing = verifiedByIdempotencyKey(principal, key);
+        if (existing != null) {
+            return result(existing, null, null, null, 0);
+        }
+        String benefitType = quotaService.normalizeBenefitType(request.benefitType());
+        String verifyCode = request.verifyCode() == null ? "" : request.verifyCode().trim();
+        if (verifyCode.isBlank()) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "核销码不能为空");
+        }
+        if (BenefitQuotaService.MOVIE_TICKET.equals(benefitType)) {
+            return verifyMovieTicket(principal, verifyCode, request.remark(), key);
+        }
+        return verifyHotel(principal, verifyCode, request.remark(), key);
+    }
+
+    private Map<String, Object> verifyMovieTicket(MerchantPrincipal principal, String verifyCode, String remark, String idempotencyKey) {
+        BenefitCinemaCouponClaim probe = movieClaimByCode(verifyCode, false);
+        if (probe == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "电影票核销码不存在");
+        }
+        if (!principal.merchantId().equals(probe.getMerchantId())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票不属于当前商户");
+        }
+        if (!verifyCode.startsWith(BenefitVerificationCodeService.MOVIE_TOKEN_PREFIX)) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码必须由二期生成核销码接口生成");
+        }
+        if (movieCodeExpired(probe.getUpdatedAt())) {
+            expireMovieClaim(probe.getId());
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已过期");
+        }
+
+        quotaService.current(BenefitQuotaService.MOVIE_TICKET, probe.getCardId(), probe.getTalentId(), probe.getMerchantId(), null);
+        BenefitCinemaCouponClaim claim = movieClaimByCode(verifyCode, true);
+        validateMovieClaim(claim);
+
+        int updated = claimMapper.update(null, new UpdateWrapper<BenefitCinemaCouponClaim>()
+                .set("code_status", "USED")
+                .set("status", "USED")
+                .setSql("used_at = now()")
+                .setSql("updated_at = now()")
+                .eq("id", claim.getId())
+                .eq("deleted", 0)
+                .eq("exchange_code", verifyCode)
+                .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'")
+                .apply("updated_at > date_sub(now(), interval 300 second)"));
+        if (updated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已使用或已过期");
+        }
+        BenefitVerifyRecord record = insertRecord(principal, null, claim.getTalentId(), claim.getCardId(), null,
+                BenefitQuotaService.MOVIE_TICKET, 0L, 0L,
+                sourceRemark("cinemaCouponClaimId=" + claim.getId(), remark), idempotencyKey);
+        BenefitQuotaService.QuotaSnapshot quotaAfter = quotaService.current(BenefitQuotaService.MOVIE_TICKET,
+                claim.getCardId(), claim.getTalentId(), claim.getMerchantId(), null);
+        return result(record, "CINEMA_COUPON_CLAIM", claim.getId(), quotaAfter, 1);
+    }
+
+    private Map<String, Object> verifyHotel(MerchantPrincipal principal, String verifyCode, String remark, String idempotencyKey) {
+        HotelOrderCodeContext orderCode = hotelOrderCode(verifyCode, principal, false);
+        if (orderCode != null) {
+            return verifyHotelOrderCode(principal, verifyCode, orderCode, remark, idempotencyKey);
+        }
+        FreeStayCodeContext freeStayCode = freeStayCode(verifyCode, false);
+        if (freeStayCode != null) {
+            return verifyFreeStayCode(principal, verifyCode, freeStayCode, remark, idempotencyKey);
+        }
+        throw new BusinessException(ErrorCode.NOT_FOUND, "酒店核销码不存在");
+    }
+
+    private Map<String, Object> verifyHotelOrderCode(MerchantPrincipal principal, String verifyCode, HotelOrderCodeContext probe,
+                                                     String remark, String idempotencyKey) {
+        if (!belongsToPrincipal(probe.order(), principal)) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店订单不属于当前门店");
+        }
+        BenefitQuotaService.QuotaSnapshot quotaBefore = quotaService.requireAvailable(BenefitQuotaService.HOTEL,
+                probe.order().getCardId(), probe.order().getTalentId(), principal.merchantId(), probe.order().getProductId(), 1);
+        HotelOrderCodeContext context = hotelOrderCode(verifyCode, principal, true);
+        if (context == null || !belongsToPrincipal(context.order(), principal)) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店订单核销码不属于当前门店");
+        }
+        BenefitOrderDynamicVerificationCode code = context.code();
+        BenefitOrder order = context.order();
+        if (intValue(code.getStatus()) != 0 || expiredDateTime(code.getExpiresAt())
+                || intValue(order.getOrderStatus()) != 1 || intValue(order.getVerifyStatus()) != 0) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店订单当前不可核销");
+        }
+        int codeUpdated = orderCodeMapper.update(null, new UpdateWrapper<BenefitOrderDynamicVerificationCode>()
+                .set("status", 1)
+                .eq("id", code.getId())
+                .eq("status", 0)
+                .gt("expires_at", LocalDateTime.now()));
+        int orderUpdated = orderMapper.update(null, new UpdateWrapper<BenefitOrder>()
+                .set("verify_status", 1)
+                .eq("id", order.getId())
+                .eq("order_status", 1)
+                .eq("verify_status", 0)
+                .eq("deleted", 0));
+        if (codeUpdated != 1 || orderUpdated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店订单已核销或核销码已过期");
+        }
+        BenefitVerifyRecord record = insertRecord(principal, order.getId(), order.getTalentId(), order.getCardId(),
+                order.getBenefitId(), "HOTEL_ORDER", order.getPayAmount(), order.getSubsidyAmount(),
+                sourceRemark("orderNo=" + order.getOrderNo(), remark), idempotencyKey);
+        return result(record, "BENEFIT_ORDER", order.getId(), afterDeduct(quotaBefore, 1), 1);
+    }
+
+    private Map<String, Object> verifyFreeStayCode(MerchantPrincipal principal, String verifyCode, FreeStayCodeContext probe,
+                                                   String remark, String idempotencyKey) {
+        if (!principal.merchantId().equals(probe.voucher().getMerchantId())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码不属于当前商户");
+        }
+        HotelToken token = parseHotelToken(verifyCode);
+        Long sourceCardId = token.sourceCardId() == null ? probe.voucher().getCardId() : token.sourceCardId();
+        Long productId = token.productId();
+        BenefitQuotaService.QuotaSnapshot quotaBefore = quotaService.requireAvailable(BenefitQuotaService.HOTEL,
+                sourceCardId, null, principal.merchantId(), productId, 1);
+
+        FreeStayCodeContext context = freeStayCode(verifyCode, true);
+        if (context == null || !principal.merchantId().equals(context.voucher().getMerchantId())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码不属于当前商户");
+        }
+        BenefitFreeStayVerificationCode code = context.code();
+        BenefitFreeStayVoucher voucher = context.voucher();
+        if (intValue(code.getStatus()) != 0 || expiredDateTime(code.getExpiresAt())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码已过期");
+        }
+        if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd())
+                || intValue(voucher.getUsedCount()) >= intValue(voucher.getTotalCount())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店权益券次数已用完");
+        }
+        if (productId != null) {
+            decrementHotelProductStock(principal.merchantId(), productId);
+        }
+        int codeUpdated = freeStayCodeMapper.update(null, new UpdateWrapper<BenefitFreeStayVerificationCode>()
+                .set("status", 1)
+                .eq("id", code.getId())
+                .eq("status", 0)
+                .gt("expires_at", LocalDateTime.now()));
+        int voucherUpdated = voucherMapper.update(null, new UpdateWrapper<BenefitFreeStayVoucher>()
+                .setSql("used_count = used_count + 1")
+                .eq("id", voucher.getId())
+                .eq("status", 1)
+                .ge("valid_end", LocalDate.now())
+                .apply("used_count < total_count"));
+        if (codeUpdated != 1 || voucherUpdated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码已使用或权益次数已用完");
+        }
+        BenefitVerifyRecord record = insertRecord(principal, null, quotaBefore.talentId(), quotaBefore.cardId(), null,
+                "HOTEL", 0L, 0L,
+                sourceRemark("freeStayVoucherId=" + voucher.getId() + productRemark(productId), remark), idempotencyKey);
+        return result(record, "FREE_STAY_VERIFICATION_CODE", code.getId(), afterDeduct(quotaBefore, 1), 1);
+    }
+
+    private void validateMovieClaim(BenefitCinemaCouponClaim claim) {
+        if (claim == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "电影票核销码不存在");
+        }
+        BenefitCinemaCoupon coupon = couponMapper.selectById(claim.getCouponId());
+        if (!"ACTIVE".equals(upper(claim.getCodeStatus())) || movieCodeExpired(claim.getUpdatedAt())) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已过期");
+        }
+        if (coupon == null || expiredDate(claim.getValidEnd()) || intValue(coupon.getStatus()) != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "电影票权益券当前不可用");
+        }
+    }
+
+    private void expireMovieClaim(Long claimId) {
+        BenefitCinemaCouponClaim claim = claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("id", claimId)
+                .eq("deleted", 0)
+                .last("limit 1 for update"));
+        if (claim == null) {
+            return;
+        }
+        if ("ACTIVE".equals(upper(claim.getCodeStatus()))
+                && startsWithMoviePrefix(claim.getExchangeCode())
+                && movieCodeExpired(claim.getUpdatedAt())) {
+            int updated = claimMapper.update(null, new UpdateWrapper<BenefitCinemaCouponClaim>()
+                    .set("code_status", "EXPIRED")
+                    .set("status", "EXPIRED")
+                    .eq("id", claimId)
+                    .eq("deleted", 0)
+                    .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'"));
+            if (updated == 1) {
+                couponMapper.update(null, new UpdateWrapper<BenefitCinemaCoupon>()
+                        .setSql("stock = stock + 1")
+                        .eq("id", claim.getCouponId())
+                        .eq("deleted", 0));
+            }
+        }
+    }
+
+    private BenefitCinemaCouponClaim movieClaimByCode(String code, boolean lock) {
+        return claimMapper.selectOne(new QueryWrapper<BenefitCinemaCouponClaim>()
+                .eq("deleted", 0)
+                .eq("exchange_code", code)
+                .last("limit 1" + (lock ? " for update" : "")));
+    }
+
+    private HotelOrderCodeContext hotelOrderCode(String token, MerchantPrincipal principal, boolean lock) {
+        BenefitOrderDynamicVerificationCode code = orderCodeMapper.selectOne(new QueryWrapper<BenefitOrderDynamicVerificationCode>()
+                .eq("code_token", token)
+                .last("limit 1" + (lock ? " for update" : "")));
+        if (code == null) {
+            return null;
+        }
+        BenefitOrder order = orderMapper.selectOne(new QueryWrapper<BenefitOrder>()
+                .eq("id", code.getOrderId())
+                .eq("deleted", 0)
+                .last("limit 1" + (lock ? " for update" : "")));
+        if (order == null || !principal.merchantId().equals(order.getMerchantId())) {
+            return null;
+        }
+        BenefitMerchant merchant = merchantMapper.selectById(order.getMerchantId());
+        if (merchant == null || intValue(merchant.getDeleted()) != 0 || !"hotel".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) {
+            return null;
+        }
+        return new HotelOrderCodeContext(code, order);
+    }
+
+    private FreeStayCodeContext freeStayCode(String token, boolean lock) {
+        BenefitFreeStayVerificationCode code = freeStayCodeMapper.selectOne(new QueryWrapper<BenefitFreeStayVerificationCode>()
+                .eq("code_token", token)
+                .last("limit 1" + (lock ? " for update" : "")));
+        if (code == null) {
+            return null;
+        }
+        BenefitFreeStayVoucher voucher = voucherMapper.selectOne(new QueryWrapper<BenefitFreeStayVoucher>()
+                .eq("id", code.getVoucherId())
+                .last("limit 1" + (lock ? " for update" : "")));
+        if (voucher == null) {
+            return null;
+        }
+        return new FreeStayCodeContext(code, voucher);
+    }
+
+    private void decrementHotelProductStock(Long merchantId, Long productId) {
+        int updated = productMapper.update(null, new UpdateWrapper<BenefitMerchantProduct>()
+                .setSql("stock = case when stock = -1 then stock else stock - 1 end")
+                .setSql("sales_count = sales_count + 1")
+                .eq("id", productId)
+                .eq("merchant_id", merchantId)
+                .eq("deleted", 0)
+                .eq("status", 1)
+                .and(wrapper -> wrapper.isNull("valid_start_time").or().le("valid_start_time", LocalDateTime.now()))
+                .and(wrapper -> wrapper.isNull("valid_end_time").or().gt("valid_end_time", LocalDateTime.now()))
+                .apply("(stock = -1 or stock > 0)"));
+        if (updated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店房型或套餐库存不足");
+        }
+    }
+
+    private BenefitVerifyRecord insertRecord(MerchantPrincipal principal, Long orderId, Long talentId, Long cardId, Long benefitId,
+                                             String verifyType, Long verifyAmount, Long subsidyAmount, String remark, String idempotencyKey) {
+        BenefitVerifyRecord record = new BenefitVerifyRecord();
+        record.setVerifyNo(BusinessIdGenerator.next("V"));
+        record.setOrderId(orderId);
+        record.setTalentId(talentId);
+        record.setCardId(cardId);
+        record.setBenefitId(benefitId);
+        record.setMerchantId(principal.merchantId());
+        record.setStoreId(principal.storeId());
+        record.setStaffId(principal.operatorId());
+        record.setVerifyType(verifyType);
+        record.setVerifyAmount(zeroIfNull(verifyAmount));
+        record.setSubsidyAmount(zeroIfNull(subsidyAmount));
+        record.setRemark(trimToLength(remark, 512));
+        record.setStatus(1);
+        record.setIdempotencyKey(idempotencyKey);
+        verifyRecordMapper.insert(record);
+        return record(record.getId());
+    }
+
+    private BenefitVerifyRecord verifiedByIdempotencyKey(MerchantPrincipal principal, String key) {
+        return verifyRecordMapper.selectOne(new QueryWrapper<BenefitVerifyRecord>()
+                .eq("idempotency_key", key)
+                .eq("merchant_id", principal.merchantId())
+                .eq("status", 1)
+                .last("limit 1"));
+    }
+
+    private BenefitVerifyRecord record(Long recordId) {
+        BenefitVerifyRecord record = verifyRecordMapper.selectById(recordId);
+        if (record == null) {
+            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "核销记录保存失败");
+        }
+        return record;
+    }
+
+    private Map<String, Object> result(BenefitVerifyRecord record, String sourceType, Object sourceId,
+                                       BenefitQuotaService.QuotaSnapshot quota, int quantity) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("verified", true);
+        result.put("verifyRecordId", record.getId());
+        result.put("verifyNo", record.getVerifyNo());
+        result.put("benefitType", record.getVerifyType());
+        result.put("sourceType", sourceType);
+        result.put("sourceId", sourceId);
+        result.put("quantity", quantity);
+        result.put("verifiedAt", record.getVerifyTime());
+        result.put("sharedQuotaMode", "AUTO_DEDUCT");
+        if (quota != null) {
+            result.put("sourceCardId", quota.cardId());
+            result.put("sourceTalentId", quota.talentId());
+            result.put("familyCard", quota.familyCard());
+            result.put("primaryCardId", quota.primaryCardId());
+            result.put("primaryTalentId", quota.primaryTalentId());
+            result.put("primaryCardNo", quota.primaryCardNo());
+            result.put("cardLevel", quota.cardLevel());
+            result.put("quotaTotal", quota.total());
+            result.put("quotaUsed", quota.used());
+            result.put("quotaRemaining", quota.remaining());
+            result.put("quotaResetCycle", quota.resetCycle());
+            result.put("quotaWindowStart", quota.windowStart());
+        }
+        return result;
+    }
+
+    private BenefitQuotaService.QuotaSnapshot afterDeduct(BenefitQuotaService.QuotaSnapshot quota, int quantity) {
+        return new BenefitQuotaService.QuotaSnapshot(
+                quota.benefitType(),
+                quota.talentId(),
+                quota.cardId(),
+                quota.cardNo(),
+                quota.cardType(),
+                quota.primaryTalentId(),
+                quota.primaryCardId(),
+                quota.primaryCardNo(),
+                quota.cardLevel(),
+                quota.total(),
+                quota.used() + quantity,
+                Math.max(quota.remaining() - quantity, 0),
+                quota.resetCycle(),
+                quota.windowStart(),
+                quota.familyCard(),
+                quota.sharedCardIds()
+        );
+    }
+
+    private boolean belongsToPrincipal(BenefitOrder order, MerchantPrincipal principal) {
+        Long storeId = order.getStoreId();
+        return principal.merchantId().equals(order.getMerchantId()) && (storeId == null || principal.storeId().equals(storeId));
+    }
+
+    private HotelToken parseHotelToken(String token) {
+        if (token == null || !token.startsWith(BenefitVerificationCodeService.HOTEL_TOKEN_PREFIX + "C")) {
+            return new HotelToken(null, null);
+        }
+        try {
+            int cIndex = token.indexOf('C', BenefitVerificationCodeService.HOTEL_TOKEN_PREFIX.length());
+            int pIndex = token.indexOf('P', cIndex + 1);
+            int splitIndex = token.indexOf('_', pIndex + 1);
+            if (cIndex < 0 || pIndex < 0 || splitIndex < 0) {
+                return new HotelToken(null, null);
+            }
+            Long cardId = parsePositiveLong(token.substring(cIndex + 1, pIndex));
+            Long productId = parsePositiveLong(token.substring(pIndex + 1, splitIndex));
+            return new HotelToken(cardId, productId);
+        } catch (RuntimeException ignored) {
+            return new HotelToken(null, null);
+        }
+    }
+
+    private Long parsePositiveLong(String value) {
+        long parsed = Long.parseLong(value);
+        return parsed > 0 ? parsed : null;
+    }
+
+    private boolean movieCodeExpired(LocalDateTime updatedAt) {
+        return updatedAt != null && !updatedAt.plusSeconds(BenefitVerificationCodeService.CODE_SECONDS).isAfter(LocalDateTime.now());
+    }
+
+    private boolean startsWithMoviePrefix(String code) {
+        return code != null && code.startsWith(BenefitVerificationCodeService.MOVIE_TOKEN_PREFIX);
+    }
+
+    private boolean expiredDate(LocalDate value) {
+        return value != null && value.isBefore(LocalDate.now());
+    }
+
+    private boolean expiredDateTime(LocalDateTime value) {
+        return value != null && !value.isAfter(LocalDateTime.now());
+    }
+
+    private String normalizeIdempotencyKey(String idempotencyKey) {
+        if (idempotencyKey == null || idempotencyKey.isBlank()) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "幂等键不能为空");
+        }
+        String key = idempotencyKey.trim();
+        if (key.length() > 128) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "幂等键长度不能超过限制");
+        }
+        return key;
+    }
+
+    private String sourceRemark(String source, String remark) {
+        String cleaned = remark == null ? "" : remark.trim();
+        return cleaned.isBlank() ? source : source + "; " + cleaned;
+    }
+
+    private String productRemark(Long productId) {
+        return productId == null ? "" : "; productId=" + productId;
+    }
+
+    private String trimToLength(String value, int maxLength) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.length() <= maxLength ? trimmed : trimmed.substring(0, maxLength);
+    }
+
+    private String upper(String value) {
+        return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
+    }
+
+    private int intValue(Object value) {
+        return value instanceof Number number ? number.intValue() : 0;
+    }
+
+    private long zeroIfNull(Long value) {
+        return value == null ? 0L : value;
+    }
+
+    private record HotelToken(Long sourceCardId, Long productId) {
+    }
+
+    private record HotelOrderCodeContext(BenefitOrderDynamicVerificationCode code, BenefitOrder order) {
+    }
+
+    private record FreeStayCodeContext(BenefitFreeStayVerificationCode code, BenefitFreeStayVoucher voucher) {
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java
new file mode 100644
index 0000000..7deb67e
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java
@@ -0,0 +1,185 @@
+package com.talentcard.server.service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.talentcard.server.business.PageResult;
+import com.talentcard.server.request.DependentCardAuditRequest;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import com.talentcard.server.entity.DependentCardApply;
+import com.talentcard.server.entity.DependentCardApplyStatusLog;
+import com.talentcard.server.mapper.DependentCardApplyMapper;
+import com.talentcard.server.mapper.DependentCardApplyStatusLogMapper;
+import com.talentcard.server.response.DependentCardApplyAdminResponse;
+import com.talentcard.server.response.DependentCardApplyStatusLogResponse;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class DependentCardApplyAdminService {
+
+	private static final String ELITE_CARD_LEVEL = "卓越人才卡";
+	private static final int MAX_FAMILY_CARD_COUNT = 2;
+
+	private final DependentCardApplyMapper mapper;
+	private final DependentCardApplyStatusLogMapper statusLogMapper;
+
+	public DependentCardApplyAdminService(
+			DependentCardApplyMapper mapper,
+			DependentCardApplyStatusLogMapper statusLogMapper
+	) {
+		this.mapper = mapper;
+		this.statusLogMapper = statusLogMapper;
+	}
+
+	@Transactional(readOnly = true)
+	public PageResult<DependentCardApplyAdminResponse> page(
+			Long talentCardId,
+			Integer status,
+			int page,
+			int size
+	) {
+		validateStatusFilter(status);
+		int safePage = Math.max(page, 1);
+		int safeSize = Math.min(Math.max(size, 1), 100);
+		LambdaQueryWrapper<DependentCardApply> query = Wrappers.<DependentCardApply>lambdaQuery()
+				.eq(talentCardId != null, DependentCardApply::getTalentCardId, talentCardId)
+				.eq(status != null, DependentCardApply::getStatus, status);
+		long total = mapper.selectCount(query);
+		if (total == 0) {
+			return new PageResult<>(List.of(), safePage, safeSize, 0);
+		}
+		long offset = (long) (safePage - 1) * safeSize;
+		List<DependentCardApplyAdminResponse> records = mapper.selectList(
+						query.orderByDesc(DependentCardApply::getApplyTime)
+								.orderByDesc(DependentCardApply::getId)
+								.last("limit " + safeSize + " offset " + offset)
+				).stream()
+				.map(DependentCardApplyAdminResponse::from)
+				.toList();
+		return new PageResult<>(records, safePage, safeSize, total);
+	}
+
+	@Transactional(readOnly = true)
+	public DependentCardApplyAdminResponse detail(Long id) {
+		return DependentCardApplyAdminResponse.from(requireApplication(id));
+	}
+
+	@Transactional(readOnly = true)
+	public List<DependentCardApplyStatusLogResponse> statusLogs(Long id) {
+		DependentCardApply apply = requireApplication(id);
+		return statusLogMapper.selectList(
+						Wrappers.<DependentCardApplyStatusLog>lambdaQuery()
+								.eq(DependentCardApplyStatusLog::getApplyId, apply.getId())
+								.orderByAsc(DependentCardApplyStatusLog::getCreatedAt)
+								.orderByAsc(DependentCardApplyStatusLog::getId)
+				).stream()
+				.map(DependentCardApplyStatusLogResponse::from)
+				.toList();
+	}
+
+	@Transactional
+	public DependentCardApplyAdminResponse audit(
+			Long id,
+			DependentCardAuditRequest request,
+			Long operatorId
+	) {
+		int targetStatus = request.status();
+		if (targetStatus != DependentCardApply.STATUS_APPROVED
+				&& targetStatus != DependentCardApply.STATUS_REJECTED) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "审核状态只能为1通过或2驳回");
+		}
+		String auditRemark = trimToNull(request.auditRemark());
+		if (targetStatus == DependentCardApply.STATUS_REJECTED && auditRemark == null) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "驳回申请时必须填写审核备注");
+		}
+		DependentCardApply apply = mapper.selectByIdForUpdate(id);
+		if (apply == null) {
+			throw new BusinessException(ErrorCode.NOT_FOUND, "家属附属卡申请不存在");
+		}
+		if (!Integer.valueOf(DependentCardApply.STATUS_PENDING_REVIEW).equals(apply.getStatus())) {
+			throw new BusinessException(ErrorCode.CONFLICT, "该申请已审核或已解绑");
+		}
+		if (targetStatus == DependentCardApply.STATUS_APPROVED) {
+			ensureApprovalSlot(apply);
+		}
+		LocalDateTime auditTime = LocalDateTime.now();
+		int updated = mapper.update(
+				null,
+				Wrappers.<DependentCardApply>lambdaUpdate()
+						.eq(DependentCardApply::getId, id)
+						.eq(DependentCardApply::getStatus, DependentCardApply.STATUS_PENDING_REVIEW)
+						.set(DependentCardApply::getStatus, targetStatus)
+						.set(DependentCardApply::getAuditBy, operatorId)
+						.set(DependentCardApply::getAuditTime, auditTime)
+						.set(DependentCardApply::getAuditRemark, auditRemark)
+		);
+		if (updated != 1) {
+			throw new BusinessException(ErrorCode.CONFLICT, "申请状态已变化,请刷新后重试");
+		}
+		if (statusLogMapper.insert(new DependentCardApplyStatusLog(
+				apply.getId(),
+				apply.getApplyNo(),
+				DependentCardApply.STATUS_PENDING_REVIEW,
+				targetStatus,
+				"ADMIN",
+				operatorId,
+				auditRemark
+		)) != 1) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "审核状态记录保存失败");
+		}
+		return DependentCardApplyAdminResponse.from(mapper.selectById(id));
+	}
+
+	private void ensureApprovalSlot(DependentCardApply apply) {
+		DependentCardApply ownerCard = mapper.selectOwnerCard(apply.getTalentCardId());
+		if (ownerCard == null) {
+			throw new BusinessException(ErrorCode.CONFLICT, "主卡不存在或已失效,不能审核通过");
+		}
+		if (!ELITE_CARD_LEVEL.equals(ownerCard.getCardLevel())
+				|| !Integer.valueOf(1).equals(ownerCard.getIsFamilyCardEnabled())) {
+			throw new BusinessException(ErrorCode.CONFLICT, "该主卡当前无家属附属卡权限");
+		}
+		Integer configuredLimit = ownerCard.getFamilyCardLimit();
+		if (configuredLimit == null || configuredLimit <= 0) {
+			throw new BusinessException(ErrorCode.CONFLICT, "该主卡未配置家属附属卡名额");
+		}
+		int familyCardLimit = Math.min(configuredLimit, MAX_FAMILY_CARD_COUNT);
+		long approvedCount = mapper.selectCount(
+				Wrappers.<DependentCardApply>lambdaQuery()
+						.eq(DependentCardApply::getTalentCardId, apply.getTalentCardId())
+						.eq(DependentCardApply::getStatus, DependentCardApply.STATUS_APPROVED)
+		);
+		if (approvedCount >= familyCardLimit) {
+			throw new BusinessException(ErrorCode.CONFLICT, "该主卡已达到家属附属卡绑定上限");
+		}
+	}
+
+	private DependentCardApply requireApplication(Long id) {
+		if (id == null || id <= 0) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "申请ID不能为空");
+		}
+		DependentCardApply apply = mapper.selectById(id);
+		if (apply == null) {
+			throw new BusinessException(ErrorCode.NOT_FOUND, "家属附属卡申请不存在");
+		}
+		return apply;
+	}
+
+	private void validateStatusFilter(Integer status) {
+		if (status != null && (status < DependentCardApply.STATUS_PENDING_REVIEW
+				|| status > DependentCardApply.STATUS_CANCELLED)) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "申请状态不正确");
+		}
+	}
+
+	private String trimToNull(String value) {
+		if (value == null || value.isBlank()) {
+			return null;
+		}
+		return value.trim();
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java b/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java
new file mode 100644
index 0000000..668919b
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java
@@ -0,0 +1,192 @@
+package com.talentcard.server.service;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.talentcard.server.business.BusinessIdGenerator;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import com.talentcard.server.entity.DependentCardApply;
+import com.talentcard.server.entity.DependentCardApplyStatusLog;
+import com.talentcard.server.mapper.DependentCardApplyMapper;
+import com.talentcard.server.mapper.DependentCardApplyStatusLogMapper;
+import com.talentcard.server.request.DependentCardApplyRequest;
+import com.talentcard.server.request.DependentCardUnbindRequest;
+import com.talentcard.server.response.DependentCardApplyItemResponse;
+import com.talentcard.server.response.DependentCardApplyResponse;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class DependentCardApplyService {
+
+	private static final String ELITE_CARD_LEVEL = "卓越人才卡";
+	private static final int MAX_APPLICATION_COUNT = 3;
+	private static final int MAX_FAMILY_CARD_COUNT = 2;
+
+	private final DependentCardApplyMapper mapper;
+	private final DependentCardApplyStatusLogMapper statusLogMapper;
+
+	public DependentCardApplyService(
+			DependentCardApplyMapper mapper,
+			DependentCardApplyStatusLogMapper statusLogMapper
+	) {
+		this.mapper = mapper;
+		this.statusLogMapper = statusLogMapper;
+	}
+
+	@Transactional
+	public DependentCardApplyResponse submit(DependentCardApplyRequest request) {
+		DependentCardApply ownerCard = ownerCard(request.talentCardId());
+		int familyCardLimit = familyCardLimit(ownerCard);
+		long applicationCount = mapper.selectCount(
+				Wrappers.<DependentCardApply>lambdaQuery()
+						.eq(DependentCardApply::getTalentCardId, request.talentCardId())
+						.in(
+								DependentCardApply::getStatus,
+								DependentCardApply.STATUS_PENDING_REVIEW,
+								DependentCardApply.STATUS_APPROVED,
+								DependentCardApply.STATUS_REJECTED
+						)
+		);
+		if (applicationCount >= MAX_APPLICATION_COUNT) {
+			throw new BusinessException(ErrorCode.CONFLICT, "同一张主卡最多提交3个家属附属卡申请");
+		}
+		long occupiedCount = mapper.selectCount(
+				Wrappers.<DependentCardApply>lambdaQuery()
+						.eq(DependentCardApply::getTalentCardId, request.talentCardId())
+						.in(
+								DependentCardApply::getStatus,
+								DependentCardApply.STATUS_PENDING_REVIEW,
+								DependentCardApply.STATUS_APPROVED
+						)
+		);
+		if (occupiedCount >= familyCardLimit) {
+			throw new BusinessException(
+					ErrorCode.CONFLICT,
+					"该主卡家属附属卡名额已满,最多绑定" + familyCardLimit + "个"
+			);
+		}
+		DependentCardApply apply = new DependentCardApply(
+				BusinessIdGenerator.next("DCA"),
+				number(ownerCard.getTalentId()),
+				number(ownerCard.getTalentCardId()),
+				text(ownerCard.getTalentName()),
+				text(ownerCard.getTalentMobile()),
+				text(ownerCard.getTalentCardNo()),
+				text(ownerCard.getCardLevel()),
+				request.dependentName().trim(),
+				request.dependentMobile().trim(),
+				request.dependentIdCardNo().trim().toUpperCase(),
+				request.relation().trim()
+		);
+		if (mapper.insert(apply) != 1) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "家属附属卡申请提交失败");
+		}
+		recordStatus(apply, null, DependentCardApply.STATUS_PENDING_REVIEW, "TALENT", apply.getTalentId(), "小程序提交申请");
+		return DependentCardApplyResponse.from(apply);
+	}
+
+	@Transactional(readOnly = true)
+	public List<DependentCardApplyItemResponse> list(Long talentCardId) {
+		requireTalentCardId(talentCardId);
+		return mapper.selectList(
+						Wrappers.<DependentCardApply>lambdaQuery()
+								.eq(DependentCardApply::getTalentCardId, talentCardId)
+								.orderByDesc(DependentCardApply::getApplyTime)
+								.orderByDesc(DependentCardApply::getId)
+				).stream()
+				.map(DependentCardApplyItemResponse::from)
+				.toList();
+	}
+
+	@Transactional
+	public DependentCardApplyItemResponse unbind(Long id, DependentCardUnbindRequest request) {
+		requireTalentCardId(request.talentCardId());
+		DependentCardApply apply = mapper.selectByIdForUpdate(id);
+		if (apply == null || !request.talentCardId().equals(apply.getTalentCardId())) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "家属附属卡申请不存在");
+		}
+//		if (!Integer.valueOf(DependentCardApply.STATUS_APPROVED).equals(apply.getStatus())) {
+//			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "仅已通过审核的家属附属卡可以解绑");
+//		}
+		int updated = mapper.update(
+				null,
+				Wrappers.<DependentCardApply>lambdaUpdate()
+						.eq(DependentCardApply::getId, id)
+						.eq(DependentCardApply::getTalentCardId, request.talentCardId())
+						.eq(DependentCardApply::getStatus, DependentCardApply.STATUS_APPROVED)
+						.set(DependentCardApply::getStatus, DependentCardApply.STATUS_CANCELLED)
+		);
+		if (updated != 1) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "家属附属卡状态已变化,请刷新后重试");
+		}
+		recordStatus(
+				apply,
+				DependentCardApply.STATUS_APPROVED,
+				DependentCardApply.STATUS_CANCELLED,
+				"TALENT",
+				apply.getTalentId(),
+				"小程序用户主动解绑"
+		);
+		return DependentCardApplyItemResponse.from(mapper.selectById(id));
+	}
+
+	private DependentCardApply ownerCard(Long talentCardId) {
+		requireTalentCardId(talentCardId);
+		DependentCardApply card = mapper.selectOwnerCard(talentCardId);
+		if (card == null) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "主卡不存在或已失效");
+		}
+		return card;
+	}
+
+	private int familyCardLimit(DependentCardApply card) {
+		if (!ELITE_CARD_LEVEL.equals(card.getCardLevel())) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "仅卓越人才卡可申请家属附属卡");
+		}
+		if (!Integer.valueOf(1).equals(card.getIsFamilyCardEnabled())) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未开通家属附属卡申请权限");
+		}
+		Integer configuredLimit = card.getFamilyCardLimit();
+		if (configuredLimit == null || configuredLimit <= 0) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未配置家属附属卡名额");
+		}
+		return Math.min(configuredLimit, MAX_FAMILY_CARD_COUNT);
+	}
+
+	private void recordStatus(
+			DependentCardApply apply,
+			Integer fromStatus,
+			Integer toStatus,
+			String operatorType,
+			Long operatorId,
+			String remark
+	) {
+		if (statusLogMapper.insert(new DependentCardApplyStatusLog(
+				apply.getId(),
+				apply.getApplyNo(),
+				fromStatus,
+				toStatus,
+				operatorType,
+				operatorId,
+				remark
+		)) != 1) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "家属附属卡状态记录保存失败");
+		}
+	}
+
+	private void requireTalentCardId(Long talentCardId) {
+		if (talentCardId == null || talentCardId <= 0) {
+			throw new BusinessException(ErrorCode.INTERNAL_ERROR, "主卡ID不能为空");
+		}
+	}
+
+	private Long number(Object value) {
+		return value instanceof Number number ? number.longValue() : null;
+	}
+
+	private String text(Object value) {
+		return value == null ? null : String.valueOf(value);
+	}
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java
new file mode 100644
index 0000000..f3388b8
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java
@@ -0,0 +1,407 @@
+package com.talentcard.server.service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.talentcard.server.business.PageResult;
+import com.talentcard.server.request.ServiceAppointmentStatusUpdateRequest;
+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.mapper.ChildInformationMapper;
+import com.talentcard.server.mapper.ServiceApplicationMapper;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class ServiceAppointmentAdminService {
+    private static final List<String> PENDING_STATUSES = List.of("待处理", "待审核");
+    private static final List<String> REJECTED_STATUSES = List.of("已驳回", "审核不通过", "已取消");
+
+    private final ServiceApplicationMapper applicationMapper;
+    private final ChildInformationMapper childInformationMapper;
+    private final SensitiveDataCrypto sensitiveDataCrypto;
+
+    public ServiceAppointmentAdminService(
+            ServiceApplicationMapper applicationMapper,
+            ChildInformationMapper childInformationMapper,
+            SensitiveDataCrypto sensitiveDataCrypto
+    ) {
+        this.applicationMapper = applicationMapper;
+        this.childInformationMapper = childInformationMapper;
+        this.sensitiveDataCrypto = sensitiveDataCrypto;
+    }
+
+    @Transactional(readOnly = true)
+    public PageResult<Map<String, Object>> page(String applicationType, String keyword, String serviceKeyword,
+                                                String cardLevel, String status, String submittedStart,
+                                                String submittedEnd, int page, int size) {
+        int safePage = Math.max(page, 1);
+        int safeSize = Math.min(Math.max(size, 1), 100);
+        QueryWrapper<ServiceApplication> countWrapper = filter(applicationType, keyword, serviceKeyword, cardLevel, status, submittedStart, submittedEnd);
+        Long total = applicationMapper.selectCount(countWrapper);
+        QueryWrapper<ServiceApplication> pageWrapper = filter(applicationType, keyword, serviceKeyword, cardLevel, status, submittedStart, submittedEnd)
+                .orderByDesc("submitted_at", "id")
+                .last("limit " + safeSize + " offset " + ((safePage - 1) * safeSize));
+        List<Map<String, Object>> records = applicationMapper.selectList(pageWrapper).stream()
+                .map(this::listView)
+                .toList();
+        return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
+    }
+
+    @Transactional(readOnly = true)
+    public Map<String, Object> detail(Long id) {
+        ServiceApplication application = application(id);
+        return detailView(application);
+    }
+
+    @Transactional
+    public Map<String, Object> updateStatus(Long id, ServiceAppointmentStatusUpdateRequest request, Long operatorId) {
+        ServiceApplication current = applicationMapper.selectByIdForUpdate(id);
+        if (current == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "预约申请不存在");
+        }
+        String nextStatus = normalizeStatus(request.status(), current.getApplicationType());
+        String remark = first(request.remark(), request.rejectReason());
+        if (REJECTED_STATUSES.contains(nextStatus) && remark.isBlank()) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "拒绝或驳回时必须填写处理备注");
+        }
+        String handlerName = first(request.handlerName(), operatorId == null ? "" : "管理员" + operatorId, "管理员");
+        LocalDateTime handledAt = PENDING_STATUSES.contains(nextStatus) ? null : LocalDateTime.now();
+        UpdateWrapper<ServiceApplication> update = new UpdateWrapper<ServiceApplication>()
+                .set("status", nextStatus)
+                .set("handled_at", handledAt)
+                .set("handler_name", handledAt == null ? null : handlerName)
+                .set("reject_reason", remark.isBlank() ? null : remark)
+                .eq("id", id)
+                .eq("deleted", 0);
+        int updated = applicationMapper.update(null, update);
+        if (updated != 1) {
+            throw new BusinessException(ErrorCode.CONFLICT, "预约申请状态已变化,请刷新后重试");
+        }
+        syncChildAudit(current, nextStatus, remark);
+        return detail(id);
+    }
+
+    @Transactional(readOnly = true)
+    public byte[] exportExcel(String applicationType, String keyword, String serviceKeyword,
+                              String cardLevel, String status, String submittedStart, String submittedEnd) {
+        QueryWrapper<ServiceApplication> wrapper = filter(applicationType, keyword, serviceKeyword, cardLevel, status, submittedStart, submittedEnd)
+                .orderByDesc("submitted_at", "id");
+        List<ServiceApplication> applications = applicationMapper.selectList(wrapper);
+        String[] headers = {
+                "序号", "申请编号", "预约类型", "状态", "持卡人", "持卡人手机号", "人才卡等级", "人才卡号",
+                "服务机构", "联系电话", "预约内容", "预约时间", "申请人/配偶/子女", "申请人手机号",
+                "证件号", "子女性别", "子女出生日期", "处理人", "处理时间", "处理备注", "提交时间"
+        };
+        try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            Sheet sheet = workbook.createSheet("预约申请");
+            sheet.createFreezePane(0, 1);
+            CellStyle headerStyle = headerStyle(workbook);
+            Row headerRow = sheet.createRow(0);
+            for (int index = 0; index < headers.length; index++) {
+                headerRow.createCell(index).setCellValue(headers[index]);
+                headerRow.getCell(index).setCellStyle(headerStyle);
+            }
+            int rowIndex = 1;
+            for (ServiceApplication application : applications) {
+                Row row = sheet.createRow(rowIndex);
+                List<String> values = exportValues(rowIndex, application);
+                for (int index = 0; index < values.size(); index++) {
+                    row.createCell(index).setCellValue(values.get(index));
+                }
+                rowIndex++;
+            }
+            int[] widths = { 8, 24, 18, 14, 16, 18, 16, 20, 28, 18, 24, 28, 20, 18, 24, 12, 16, 16, 22, 30, 22 };
+            for (int index = 0; index < widths.length; index++) {
+                sheet.setColumnWidth(index, widths[index] * 256);
+            }
+            workbook.write(output);
+            return output.toByteArray();
+        } catch (IOException exception) {
+            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "预约申请导出失败");
+        }
+    }
+
+    private QueryWrapper<ServiceApplication> filter(String applicationType, String keyword, String serviceKeyword,
+                                                    String cardLevel, String status, String submittedStart, String submittedEnd) {
+        QueryWrapper<ServiceApplication> wrapper = new QueryWrapper<ServiceApplication>().eq("deleted", 0);
+        String type = normalizeFilterType(applicationType);
+        if (!type.isBlank()) {
+            wrapper.eq("application_type", type);
+        }
+        String key = clean(keyword);
+        if (!key.isBlank()) {
+            String like = "%" + key + "%";
+            wrapper.and(query -> query.like("application_no", like)
+                    .or().like("talent_name", like)
+                    .or().like("talent_mobile", like)
+                    .or().like("card_no", like)
+                    .or().like("applicant_name", like)
+                    .or().like("applicant_mobile", like)
+                    .or().like("spouse_name", like)
+                    .or().like("child_name", like));
+        }
+        String service = clean(serviceKeyword);
+        if (!service.isBlank()) {
+            String like = "%" + service + "%";
+            wrapper.and(query -> query.like("merchant_name", like)
+                    .or().like("hospital_name", like)
+                    .or().like("package_name", like)
+                    .or().like("product_name", like));
+        }
+        if (!clean(cardLevel).isBlank()) {
+            wrapper.eq("talent_level", clean(cardLevel));
+        }
+        if (!clean(status).isBlank()) {
+            wrapper.eq("status", clean(status));
+        }
+        LocalDate start = localDate(submittedStart, "提交开始日期格式不正确");
+        LocalDate end = localDate(submittedEnd, "提交结束日期格式不正确");
+        if (start != null) {
+            wrapper.ge("submitted_at", start.atStartOfDay());
+        }
+        if (end != null) {
+            wrapper.lt("submitted_at", end.plusDays(1).atStartOfDay());
+        }
+        return wrapper;
+    }
+
+    private Map<String, Object> listView(ServiceApplication application) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("id", 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("talentName", application.getTalentName());
+        result.put("talentMobile", application.getTalentMobile());
+        result.put("talentLevel", application.getTalentLevel());
+        result.put("cardNo", application.getCardNo());
+        result.put("serviceName", serviceName(application));
+        result.put("appointmentContent", appointmentContent(application));
+        result.put("appointmentTime", first(application.getExpectedTime(), "以专员通知为准"));
+        result.put("applicantName", participantName(application));
+        result.put("submittedAt", application.getSubmittedAt());
+        result.put("handledAt", application.getHandledAt());
+        result.put("handlerName", application.getHandlerName());
+        return result;
+    }
+
+    private Map<String, Object> detailView(ServiceApplication application) {
+        Map<String, Object> result = listView(application);
+        result.put("talentId", application.getTalentId());
+        result.put("cardId", application.getCardId());
+        result.put("merchantId", application.getMerchantId());
+        result.put("contactMobile", application.getContactMobile());
+        result.put("packageName", application.getPackageName());
+        result.put("productName", application.getProductName());
+        result.put("applicantMobile", sensitiveDataCrypto.decrypt(application.getApplicantMobile()));
+        result.put("applicantIdCardNo", sensitiveDataCrypto.decrypt(application.getApplicantIdCardNo()));
+        result.put("checkInDate", application.getCheckInDate());
+        result.put("checkOutDate", application.getCheckOutDate());
+        result.put("guestCount", application.getGuestCount());
+        result.put("subsidy", application.getSubsidy());
+        result.put("remark", application.getRejectReason());
+        result.put("estimatedProcessingTime", application.getEstimatedProcessingTime());
+        result.put("institutionAddress", application.getInstitutionAddressSnapshot());
+        result.put("institutionLongitude", application.getInstitutionLongitudeSnapshot());
+        result.put("institutionLatitude", application.getInstitutionLatitudeSnapshot());
+        Map<String, Object> spouse = new LinkedHashMap<>();
+        spouse.put("name", first(application.getSpouseName()));
+        spouse.put("mobile", sensitiveDataCrypto.decrypt(application.getSpouseMobile()));
+        spouse.put("idCardNo", sensitiveDataCrypto.decrypt(application.getSpouseIdCardNo()));
+        spouse.put("politicalStatus", first(application.getSpousePoliticalStatus()));
+        spouse.put("education", first(application.getSpouseEducation()));
+        spouse.put("schoolMajor", first(application.getSpouseSchoolMajor()));
+        spouse.put("workUnit", first(application.getSpouseWorkUnit()));
+        spouse.put("inInstitution", first(application.getSpouseInInstitution()));
+        spouse.put("workExperience", first(application.getSpouseWorkExperience()));
+        result.put("spouse", spouse);
+
+        Map<String, Object> child = new LinkedHashMap<>();
+        child.put("id", application.getChildId());
+        child.put("name", first(application.getChildName()));
+        child.put("gender", first(application.getChildGender()));
+        child.put("birthDate", application.getChildBirthDate());
+        child.put("proofFileIds", proofFileIds(application.getProofFileIds()));
+        result.put("child", child);
+        return result;
+    }
+
+    private List<String> exportValues(int index, ServiceApplication application) {
+        return List.of(
+                String.valueOf(index),
+                first(application.getApplicationNo()),
+                typeName(application.getApplicationType()),
+                first(application.getStatus()),
+                first(application.getTalentName()),
+                first(application.getTalentMobile()),
+                first(application.getTalentLevel()),
+                first(application.getCardNo()),
+                serviceName(application),
+                first(application.getContactMobile()),
+                appointmentContent(application),
+                first(application.getExpectedTime()),
+                participantName(application),
+                sensitiveDataCrypto.decrypt(application.getApplicantMobile()),
+                sensitiveDataCrypto.decrypt(first(application.getApplicantIdCardNo(), application.getSpouseIdCardNo())),
+                first(application.getChildGender()),
+                application.getChildBirthDate() == null ? "" : application.getChildBirthDate().toString(),
+                first(application.getHandlerName()),
+                application.getHandledAt() == null ? "" : application.getHandledAt().toString(),
+                first(application.getRejectReason()),
+                application.getSubmittedAt() == null ? "" : application.getSubmittedAt().toString()
+        );
+    }
+
+    private void syncChildAudit(ServiceApplication application, String status, String remark) {
+        if (!ServiceAppointmentApplicationService.TYPE_CHILD.equals(application.getApplicationType()) || application.getChildId() == null) {
+            return;
+        }
+        String auditStatus = switch (status) {
+            case "审核通过", "已处理" -> "已通过";
+            case "审核不通过", "已驳回", "已取消" -> "审核不通过";
+            case "待审核", "待处理" -> "待审核";
+            default -> null;
+        };
+        if (auditStatus == null) {
+            return;
+        }
+        childInformationMapper.update(null, new UpdateWrapper<ChildInformation>()
+                .set("audit_status", auditStatus)
+                .set("reject_reason", "审核不通过".equals(auditStatus) ? remark : null)
+                .eq("id", application.getChildId())
+                .eq("talent_id", application.getTalentId())
+                .eq("deleted", 0));
+    }
+
+    private ServiceApplication application(Long id) {
+        if (id == null || id <= 0) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "预约申请ID不能为空");
+        }
+        ServiceApplication application = applicationMapper.selectOne(new QueryWrapper<ServiceApplication>()
+                .eq("id", id)
+                .eq("deleted", 0)
+                .last("limit 1"));
+        if (application == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "预约申请不存在");
+        }
+        return application;
+    }
+
+    private String normalizeStatus(String raw, String applicationType) {
+        String status = clean(raw);
+        boolean child = ServiceAppointmentApplicationService.TYPE_CHILD.equals(applicationType);
+        return switch (status) {
+            case "待处理", "处理中", "已处理", "已驳回", "已取消", "待审核", "审核通过", "审核不通过" -> status;
+            case "通过", "已通过", "完成", "处理完成" -> child ? "审核通过" : "已处理";
+            case "拒绝", "驳回", "不通过", "审核拒绝" -> child ? "审核不通过" : "已驳回";
+            default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "处理状态不正确");
+        };
+    }
+
+    private String normalizeFilterType(String raw) {
+        String value = clean(raw).toUpperCase(Locale.ROOT).replace('-', '_');
+        if (value.isBlank()) {
+            return "";
+        }
+        return switch (value) {
+            case "MEDICAL", "MEDICAL_CHECKUP", "MEDICAL_RESERVATION", "PHYSICAL_EXAM", "HEALTH_CHECK" -> ServiceAppointmentApplicationService.TYPE_MEDICAL;
+            case "FAST_TRACK", "QUICK_CHANNEL", "GREEN_CHANNEL", "MEDICAL_APPLICATION", "HOSPITAL", "HOSPITAL_APPOINTMENT" -> ServiceAppointmentApplicationService.TYPE_FAST_TRACK;
+            case "HOTEL", "HOTEL_RESERVATION", "HOTEL_BOOKING" -> ServiceAppointmentApplicationService.TYPE_HOTEL;
+            case "SPOUSE", "SPOUSE_EMPLOYMENT", "EMPLOYMENT" -> ServiceAppointmentApplicationService.TYPE_SPOUSE;
+            case "CHILD", "CHILDREN", "CHILD_ENTRUSTMENT", "CHILDREN_ENTRUSTMENT", "STUDENT_CARE", "STUDENT_ENTRUSTMENT" -> ServiceAppointmentApplicationService.TYPE_CHILD;
+            default -> raw;
+        };
+    }
+
+    private String typeName(String type) {
+        return switch (type) {
+            case ServiceAppointmentApplicationService.TYPE_MEDICAL -> "体检预约";
+            case ServiceAppointmentApplicationService.TYPE_FAST_TRACK -> "医院快捷通道";
+            case ServiceAppointmentApplicationService.TYPE_HOTEL -> "酒店预约";
+            case ServiceAppointmentApplicationService.TYPE_SPOUSE -> "配偶推荐就业";
+            case ServiceAppointmentApplicationService.TYPE_CHILD -> "学生托管";
+            default -> "预约申请";
+        };
+    }
+
+    private String serviceName(ServiceApplication application) {
+        return first(application.getHospitalName(), application.getMerchantName(), application.getParkingLot(), "人才服务机构");
+    }
+
+    private String appointmentContent(ServiceApplication application) {
+        return first(application.getProductName(), application.getPackageName(), typeName(application.getApplicationType()));
+    }
+
+    private String participantName(ServiceApplication application) {
+        return first(application.getApplicantName(), application.getSpouseName(), application.getChildName(), application.getTalentName());
+    }
+
+    private List<String> proofFileIds(String value) {
+        if (value == null || value.isBlank()) {
+            return List.of();
+        }
+        List<String> result = new ArrayList<>();
+        for (String item : value.split(",")) {
+            String cleaned = clean(item);
+            if (!cleaned.isBlank()) {
+                result.add(cleaned);
+            }
+        }
+        return result;
+    }
+
+    private LocalDate localDate(String value, String message) {
+        String text = clean(value);
+        if (text.isBlank()) {
+            return null;
+        }
+        try {
+            return LocalDate.parse(text);
+        } catch (DateTimeParseException exception) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, message);
+        }
+    }
+
+    private CellStyle headerStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        return style;
+    }
+
+    private String first(String... values) {
+        for (String value : values) {
+            String text = clean(value);
+            if (!text.isBlank()) {
+                return text;
+            }
+        }
+        return "";
+    }
+
+    private String clean(String value) {
+        return value == null ? "" : value.trim();
+    }
+}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java b/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java
new file mode 100644
index 0000000..82a1972
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java
@@ -0,0 +1,583 @@
+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.mapper.ChildInformationMapper;
+import com.talentcard.server.mapper.ServiceApplicationMapper;
+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 SensitiveDataCrypto sensitiveDataCrypto;
+
+    public ServiceAppointmentApplicationService(
+            ServiceApplicationMapper applicationMapper,
+            ChildInformationMapper childInformationMapper,
+            SensitiveDataCrypto sensitiveDataCrypto
+    ) {
+        this.applicationMapper = applicationMapper;
+        this.childInformationMapper = childInformationMapper;
+        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, "预约申请保存失败");
+        }
+        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());
+        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);
+        }
+        if (expectedType != null && !expectedType.equalsIgnoreCase(text(merchant, "merchantType", "merchant_type"))) {
+            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();
+    }
+}
\ No newline at end of file

--
Gitblit v1.7.1