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.banner.service.HomeBannerService;
|
||||||
import com.danyang.match.platform.activity.entity.Activity;
|
import com.danyang.match.platform.activity.entity.Activity;
|
||||||
import com.danyang.match.platform.activity.mapper.ActivityMapper;
|
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.content.mapper.ContentArticleMapper;
|
||||||
import com.danyang.match.platform.service_appointment.entity.ServiceLocation;
|
import com.danyang.match.platform.service_appointment.entity.ServiceLocation;
|
||||||
import com.danyang.match.platform.service_appointment.mapper.ServiceLocationMapper;
|
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.match.service.RecommendationEngineService;
|
||||||
import com.danyang.match.platform.profile.entity.UserProfile;
|
import com.danyang.match.platform.profile.entity.UserProfile;
|
||||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||||
@@ -32,6 +30,7 @@ import java.util.stream.Collectors;
|
|||||||
* 首页聚合服务
|
* 首页聚合服务
|
||||||
* 对应 GET /api/v1/home
|
* 对应 GET /api/v1/home
|
||||||
* 不复制业务规则,仅聚合各模块数据
|
* 不复制业务规则,仅聚合各模块数据
|
||||||
|
* 能力判断已委托给 CapabilityService(AGENTS.md 第 26 条)
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -48,7 +47,7 @@ public class HomeApplicationService {
|
|||||||
private final ActivityMapper activityMapper;
|
private final ActivityMapper activityMapper;
|
||||||
private final ContentArticleMapper contentArticleMapper;
|
private final ContentArticleMapper contentArticleMapper;
|
||||||
private final ServiceLocationMapper serviceLocationMapper;
|
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) {
|
private HomeDTO.Capabilities buildCapabilities(String userId) {
|
||||||
User user = userMapper.findById(userId).orElse(null);
|
return capabilityService.getCapabilities(userId);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HomeDTO.BannerItem> buildBannerItems(String userStatus) {
|
private List<HomeDTO.BannerItem> buildBannerItems(String userStatus) {
|
||||||
@@ -185,15 +166,16 @@ public class HomeApplicationService {
|
|||||||
private List<HomeDTO.QuickEntry> buildQuickEntries(HomeDTO.Capabilities caps) {
|
private List<HomeDTO.QuickEntry> buildQuickEntries(HomeDTO.Capabilities caps) {
|
||||||
// 固定8个入口,按V1.1规范顺序
|
// 固定8个入口,按V1.1规范顺序
|
||||||
List<HomeDTO.QuickEntry> entries = new ArrayList<>();
|
List<HomeDTO.QuickEntry> entries = new ArrayList<>();
|
||||||
|
CapabilityService capsSvc = this.capabilityService;
|
||||||
|
|
||||||
entries.add(entry("find_object", "找对象", "icon-dating", caps.isCanSearchUsers(), "/pages/discover/discover"));
|
entries.add(capsSvc.entry("find_object", "找对象", "icon-dating", caps.isCanSearchUsers(), "/pages/discover/discover"));
|
||||||
entries.add(entry("matchmaker", "公益红娘", "icon-matchmaker", caps.isCanApplyMatchmaker(), "/pages/matchmaker/matchmaker"));
|
entries.add(capsSvc.entry("matchmaker", "公益红娘", "icon-matchmaker", caps.isCanApplyMatchmaker(), "/pages/matchmaker/matchmaker"));
|
||||||
entries.add(entry("activities", "公益活动", "icon-activity", true, "/pages/activity/activity"));
|
entries.add(capsSvc.entry("activities", "公益活动", "icon-activity", true, "/pages/activity/activity"));
|
||||||
entries.add(entry("counseling", "婚恋咨询", "icon-counseling", caps.isCanApplyCounseling(), "/pages/counseling-intro/counseling-intro"));
|
entries.add(capsSvc.entry("counseling", "婚恋咨询", "icon-counseling", caps.isCanApplyCounseling(), "/pages/counseling-intro/counseling-intro"));
|
||||||
entries.add(entry("classroom", "婚恋课堂", "icon-class", true, "/pages/content/content"));
|
entries.add(capsSvc.entry("classroom", "婚恋课堂", "icon-class", true, "/pages/content/content"));
|
||||||
entries.add(entry("premarital", "婚前辅导", "icon-premarital", caps.isCanApplyCounseling(), "/pages/premarital-intro/premarital-intro"));
|
entries.add(capsSvc.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(capsSvc.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("service_station", "善缘服务站", "icon-location", true, "/pages/service-station/service-station"));
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
@@ -202,10 +184,10 @@ public class HomeApplicationService {
|
|||||||
if (user == null || !Boolean.TRUE.equals(user.getRecommendationEnabled())) {
|
if (user == null || !Boolean.TRUE.equals(user.getRecommendationEnabled())) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
if (!isSearching(user)) {
|
if (!capabilityService.isSearching(user)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
if (!isProfileApproved(user)) {
|
if (!capabilityService.isProfileApproved(user)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return recommendationEngineService.getDailyRecommendations(userId).stream()
|
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) {
|
private HomeDTO.ServiceRecommendation serviceRec(String type, String title, String desc, String cta, String target) {
|
||||||
HomeDTO.ServiceRecommendation sr = new HomeDTO.ServiceRecommendation();
|
HomeDTO.ServiceRecommendation sr = new HomeDTO.ServiceRecommendation();
|
||||||
sr.setType(type);
|
sr.setType(type);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import fs from 'node:fs'
|
|||||||
|
|
||||||
const read = (path) => fs.readFileSync(path, 'utf8')
|
const read = (path) => fs.readFileSync(path, 'utf8')
|
||||||
const home = read('backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java')
|
const home = read('backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java')
|
||||||
|
const caps = fs.existsSync('backend/src/main/java/com/danyang/match/platform/capability/service/CapabilityService.java')
|
||||||
|
? read('backend/src/main/java/com/danyang/match/platform/capability/service/CapabilityService.java')
|
||||||
|
: home // fallback: CapabilityService 独立化前逻辑在 HomeApplicationService 里
|
||||||
const miniapp = read('miniapp/pages/index/index.ts')
|
const miniapp = read('miniapp/pages/index/index.ts')
|
||||||
const wxml = read('miniapp/pages/index/index.wxml')
|
const wxml = read('miniapp/pages/index/index.wxml')
|
||||||
const project = JSON.parse(read('project.config.json'))
|
const project = JSON.parse(read('project.config.json'))
|
||||||
@@ -19,9 +22,9 @@ assert.match(home, /userIdentityMapper\.findByUserId/, '首页实名认证状态
|
|||||||
assert.match(home, /recommendationEngineService\.getDailyRecommendations/, '首页推荐必须来自正式推荐引擎')
|
assert.match(home, /recommendationEngineService\.getDailyRecommendations/, '首页推荐必须来自正式推荐引擎')
|
||||||
assert.match(home, /contentArticleMapper\.findPublishedAll/, '首页课堂内容必须来自内容模块')
|
assert.match(home, /contentArticleMapper\.findPublishedAll/, '首页课堂内容必须来自内容模块')
|
||||||
assert.match(home, /serviceLocationMapper\.selectOne/, '首页服务站必须来自服务站模块')
|
assert.match(home, /serviceLocationMapper\.selectOne/, '首页服务站必须来自服务站模块')
|
||||||
assert.match(home, /setDisabledReason/, '不可用入口必须返回服务端原因')
|
assert.match(caps, /setDisabledReason/, '不可用入口必须返回服务端原因')
|
||||||
assert.match(home, /isIdentityVerified/, 'Capability 必须校验真实实名状态')
|
assert.match(caps, /isIdentityVerified/, 'Capability 必须校验真实实名状态')
|
||||||
assert.match(home, /ACCOUNT_STATUS_NORMAL/, 'Capability 必须拒绝非正常账户')
|
assert.match(caps, /ACCOUNT_STATUS_NORMAL/, 'Capability 必须拒绝非正常账户')
|
||||||
assert.match(home, /\/pages\/counseling-intro\/counseling-intro/, '咨询服务建议必须跳转真实页面')
|
assert.match(home, /\/pages\/counseling-intro\/counseling-intro/, '咨询服务建议必须跳转真实页面')
|
||||||
assert.match(home, /\/pages\/premarital-intro\/premarital-intro/, '婚前辅导建议必须跳转真实页面')
|
assert.match(home, /\/pages\/premarital-intro\/premarital-intro/, '婚前辅导建议必须跳转真实页面')
|
||||||
assert.doesNotMatch(home, /"pages\//, '服务端返回的小程序路由必须以斜杠开头')
|
assert.doesNotMatch(home, /"pages\//, '服务端返回的小程序路由必须以斜杠开头')
|
||||||
|
|||||||
Reference in New Issue
Block a user