feat(capability): extract CapabilityService — AGENTS.md rule #26 compliance
New module backend/capability/: - CapabilityService: getCapabilities(entry point for frontend), plus public helpers (isAccountNormal/isIdentityVerified/isProfileApproved/ isSearching/isBaseEligible/hasLocalEligibility/entry builder) - CapabilityController: GET /api/v1/capabilities endpoint HomeApplicationService refactored: - Injects CapabilityService, delegates buildCapabilities() and QuickEntry construction + recommendation eligibility checks - Removes duplicated private methods (isSearching/isIdentityVerified/ isProfileApproved/entry) — single source of truth now in CapabilityService Contracts updated to check CapabilityService.java for capability-related assertions (setDisabledReason/isIdentityVerified/ACCOUNT_STATUS_NORMAL)
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
package com.danyang.match.platform.capability.controller;
|
||||
|
||||
import com.danyang.match.platform.capability.service.CapabilityService;
|
||||
import com.danyang.match.platform.home.dto.HomeDTO;
|
||||
import com.danyang.match.platform.shared.dto.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
|
||||
|
||||
/**
|
||||
* 用户能力开关端点
|
||||
* GET /api/v1/capabilities
|
||||
*
|
||||
* 前端各功能入口的 enabled/disabledReason 由本接口返回决定,
|
||||
* 不自行拼状态判断(AGENTS.md 第 26 条)。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/capabilities")
|
||||
@RequiredArgsConstructor
|
||||
public class CapabilityController {
|
||||
|
||||
private final CapabilityService capabilityService;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<HomeDTO.Capabilities> getCapabilities(
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
String userId = principal == null ? null : principal.id();
|
||||
if (userId == null) {
|
||||
return ApiResponse.ok(new HomeDTO.Capabilities());
|
||||
}
|
||||
return ApiResponse.ok(capabilityService.getCapabilities(userId));
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.danyang.match.platform.capability.service;
|
||||
|
||||
import com.danyang.match.platform.home.dto.HomeDTO;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
|
||||
import com.danyang.match.platform.profile.entity.UserProfile;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.user.entity.User;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import com.danyang.match.platform.verification.mapper.UserIdentityMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 用户能力判断服务 — 前端 Capability 控制入口
|
||||
*
|
||||
* AGENTS.md 第 26 条:
|
||||
* 前端功能入口可见性由服务端 CapabilityService 返回决定,
|
||||
* 前端不自行拼状态判断。
|
||||
*
|
||||
* 不可违反的业务规则:
|
||||
* - 能力判断仅基于账号状态、实名认证、资料审核、本地资格
|
||||
* - 不复制业务逻辑(Required 条件、Mutual Match 状态由各业务模块自己判断)
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CapabilityService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final UserProfileMapper userProfileMapper;
|
||||
private final UserIdentityMapper userIdentityMapper;
|
||||
private final LocalEligibilityMapper localEligibilityMapper;
|
||||
|
||||
/**
|
||||
* 获取用户的能力开关集合
|
||||
* GET /api/v1/capabilities 直接返回,首页聚合也复用这里
|
||||
*/
|
||||
public HomeDTO.Capabilities getCapabilities(String userId) {
|
||||
User user = userMapper.findById(userId).orElse(null);
|
||||
if (user == null) {
|
||||
return new HomeDTO.Capabilities();
|
||||
}
|
||||
|
||||
boolean baseEligible = Constants.ACCOUNT_STATUS_NORMAL.equals(user.getAccountStatus())
|
||||
&& isIdentityVerified(userId)
|
||||
&& isProfileApproved(user);
|
||||
|
||||
HomeDTO.Capabilities caps = new HomeDTO.Capabilities();
|
||||
caps.setCanReceiveRecommendations(baseEligible
|
||||
&& isSearching(user)
|
||||
&& Boolean.TRUE.equals(user.getRecommendationEnabled()));
|
||||
caps.setCanSearchUsers(baseEligible);
|
||||
caps.setCanLike(baseEligible);
|
||||
caps.setCanChat(false); // 由 Match 状态决定,首页层暂不展示
|
||||
caps.setCanApplyMatchmakingActivity(baseEligible);
|
||||
caps.setCanApplyCounseling(baseEligible);
|
||||
caps.setCanApplyMatchmaker(baseEligible);
|
||||
return caps;
|
||||
}
|
||||
|
||||
// ==================== 公共判断方法(供各业务模块复用) ====================
|
||||
|
||||
/** 账号是否正常 */
|
||||
public boolean isAccountNormal(User user) {
|
||||
return user != null && Constants.ACCOUNT_STATUS_NORMAL.equals(user.getAccountStatus());
|
||||
}
|
||||
|
||||
/** 是否已实名认证 */
|
||||
public boolean isIdentityVerified(String userId) {
|
||||
return userIdentityMapper.findByUserId(userId)
|
||||
.map(identity -> Constants.VERIFICATION_STATUS_VERIFIED.equals(identity.getVerificationStatus()))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/** 资料是否审核通过 */
|
||||
public boolean isProfileApproved(User user) {
|
||||
if (user == null) return false;
|
||||
String status = userProfileMapper.findByUserId(user.getId())
|
||||
.map(UserProfile::getProfileStatus).orElse(null);
|
||||
return Constants.PROFILE_STATUS_APPROVED.equals(status)
|
||||
|| Constants.PROFILE_STATUS_COMPLETED.equals(status);
|
||||
}
|
||||
|
||||
/** 是否在认真寻找中 */
|
||||
public boolean isSearching(User user) {
|
||||
return user != null && Constants.RELATIONSHIP_STATUS_SEARCHING.equals(user.getRelationshipStatus());
|
||||
}
|
||||
|
||||
/** 是否基础资格满足(正常账号 + 实名 + 资料通过) */
|
||||
public boolean isBaseEligible(User user, String userId) {
|
||||
return isAccountNormal(user) && isIdentityVerified(userId) && isProfileApproved(user);
|
||||
}
|
||||
|
||||
/** 是否有本地资格(丹阳户籍/工作/居住) */
|
||||
public boolean hasLocalEligibility(String userId) {
|
||||
return localEligibilityMapper.findByUserId(userId).isPresent();
|
||||
}
|
||||
|
||||
/** 根据能力生成 QuickEntry 的 enabled/disabledReason */
|
||||
public HomeDTO.QuickEntry entry(String code, String label, String icon,
|
||||
boolean enabled, String targetPage) {
|
||||
HomeDTO.QuickEntry e = new HomeDTO.QuickEntry();
|
||||
e.setCode(code);
|
||||
e.setLabel(label);
|
||||
e.setIcon(icon);
|
||||
e.setEnabled(enabled);
|
||||
e.setDisabledReason(enabled ? null : "完成实名认证和资料审核后可使用");
|
||||
e.setTargetPage(targetPage);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
+15
-61
@@ -5,12 +5,10 @@ import com.danyang.match.platform.banner.mapper.HomeBannerMapper;
|
||||
import com.danyang.match.platform.banner.service.HomeBannerService;
|
||||
import com.danyang.match.platform.activity.entity.Activity;
|
||||
import com.danyang.match.platform.activity.mapper.ActivityMapper;
|
||||
import com.danyang.match.platform.capability.service.CapabilityService;
|
||||
import com.danyang.match.platform.content.mapper.ContentArticleMapper;
|
||||
import com.danyang.match.platform.service_appointment.entity.ServiceLocation;
|
||||
import com.danyang.match.platform.service_appointment.mapper.ServiceLocationMapper;
|
||||
import com.danyang.match.platform.verification.mapper.UserIdentityMapper;
|
||||
import com.danyang.match.platform.local_eligibility.entity.LocalEligibility;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
|
||||
import com.danyang.match.platform.match.service.RecommendationEngineService;
|
||||
import com.danyang.match.platform.profile.entity.UserProfile;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
@@ -32,6 +30,7 @@ import java.util.stream.Collectors;
|
||||
* 首页聚合服务
|
||||
* 对应 GET /api/v1/home
|
||||
* 不复制业务规则,仅聚合各模块数据
|
||||
* 能力判断已委托给 CapabilityService(AGENTS.md 第 26 条)
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -48,7 +47,7 @@ public class HomeApplicationService {
|
||||
private final ActivityMapper activityMapper;
|
||||
private final ContentArticleMapper contentArticleMapper;
|
||||
private final ServiceLocationMapper serviceLocationMapper;
|
||||
private final UserIdentityMapper userIdentityMapper;
|
||||
private final CapabilityService capabilityService;
|
||||
|
||||
/**
|
||||
* 获取首页聚合数据
|
||||
@@ -143,25 +142,7 @@ public class HomeApplicationService {
|
||||
}
|
||||
|
||||
private HomeDTO.Capabilities buildCapabilities(String userId) {
|
||||
User user = userMapper.findById(userId).orElse(null);
|
||||
if (user == null) {
|
||||
return new HomeDTO.Capabilities();
|
||||
}
|
||||
|
||||
boolean baseEligible = Constants.ACCOUNT_STATUS_NORMAL.equals(user.getAccountStatus())
|
||||
&& isIdentityVerified(userId)
|
||||
&& isProfileApproved(user);
|
||||
HomeDTO.Capabilities caps = new HomeDTO.Capabilities();
|
||||
caps.setCanReceiveRecommendations(baseEligible
|
||||
&& isSearching(user)
|
||||
&& Boolean.TRUE.equals(user.getRecommendationEnabled()));
|
||||
caps.setCanSearchUsers(baseEligible);
|
||||
caps.setCanLike(baseEligible);
|
||||
caps.setCanChat(false); // 由Match状态决定,首页层暂不展示
|
||||
caps.setCanApplyMatchmakingActivity(baseEligible);
|
||||
caps.setCanApplyCounseling(baseEligible);
|
||||
caps.setCanApplyMatchmaker(baseEligible);
|
||||
return caps;
|
||||
return capabilityService.getCapabilities(userId);
|
||||
}
|
||||
|
||||
private List<HomeDTO.BannerItem> buildBannerItems(String userStatus) {
|
||||
@@ -185,15 +166,16 @@ public class HomeApplicationService {
|
||||
private List<HomeDTO.QuickEntry> buildQuickEntries(HomeDTO.Capabilities caps) {
|
||||
// 固定8个入口,按V1.1规范顺序
|
||||
List<HomeDTO.QuickEntry> entries = new ArrayList<>();
|
||||
CapabilityService capsSvc = this.capabilityService;
|
||||
|
||||
entries.add(entry("find_object", "找对象", "icon-dating", caps.isCanSearchUsers(), "/pages/discover/discover"));
|
||||
entries.add(entry("matchmaker", "公益红娘", "icon-matchmaker", caps.isCanApplyMatchmaker(), "/pages/matchmaker/matchmaker"));
|
||||
entries.add(entry("activities", "公益活动", "icon-activity", true, "/pages/activity/activity"));
|
||||
entries.add(entry("counseling", "婚恋咨询", "icon-counseling", caps.isCanApplyCounseling(), "/pages/counseling-intro/counseling-intro"));
|
||||
entries.add(entry("classroom", "婚恋课堂", "icon-class", true, "/pages/content/content"));
|
||||
entries.add(entry("premarital", "婚前辅导", "icon-premarital", caps.isCanApplyCounseling(), "/pages/premarital-intro/premarital-intro"));
|
||||
entries.add(entry("anti_fraud", "婚恋反诈", "icon-shield", true, "/pages/anti-fraud/anti-fraud"));
|
||||
entries.add(entry("service_station", "善缘服务站", "icon-location", true, "/pages/service-station/service-station"));
|
||||
entries.add(capsSvc.entry("find_object", "找对象", "icon-dating", caps.isCanSearchUsers(), "/pages/discover/discover"));
|
||||
entries.add(capsSvc.entry("matchmaker", "公益红娘", "icon-matchmaker", caps.isCanApplyMatchmaker(), "/pages/matchmaker/matchmaker"));
|
||||
entries.add(capsSvc.entry("activities", "公益活动", "icon-activity", true, "/pages/activity/activity"));
|
||||
entries.add(capsSvc.entry("counseling", "婚恋咨询", "icon-counseling", caps.isCanApplyCounseling(), "/pages/counseling-intro/counseling-intro"));
|
||||
entries.add(capsSvc.entry("classroom", "婚恋课堂", "icon-class", true, "/pages/content/content"));
|
||||
entries.add(capsSvc.entry("premarital", "婚前辅导", "icon-premarital", caps.isCanApplyCounseling(), "/pages/premarital-intro/premarital-intro"));
|
||||
entries.add(capsSvc.entry("anti_fraud", "婚恋反诈", "icon-shield", true, "/pages/anti-fraud/anti-fraud"));
|
||||
entries.add(capsSvc.entry("service_station", "善缘服务站", "icon-location", true, "/pages/service-station/service-station"));
|
||||
|
||||
return entries;
|
||||
}
|
||||
@@ -202,10 +184,10 @@ public class HomeApplicationService {
|
||||
if (user == null || !Boolean.TRUE.equals(user.getRecommendationEnabled())) {
|
||||
return List.of();
|
||||
}
|
||||
if (!isSearching(user)) {
|
||||
if (!capabilityService.isSearching(user)) {
|
||||
return List.of();
|
||||
}
|
||||
if (!isProfileApproved(user)) {
|
||||
if (!capabilityService.isProfileApproved(user)) {
|
||||
return List.of();
|
||||
}
|
||||
return recommendationEngineService.getDailyRecommendations(userId).stream()
|
||||
@@ -298,34 +280,6 @@ public class HomeApplicationService {
|
||||
|
||||
// ==================== 私有工具方法 ====================
|
||||
|
||||
private boolean isSearching(User user) {
|
||||
return Constants.RELATIONSHIP_STATUS_SEARCHING.equals(user.getRelationshipStatus());
|
||||
}
|
||||
|
||||
private boolean isIdentityVerified(String userId) {
|
||||
return userIdentityMapper.findByUserId(userId)
|
||||
.map(identity -> Constants.VERIFICATION_STATUS_VERIFIED.equals(identity.getVerificationStatus()))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private boolean isProfileApproved(User user) {
|
||||
String status = userProfileMapper.findByUserId(user.getId())
|
||||
.map(UserProfile::getProfileStatus).orElse(null);
|
||||
return Constants.PROFILE_STATUS_APPROVED.equals(status)
|
||||
|| Constants.PROFILE_STATUS_COMPLETED.equals(status);
|
||||
}
|
||||
|
||||
private HomeDTO.QuickEntry entry(String code, String label, String icon, boolean enabled, String targetPage) {
|
||||
HomeDTO.QuickEntry e = new HomeDTO.QuickEntry();
|
||||
e.setCode(code);
|
||||
e.setLabel(label);
|
||||
e.setIcon(icon);
|
||||
e.setEnabled(enabled);
|
||||
e.setDisabledReason(enabled ? null : "完成实名认证和资料审核后可使用");
|
||||
e.setTargetPage(targetPage);
|
||||
return e;
|
||||
}
|
||||
|
||||
private HomeDTO.ServiceRecommendation serviceRec(String type, String title, String desc, String cta, String target) {
|
||||
HomeDTO.ServiceRecommendation sr = new HomeDTO.ServiceRecommendation();
|
||||
sr.setType(type);
|
||||
|
||||
Reference in New Issue
Block a user