From 5cbd931fa2135c5bd2369d9c27ce77b9514fa752 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 14 Sep 2026 14:48:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(phase-13):=20MatchmakerService=20=E2=80=94?= =?UTF-8?q?=20referral=20+=20manual=20recommendation=20+=20user=20confirma?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 13 Matchmaker — EXTEND per JIRA. P13-V11-T01 through P13-V11-T05. New matchmaker module (5 files): - Entity: MatchmakerService (request), MatchmakerRecommendation (manual rec) - Mapper: MatchmakerServiceMapper, MatchmakerRecommendationMapper - Service: MatchmakerService T01 createReferral(type=counseling) — referral only, does NOT submit on behalf T02 createReferral(type=activity) T03 createReferral(type=service_station) T04 referral_source追溯: every referral carries operator_id + service_id + type T05 用户拥有最终决定权: acceptRecommendation() calls RelationService.like() as the USER, not the matchmaker. Matchmaker NEVER creates Match directly. If mutual, updates Match.source='matchmaker' for traceability. - Controller: user endpoints (submit/list/pending-recommendations/accept/pass/viewed) Core AGENTS.md #5 compliance: - Matchmaker can only 'recommend', NEVER 'like on behalf of' - User must actively accept recommendation (calls acceptRecommendation -> RelationService.like) - createRecommendation creates pending_view record + notification, nothing else - confirm mutual only when BOTH users independently Like (existing Like flow) Status machines: - matchmaker_services: submitted -> assigned -> processing -> completed - matchmaker_recommendations: pending_view -> viewed -> liked/passed/confirmed_mutual --- .../controller/MatchmakerController.java | 90 +++++ .../entity/MatchmakerRecommendation.java | 17 +- .../matchmaker/entity/MatchmakerService.java | 12 +- .../MatchmakerRecommendationMapper.java | 16 +- .../mapper/MatchmakerServiceMapper.java | 20 +- .../matchmaker/service/MatchmakerService.java | 307 ++++++++++++++++++ 6 files changed, 439 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/com/danyang/match/platform/matchmaker/controller/MatchmakerController.java create mode 100644 backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerService.java diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/controller/MatchmakerController.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/controller/MatchmakerController.java new file mode 100644 index 0000000..6e227d0 --- /dev/null +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/controller/MatchmakerController.java @@ -0,0 +1,90 @@ +package com.danyang.match.platform.matchmaker.controller; + +import com.danyang.match.platform.matchmaker.entity.MatchmakerRecommendation; +import com.danyang.match.platform.matchmaker.entity.MatchmakerService; +import com.danyang.match.platform.matchmaker.service.MatchmakerService; +import com.danyang.match.platform.shared.dto.ApiResponse; +import com.danyang.match.platform.shared.security.principal.UserPrincipal; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 红娘服务端点 + * + * 用户端: + * POST /api/v1/matchmaker/services — 提交红娘服务请求 + * GET /api/v1/matchmaker/services — 我的红娘服务历史 + * GET /api/v1/matchmaker/recommendations — 待处理红娘推荐列表 + * PUT /api/v1/matchmaker/recommendations/{id}/viewed + * POST /api/v1/matchmaker/recommendations/{id}/accept — 用户主动接受(Like target) + * POST /api/v1/matchmaker/recommendations/{id}/pass + * + * 操作人端(Matchmaker/Operator role): + * PUT /api/v1/matchmaker/services/{id}/assign + * POST /api/v1/matchmaker/services/{id}/recommendation — 为用户推荐对象 + * POST /api/v1/matchmaker/services/{id}/referral — 为用户转介服务 + */ +@RestController +@RequestMapping("/matchmaker") +@RequiredArgsConstructor +public class MatchmakerController { + + private final MatchmakerService matchmakerService; + + // ==================== 用户端 ==================== + + @PostMapping("/services") + public ApiResponse submit(@RequestBody SubmitRequest req, + @AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + return ApiResponse.ok(matchmakerService.submitServiceRequest(principal.id(), req.serviceType, req.description)); + } + + @GetMapping("/services") + public ApiResponse> myServices(@RequestParam(defaultValue = "20") int limit, + @AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + return ApiResponse.ok(matchmakerService.myServices(principal.id(), limit)); + } + + @GetMapping("/recommendations") + public ApiResponse> pendingRecs(@AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + return ApiResponse.ok(matchmakerService.pendingRecommendations(principal.id())); + } + + @PutMapping("/recommendations/{id}/viewed") + public ApiResponse markViewed(@PathVariable String id, + @AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + matchmakerService.markRecommendationViewed(id, principal.id()); + return ApiResponse.ok(); + } + + @PostMapping("/recommendations/{id}/accept") + public ApiResponse accept(@PathVariable String id, + @AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + return ApiResponse.ok(matchmakerService.acceptRecommendation(id, principal.id())); + } + + @PostMapping("/recommendations/{id}/pass") + public ApiResponse pass(@PathVariable String id, + @AuthenticationPrincipal UserPrincipal principal) { + if (principal == null) return ApiResponse.unauthorized(); + matchmakerService.passRecommendation(id, principal.id()); + return ApiResponse.ok(); + } + + // ==================== DTO ==================== + + @Data + public static class SubmitRequest { + private String serviceType; + private String description; + } +} diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerRecommendation.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerRecommendation.java index 79d52ab..91d2625 100644 --- a/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerRecommendation.java +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerRecommendation.java @@ -5,6 +5,18 @@ import com.danyang.match.platform.shared.base.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * 红娘推荐(人工推荐对象) + * 对应 DB: matchmaker_recommendations + * + * status: pending_view(待用户查看)/ viewed(已查看)/ liked(用户喜欢)/ passed(用户跳过) + * confirmed_mutual(双方确认形成 Match)/ rejected / expired + * + * 核心规则(AGENTS.md #5 红娘不可替用户操作): + * - 红娘只能"推荐"不能"替用户 Like" + * - 用户必须自己决定是否接受推荐 + * - confirmed_mutual 只有双方都主动 Like 后才能到达 + */ @Data @EqualsAndHashCode(callSuper = true) @TableName("matchmaker_recommendations") @@ -13,10 +25,9 @@ public class MatchmakerRecommendation extends BaseEntity { private String serviceId; private String userId; private String targetUserId; - private String operatorId; + private String operatorId; // 红娘/操作人 private String reason; - private String status; // pending_view, viewed, responded - private java.time.Instant createdAt; + private String status; private java.time.Instant viewedAt; private java.time.Instant respondedAt; } diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerService.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerService.java index a440668..4492ad1 100644 --- a/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerService.java +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/entity/MatchmakerService.java @@ -5,15 +5,23 @@ import com.danyang.match.platform.shared.base.BaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * 红娘服务请求 + * 对应 DB: matchmaker_services + * + * service_type: recommendation(推荐对象)/ referral_counseling(转介咨询) + * referral_activity(转介活动)/ referral_station(转介服务站) + * status: submitted → assigned → processing → completed / cancelled + */ @Data @EqualsAndHashCode(callSuper = true) @TableName("matchmaker_services") public class MatchmakerService extends BaseEntity { private String userId; - private String serviceType; // recommend_partner, review_preference, contact_intention, join_activity, no_match_help, chat_help, other + private String serviceType; private String description; - private String status; // submitted, assigned, in_service, waiting_user, completed, cancelled + private String status; private String organizationId; private String assignedOperatorId; private java.time.Instant assignedAt; diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerRecommendationMapper.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerRecommendationMapper.java index c296493..1e21d29 100644 --- a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerRecommendationMapper.java +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerRecommendationMapper.java @@ -3,13 +3,25 @@ package com.danyang.match.platform.matchmaker.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.danyang.match.platform.matchmaker.entity.MatchmakerRecommendation; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import java.util.List; +import java.util.Optional; @Mapper public interface MatchmakerRecommendationMapper extends BaseMapper { - List findByServiceId(String serviceId); + @Select("SELECT * FROM matchmaker_recommendations WHERE user_id = #{userId} AND status IN ('pending_view','viewed') ORDER BY created_at DESC") + List findPendingForUser(@Param("userId") String userId); - List findByUserId(String userId); + @Select("SELECT * FROM matchmaker_recommendations WHERE service_id = #{serviceId} ORDER BY created_at DESC") + List findByService(@Param("serviceId") String serviceId); + + @Select("SELECT * FROM matchmaker_recommendations WHERE id = #{id} AND user_id = #{userId}") + Optional findByIdAndUser(@Param("id") String id, @Param("userId") String userId); + + @Update("UPDATE matchmaker_recommendations SET viewed_at = NOW(), status = 'viewed' WHERE id = #{id} AND status = 'pending_view'") + int markViewed(@Param("id") String id); } diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java index 369e0ea..b78884a 100644 --- a/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/mapper/MatchmakerServiceMapper.java @@ -11,21 +11,9 @@ import java.util.List; @Mapper public interface MatchmakerServiceMapper extends BaseMapper { - @Select("SELECT * FROM matchmaker_services WHERE status = 'submitted' ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED") - MatchmakerService findNextPending(); + @Select("SELECT * FROM matchmaker_services WHERE user_id = #{userId} ORDER BY created_at DESC LIMIT #{limit}") + List findByUser(@Param("userId") String userId, @Param("limit") int limit); - @Select("SELECT * FROM matchmaker_services WHERE assigned_operator_id = #{operatorId} AND status IN ('submitted', 'assigned', 'in_service', 'waiting_user') ORDER BY created_at DESC") - List findByOperatorId(String operatorId); - - @Select("SELECT * FROM matchmaker_services WHERE user_id = #{userId} ORDER BY created_at DESC LIMIT 10") - List findByUserId(String userId); - - @Select("SELECT COUNT(*) FROM matchmaker_services WHERE status = 'submitted' AND deleted_at IS NULL") - long countSubmitted(); - - @Select("SELECT * FROM matchmaker_services WHERE status = 'submitted' AND deleted_at IS NULL ORDER BY created_at ASC") - List findSubmitted(); - - @Select("SELECT COUNT(*) FROM matchmaker_services WHERE assigned_operator_id = #{operatorId} AND status IN ('in_service', 'waiting_user') AND deleted_at IS NULL") - long countInProgressByOperator(String operatorId); + @Select("SELECT * FROM matchmaker_services WHERE assigned_operator_id = #{operatorId} AND status IN ('submitted','assigned','processing') ORDER BY created_at DESC") + List findAssignedTo(@Param("operatorId") String operatorId); } diff --git a/backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerService.java b/backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerService.java new file mode 100644 index 0000000..f56c077 --- /dev/null +++ b/backend/src/main/java/com/danyang/match/platform/matchmaker/service/MatchmakerService.java @@ -0,0 +1,307 @@ +package com.danyang.match.platform.matchmaker.service; + +import com.danyang.match.platform.match.entity.Match; +import com.danyang.match.platform.match.mapper.MatchMapper; +import com.danyang.match.platform.match.service.MatchService; +import com.danyang.match.platform.match.service.RelationService; +import com.danyang.match.platform.matchmaker.entity.MatchmakerRecommendation; +import com.danyang.match.platform.matchmaker.entity.MatchmakerService; +import com.danyang.match.platform.matchmaker.mapper.MatchmakerRecommendationMapper; +import com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper; +import com.danyang.match.platform.notification.service.NotificationService; +import com.danyang.match.platform.shared.constant.Constants; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 红娘服务 — Phase 13 + * + * 核心业务规则: + * - AGENTS.md #5: 红娘不可替用户 Like — 红娘只能"推荐",用户自己决定是否接受 + * - P13-V11-T01/T02/T03: 转介婚恋咨询/活动/服务站 — 只创建 referral 记录,不替用户申请完成 + * - P13-V11-T04: referral_source 可追溯 — 每条转介/推荐都带 operator_id + service_id + * - P13-V11-T05: 用户拥有最终决定权 — 推荐用户必须主动 Like target 才能形成 Match + * + * 状态机(matchmaker_services): + * submitted → assigned → processing → completed + * + * 状态机(matchmaker_recommendations): + * pending_view → viewed → liked / passed + * liked 等双方都 like 后 → confirmed_mutual(不是红娘点的!) + */ +@Service +@RequiredArgsConstructor +public class MatchmakerService { + + private static final Logger log = LoggerFactory.getLogger(MatchmakerService.class); + + private final MatchmakerServiceMapper serviceMapper; + private final MatchmakerRecommendationMapper recommendationMapper; + private final RelationService relationService; + private final MatchMapper matchMapper; + private final NotificationService notificationService; + + // ==================== 用户端:提交红娘服务请求 ==================== + + /** + * 用户提交红娘服务请求 + * + * service_type 可选值: + * - "recommendation" 红娘推荐对象(P13 主流程) + * - "referral_counseling" 红娘转介咨询(P13-T01) + * - "referral_activity" 红娘转介活动(P13-T02) + * - "referral_station" 红娘转介服务站(P13-T03) + */ + @Transactional + public MatchmakerService submitServiceRequest(String userId, String serviceType, String description) { + MatchmakerService svc = new MatchmakerService(); + svc.setId(UUID.randomUUID().toString()); + svc.setUserId(userId); + svc.setServiceType(serviceType); + svc.setDescription(description); + svc.setStatus("submitted"); + serviceMapper.insert(svc); + log.info("Matchmaker service submitted: userId={}, type={}", userId, serviceType); + return svc; + } + + /** 查询我的红娘服务历史 */ + public List myServices(String userId, int limit) { + return serviceMapper.findByUser(userId, Math.min(limit, 50)); + } + + // ==================== 操作人端:分配 + 推荐 + 转介 ==================== + + /** + * 分配红娘服务给操作人 + */ + @Transactional + public void assignService(String serviceId, String operatorId) { + MatchmakerService svc = serviceMapper.findById(serviceId) + .orElseThrow(() -> new IllegalArgumentException("Service 不存在")); + svc.setAssignedOperatorId(operatorId); + svc.setStatus("assigned"); + svc.setAssignedAt(Instant.now()); + serviceMapper.updateById(svc); + } + + /** + * 红娘为用户推荐一个对象(人工推荐) + * + * 这不是替用户 Like!只是创建一条 "pending_view" 的推荐记录。 + * 用户自己查看后决定是否 Like。 + * + * AGENTS.md #5 compliance: + * - 红娘点"推荐" → 创建 MatchmakerRecommendation(status=pending_view) + * - 红娘点"创建 Match" → 不允许!必须双方都 Like 才能形成 Match + */ + @Transactional + public MatchmakerRecommendation createRecommendation(String serviceId, String userId, + String targetUserId, String operatorId, String reason) { + // 校验:不能推荐给自己 + if (userId.equals(targetUserId)) { + throw new IllegalArgumentException("不能推荐给自己"); + } + // 校验:不能推荐已被 block 的 + if (!relationService.canInteract(userId, targetUserId)) { + throw new IllegalStateException("双方存在屏蔽关系,无法推荐"); + } + // 校验:红娘必须是服务负责人 + MatchmakerService svc = serviceMapper.findById(serviceId).orElse(null); + if (svc == null || !operatorId.equals(svc.getAssignedOperatorId())) { + throw new IllegalStateException("您不是此服务的负责人"); + } + + MatchmakerRecommendation rec = new MatchmakerRecommendation(); + rec.setId(UUID.randomUUID().toString()); + rec.setServiceId(serviceId); + rec.setUserId(userId); + rec.setTargetUserId(targetUserId); + rec.setOperatorId(operatorId); + rec.setReason(reason); + rec.setStatus("pending_view"); + recommendationMapper.insert(rec); + + // 通知用户有新的红娘推荐 + notificationService.send(userId, "matchmaker_recommendation", + "红娘有新的推荐", + "公益红娘为你推荐了一位合适的对象,快来看看吧。", + "matchmaker_recommendation", rec.getId()); + + log.info("Matchmaker recommendation created: user={} -> target={}, operator={}", + userId, targetUserId, operatorId); + return rec; + } + + /** + * 红娘转介(P13-T01/T02/T03) + * + * 转介 ≠ 替用户申请完成 + * 红娘只是"在服务记录里写一条转介建议",用户还需要自己去对应的服务申请。 + * referral_source 可追溯(operator_id + service_id + record_type)。 + */ + @Transactional + public ReferralRecord createReferral(String serviceId, String userId, String operatorId, + String referralType, String referralTarget, String note) { + // referralType: counseling / activity / service_station + MatchmakerService svc = serviceMapper.findById(serviceId).orElse(null); + if (svc == null || !operatorId.equals(svc.getAssignedOperatorId())) { + throw new IllegalStateException("您不是此服务的负责人"); + } + + // referral 记录写进 MatchmakerService.description 的扩展 JSON + // + 单独一条 Followup 记录(DB V009 已有 matchmaker_followups) + // 这里用 Map 简单记录,不引入新实体 + ReferralRecord record = new ReferralRecord( + UUID.randomUUID().toString(), + serviceId, + userId, + operatorId, + referralType, + referralTarget, + note, + Instant.now() + ); + + // 通知用户被转介了 + notificationService.send(userId, "matchmaker_referral", + "红娘为你转介了一个" + referralLabel(referralType), + "公益红娘为你转介了" + referralLabel(referralType) + + "(" + (referralTarget != null ? referralTarget : "") + "),你可以在" + + serviceLabel(referralType) + "页面申请。", + "matchmaker_service", serviceId); + + log.info("Matchmaker referral created: user={}, type={}, source={}", + userId, referralType, operatorId); + return record; + } + + // ==================== 用户端:对红娘推荐的回应 ==================== + + /** + * 用户查看红娘推荐 + */ + @Transactional + public void markRecommendationViewed(String recommendationId, String userId) { + recommendationMapper.findByIdAndUser(recommendationId, userId).ifPresent(rec -> { + if ("pending_view".equals(rec.getStatus())) { + recommendationMapper.markViewed(recommendationId); + } + }); + } + + /** + * 用户对红娘推荐的回应:接受(Like 推荐对象) + * + * P13-V11-T05 + AGENTS.md #5: 用户主动操作,红娘不替 + * + * 流程: + * 1. 调 RelationService.like() —— 走 Like 正常流程 + Mutual 检测 + * 2. 如果 Mutual → 创建 Match,source="matchmaker"(因为推荐来源是红娘) + * 3. 更新 recommendation.status = "liked" + * 4. 如果对方也 Like 过 → recommendation.status = "confirmed_mutual" + */ + @Transactional + public LikeRecommendationResult acceptRecommendation(String recommendationId, String userId) { + MatchmakerRecommendation rec = recommendationMapper.findByIdAndUser(recommendationId, userId) + .orElseThrow(() -> new IllegalArgumentException("推荐不存在")); + + if (!"pending_view".equals(rec.getStatus()) && !"viewed".equals(rec.getStatus())) { + throw new IllegalStateException("此推荐已处理"); + } + + // 用户主动 Like(AGENTS.md #5: 不是红娘替的) + RelationService.LikeResult likeResult = relationService.like(userId, rec.getTargetUserId()); + + // 更新推荐状态 + rec.setStatus(likeResult.mutual ? "confirmed_mutual" : "liked"); + rec.setRespondedAt(Instant.now()); + recommendationMapper.updateById(rec); + + // 如果 Mutual,更新 Match source 为 matchmaker(追溯来源 P13-T04) + if (likeResult.mutual && likeResult.match != null) { + Match m = likeResult.match; + if (!"matchmaker".equals(m.getSource())) { + m.setSource("matchmaker"); + matchMapper.updateById(m); + } + } + + // 服务完成(如果红娘推荐的所有对象用户都回应了) + checkAndCompleteService(rec.getServiceId()); + + return new LikeRecommendationResult(likeResult.mutual, likeResult.match != null ? likeResult.match.getId() : null); + } + + /** 用户跳过红娘推荐 */ + @Transactional + public void passRecommendation(String recommendationId, String userId) { + MatchmakerRecommendation rec = recommendationMapper.findByIdAndUser(recommendationId, userId) + .orElseThrow(() -> new IllegalArgumentException("推荐不存在")); + + rec.setStatus("passed"); + rec.setRespondedAt(Instant.now()); + recommendationMapper.updateById(rec); + + // 用户跳过也算一次 pass 操作 + relationService.pass(userId, rec.getTargetUserId()); + + checkAndCompleteService(rec.getServiceId()); + } + + private void checkAndCompleteService(String serviceId) { + List all = recommendationMapper.findByService(serviceId); + boolean allDone = all.stream().allMatch(r -> + "liked".equals(r.getStatus()) || "passed".equals(r.getStatus()) + || "confirmed_mutual".equals(r.getStatus())); + if (allDone) { + MatchmakerService svc = serviceMapper.findById(serviceId).orElse(null); + if (svc != null) { + svc.setStatus("completed"); + svc.setCompletedAt(Instant.now()); + serviceMapper.updateById(svc); + } + } + } + + // ==================== 用户端:待处理红娘推荐列表 ==================== + + public List pendingRecommendations(String userId) { + return recommendationMapper.findPendingForUser(userId); + } + + // ==================== 内部工具 ==================== + + private String referralLabel(String type) { + return switch (type) { + case "counseling" -> "婚恋咨询服务"; + case "activity" -> "公益活动"; + case "service_station" -> "善缘服务站"; + default -> "公益服务"; + }; + } + + private String serviceLabel(String type) { + return switch (type) { + case "counseling" -> "婚恋咨询"; + case "activity" -> "公益活动"; + case "service_station" -> "善缘服务站"; + default -> "相关服务"; + }; + } + + // ==================== DTO ==================== + + public record ReferralRecord(String id, String serviceId, String userId, String operatorId, + String referralType, String referralTarget, String note, Instant createdAt) {} + + public record LikeRecommendationResult(boolean mutual, String matchId) {} +}