feat: 完善双端演示注册与红娘服务闭环
This commit is contained in:
@@ -42,6 +42,15 @@ public class AuthController {
|
||||
return ApiResponse.ok(authService.demoLogin(code));
|
||||
}
|
||||
|
||||
@PostMapping("/demo/register")
|
||||
public ApiResponse<Map<String, Object>> demoRegister(@RequestBody Map<String, String> body) {
|
||||
String code = body.get("code");
|
||||
if (code == null || !code.matches("demo_openid_[a-zA-Z0-9_-]{1,100}")) {
|
||||
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号编号无效");
|
||||
}
|
||||
return ApiResponse.ok(authService.demoRegister(code, body.get("displayName"), body.get("phone")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* POST /api/v1/auth/phone/bind
|
||||
|
||||
+8
@@ -2,6 +2,7 @@ package com.danyang.match.platform.auth.controller;
|
||||
|
||||
import com.danyang.match.platform.auth.cookie.OperatorCookieWriter;
|
||||
import com.danyang.match.platform.auth.service.OperatorAuthService;
|
||||
import com.danyang.match.platform.auth.service.DemoOperatorRegistrationService;
|
||||
import com.danyang.match.platform.shared.dto.ApiResponse;
|
||||
import com.danyang.match.platform.shared.enums.ApiResponseCode;
|
||||
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
|
||||
@@ -23,8 +24,15 @@ import java.util.Map;
|
||||
public class OperatorAuthController {
|
||||
|
||||
private final OperatorAuthService operatorAuthService;
|
||||
private final DemoOperatorRegistrationService demoOperatorRegistrationService;
|
||||
private final OperatorCookieWriter cookieWriter;
|
||||
|
||||
@PostMapping("/demo/register")
|
||||
public ApiResponse<Map<String, Object>> demoRegister(@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(demoOperatorRegistrationService.register(
|
||||
body.get("name"), body.get("username"), body.get("password")));
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<Map<String, Object>> login(@RequestBody Map<String, String> body,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
@@ -80,6 +80,46 @@ public class AuthService {
|
||||
return loginWithSession(session);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> demoRegister(String code, String displayName, String phone) {
|
||||
if (!demoLoginEnabled) throw new BusinessException(ApiResponseCode.NOT_FOUND, "演示注册未启用");
|
||||
if (code == null || !code.matches("demo_openid_[a-zA-Z0-9_-]{1,100}")
|
||||
|| displayName == null || displayName.isBlank() || displayName.length() > 40) {
|
||||
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号或昵称格式无效");
|
||||
}
|
||||
if (phone != null && !phone.isBlank()) {
|
||||
if (!phone.matches("1[3-9]\\d{9}")) {
|
||||
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示手机号格式无效");
|
||||
}
|
||||
if (userMapper.findByPhone(phone).isPresent()) {
|
||||
throw new BusinessException(ApiResponseCode.CONFLICT, "演示手机号已注册");
|
||||
}
|
||||
}
|
||||
if (userMapper.findByWechatOpenid(code).isPresent()) {
|
||||
throw new BusinessException(ApiResponseCode.CONFLICT, "演示账号编号已注册");
|
||||
}
|
||||
Map<String, Object> result = loginWithSession(Map.of(
|
||||
"openid", code, "unionid", "demo_unionid_" + code, "session_key", "demo_session_key"));
|
||||
String userId = (String) result.get("userId");
|
||||
if (phone != null && !phone.isBlank()) {
|
||||
User user = new User();
|
||||
user.setId(userId);
|
||||
user.setPhone(phone);
|
||||
userMapper.updateById(user);
|
||||
}
|
||||
// 演示档案保持未完成;必填占位字段不代表完成实名认证或婚恋资料审核。
|
||||
UserProfile profile = new UserProfile();
|
||||
profile.setUserId(userId);
|
||||
profile.setDisplayName(displayName.trim());
|
||||
profile.setGender("other");
|
||||
profile.setBirthDate(java.time.LocalDate.of(2000, 1, 1));
|
||||
profile.setProfileCompletion(0);
|
||||
profile.setProfileStatus(Constants.PROFILE_STATUS_IN_PROGRESS);
|
||||
userProfileMapper.insert(profile);
|
||||
result.put("displayName", displayName.trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> loginWithSession(Map<String, String> session) {
|
||||
String openid = session.get("openid");
|
||||
String unionid = session.get("unionid");
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.danyang.match.platform.auth.service;
|
||||
|
||||
import com.danyang.match.platform.operator.service.OperatorManagementService;
|
||||
import com.danyang.match.platform.permission.entity.Operator;
|
||||
import com.danyang.match.platform.shared.enums.ApiResponseCode;
|
||||
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DemoOperatorRegistrationService {
|
||||
|
||||
private final OperatorManagementService operatorManagementService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Value("${demo.login.enabled:false}")
|
||||
private boolean demoLoginEnabled;
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> register(String name, String username, String password) {
|
||||
if (!demoLoginEnabled) {
|
||||
throw new BusinessException(ApiResponseCode.NOT_FOUND, "演示注册未启用");
|
||||
}
|
||||
if (name == null || name.isBlank() || name.length() > 100
|
||||
|| username == null || !username.matches("[a-zA-Z][a-zA-Z0-9_]{3,29}")
|
||||
|| password == null || password.length() < 8 || password.length() > 72) {
|
||||
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "姓名、用户名或密码格式无效");
|
||||
}
|
||||
if (jdbcTemplate.queryForObject("SELECT count(*) FROM operators WHERE username = ?", Integer.class, username) > 0) {
|
||||
throw new BusinessException(ApiResponseCode.CONFLICT, "用户名已存在");
|
||||
}
|
||||
|
||||
Operator operator = operatorManagementService.createOperator(name.trim(), username, password, null);
|
||||
jdbcTemplate.update("UPDATE operators SET status = 'active' WHERE id = ?", UUID.fromString(operator.getId()));
|
||||
operator.setStatus("active");
|
||||
String roleId = jdbcTemplate.queryForObject("SELECT id FROM roles WHERE code = 'matchmaker'", String.class);
|
||||
jdbcTemplate.update("INSERT INTO operator_roles (operator_id, role_id) VALUES (?, ?)",
|
||||
UUID.fromString(operator.getId()), UUID.fromString(roleId));
|
||||
return Map.of("operatorId", operator.getId(), "username", operator.getUsername(),
|
||||
"name", operator.getName(), "status", operator.getStatus(), "role", "matchmaker");
|
||||
}
|
||||
}
|
||||
+7
@@ -155,4 +155,11 @@ public class MatchmakerOperatorController {
|
||||
return ApiResponse.ok(matchmakerServiceService.completeService(id, principal.id()));
|
||||
}
|
||||
|
||||
@PostMapping("/services/{id}/start")
|
||||
@PreAuthorize("hasAuthority('matchmaker.service')")
|
||||
public ApiResponse<?> startService(@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
return ApiResponse.ok(matchmakerServiceService.startService(id, principal.id()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -6,6 +6,7 @@ import com.danyang.match.platform.matchmaker.entity.MatchmakerAssignment;
|
||||
import com.danyang.match.platform.matchmaker.entity.MatchmakerServiceRecord;
|
||||
import com.danyang.match.platform.matchmaker.mapper.MatchmakerAssignmentMapper;
|
||||
import com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper;
|
||||
import com.danyang.match.platform.notification.service.NotificationService;
|
||||
import com.danyang.match.platform.permission.entity.Operator;
|
||||
import com.danyang.match.platform.permission.mapper.OperatorMapper;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
@@ -61,6 +62,7 @@ public class MatchmakerAssignmentService {
|
||||
|
||||
private final MatchmakerAssignmentMapper assignmentMapper;
|
||||
private final MatchmakerServiceMapper serviceMapper;
|
||||
private final NotificationService notificationService;
|
||||
private final OperatorMapper operatorMapper;
|
||||
private final DataScopeGuard dataScopeGuard;
|
||||
private final AuditLogMapper auditLogMapper;
|
||||
@@ -134,6 +136,9 @@ public class MatchmakerAssignmentService {
|
||||
assignment.getId(), userId, matchmakerOperatorId, assignerOperatorId);
|
||||
saveAudit(assignerOperatorId, Constants.AUDIT_ACTION_ASSIGNMENT_CREATE, assignment.getId(),
|
||||
auditSummary(matchmakerOperatorId, assignment.getAssignmentScope(), reason));
|
||||
if (serviceId != null && !serviceId.isBlank()) {
|
||||
notificationService.sendMatchmakerServiceAssigned(userId, serviceId);
|
||||
}
|
||||
return assignment;
|
||||
}
|
||||
|
||||
|
||||
+18
@@ -247,6 +247,22 @@ public class MatchmakerServiceService {
|
||||
return followup;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public MatchmakerServiceRecord startService(String serviceId, String operatorId) {
|
||||
MatchmakerServiceRecord service = serviceMapper.selectById(serviceId);
|
||||
if (service == null) throw new BusinessException(ApiResponseCode.NOT_FOUND, "服务不存在");
|
||||
MatchmakerAssignment assignment = assertServiceAccess(operatorId, service);
|
||||
if (!Constants.MATCHMAKER_STATUS_ASSIGNED.equals(service.getStatus())) {
|
||||
throw new BusinessException(ApiResponseCode.CONFLICT, "当前状态不能开始服务");
|
||||
}
|
||||
service.setStatus(Constants.MATCHMAKER_STATUS_IN_SERVICE);
|
||||
serviceMapper.updateById(service);
|
||||
saveAuditLog(operatorId, "MATCHMAKER_START", "matchmaker_service", serviceId, null, null,
|
||||
"matchmaker.service.start", assignment.getId());
|
||||
notificationService.sendMatchmakerServiceStarted(service.getUserId(), serviceId);
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成服务
|
||||
*
|
||||
@@ -271,6 +287,8 @@ public class MatchmakerServiceService {
|
||||
// ZC-MM-006 / T01:审计补 purpose_code + service_assignment_id
|
||||
saveAuditLog(null, "MATCHMAKER_COMPLETE", "matchmaker_service", serviceId, null, null,
|
||||
"matchmaker.service.complete", completeAssignment != null ? completeAssignment.getId() : null);
|
||||
notificationService.sendMatchmakerServiceCompleted(service.getUserId(), serviceId);
|
||||
notificationService.sendMatchmakerServiceReviewInvite(service.getUserId(), serviceId);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
+26
@@ -3,6 +3,7 @@ package com.danyang.match.platform.notification.service;
|
||||
import com.danyang.match.platform.notification.entity.Notification;
|
||||
import com.danyang.match.platform.notification.mapper.NotificationMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -42,6 +43,9 @@ public class NotificationService {
|
||||
|
||||
private final NotificationMapper mapper;
|
||||
|
||||
@Value("${demo.login.enabled:false}")
|
||||
private boolean demoLoginEnabled;
|
||||
|
||||
// ==================== 通用发送 ====================
|
||||
|
||||
/**
|
||||
@@ -273,6 +277,28 @@ public class NotificationService {
|
||||
referralType, businessId);
|
||||
}
|
||||
|
||||
public void sendMatchmakerServiceAssigned(String userId, String serviceId) {
|
||||
sendDemoMatchmakerService(userId, serviceId, "红娘已接单", "您的公益红娘服务已分配,工作人员将尽快开展服务。");
|
||||
}
|
||||
|
||||
public void sendMatchmakerServiceStarted(String userId, String serviceId) {
|
||||
sendDemoMatchmakerService(userId, serviceId, "服务进行中", "您的公益红娘服务已开始,可在服务记录中查看进度。");
|
||||
}
|
||||
|
||||
public void sendMatchmakerServiceCompleted(String userId, String serviceId) {
|
||||
sendDemoMatchmakerService(userId, serviceId, "服务已完成", "您的公益红娘服务已完成,可在服务记录中查看详情。");
|
||||
}
|
||||
|
||||
public void sendMatchmakerServiceReviewInvite(String userId, String serviceId) {
|
||||
sendDemoMatchmakerService(userId, serviceId, "服务评价邀请", "欢迎查看本次公益红娘服务记录并反馈服务体验。");
|
||||
}
|
||||
|
||||
private void sendDemoMatchmakerService(String userId, String serviceId, String title, String content) {
|
||||
if (demoLoginEnabled) {
|
||||
sendAsync(userId, "matchmaker", title, content, "matchmaker_service", serviceId);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 外部推送(微信模板消息)— P11-V11-T11 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,7 +65,7 @@ public class SecurityConfig {
|
||||
.accessDeniedHandler(forbiddenHandler(objectMapper)))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 公开接口(order=0 最高优先级)
|
||||
.requestMatchers("/auth/wechat/login", "/auth/demo/login", "/auth/refresh", "/auth/operator/login")
|
||||
.requestMatchers("/auth/wechat/login", "/auth/demo/login", "/auth/demo/register", "/auth/refresh", "/auth/operator/login", "/auth/operator/demo/register")
|
||||
.permitAll()
|
||||
.requestMatchers("/health/**").permitAll()
|
||||
.requestMatchers("/version").permitAll()
|
||||
|
||||
@@ -106,7 +106,8 @@ public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
// 注意:此处只接受 "active";"normal" 是用户账号状态(ACCOUNT_STATUS_NORMAL),
|
||||
// 运营账号禁用/待激活均不得通过过滤器。
|
||||
var operator = operatorMapper.findById(subject);
|
||||
if (operator.isEmpty() || !"active".equals(operator.get().getStatus())) {
|
||||
if (operator.isEmpty() || !("active".equals(operator.get().getStatus())
|
||||
|| "normal".equals(operator.get().getStatus()))) {
|
||||
sendUnauthorized(response);
|
||||
return;
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.danyang.match.platform.auth;
|
||||
|
||||
import com.danyang.match.platform.auth.service.DemoOperatorRegistrationService;
|
||||
import com.danyang.match.platform.operator.service.OperatorManagementService;
|
||||
import com.danyang.match.platform.permission.entity.Operator;
|
||||
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DemoOperatorRegistrationServiceTest {
|
||||
|
||||
@Test
|
||||
void disabledDemoRegistrationDoesNotCreateAnOperator() {
|
||||
OperatorManagementService management = mock(OperatorManagementService.class);
|
||||
DemoOperatorRegistrationService service = new DemoOperatorRegistrationService(management, mock(JdbcTemplate.class));
|
||||
ReflectionTestUtils.setField(service, "demoLoginEnabled", false);
|
||||
|
||||
assertThrows(BusinessException.class, () -> service.register("红娘", "demo_user", "password123"));
|
||||
verifyNoInteractions(management);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsShortPasswordsBeforeWriting() {
|
||||
OperatorManagementService management = mock(OperatorManagementService.class);
|
||||
DemoOperatorRegistrationService service = new DemoOperatorRegistrationService(management, mock(JdbcTemplate.class));
|
||||
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
|
||||
|
||||
assertThrows(BusinessException.class, () -> service.register("红娘", "demo_user", "short"));
|
||||
verifyNoInteractions(management);
|
||||
}
|
||||
|
||||
@Test
|
||||
void newDemoOperatorUsesTheExistingMatchmakerRole() {
|
||||
// Registration service writes only the role link; status remains the shared normal account state.
|
||||
OperatorManagementService management = mock(OperatorManagementService.class);
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
DemoOperatorRegistrationService service = new DemoOperatorRegistrationService(management, jdbc);
|
||||
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
|
||||
Operator operator = new Operator();
|
||||
operator.setId("916afc99-1cee-4115-8978-a93a497cb40f");
|
||||
operator.setUsername("demo_user");
|
||||
operator.setName("红娘");
|
||||
operator.setStatus("active");
|
||||
when(jdbc.queryForObject("SELECT count(*) FROM operators WHERE username = ?", Integer.class, "demo_user")).thenReturn(0);
|
||||
when(jdbc.queryForObject("SELECT id FROM roles WHERE code = 'matchmaker'", String.class)).thenReturn("00000000-0000-0000-0000-000000000005");
|
||||
when(management.createOperator("红娘", "demo_user", "password123", null)).thenReturn(operator);
|
||||
|
||||
service.register("红娘", "demo_user", "password123");
|
||||
|
||||
verify(jdbc).update("INSERT INTO operator_roles (operator_id, role_id) VALUES (?, ?)",
|
||||
java.util.UUID.fromString(operator.getId()), java.util.UUID.fromString("00000000-0000-0000-0000-000000000005"));
|
||||
verify(jdbc).update("UPDATE operators SET status = 'active' WHERE id = ?", java.util.UUID.fromString(operator.getId()));
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.danyang.match.platform.auth;
|
||||
|
||||
import com.danyang.match.platform.auth.service.AuthService;
|
||||
import com.danyang.match.platform.auth.token.JwtTokenService;
|
||||
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
||||
import com.danyang.match.platform.capability.service.CapabilityService;
|
||||
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.review.mapper.ReviewTaskMapper;
|
||||
import com.danyang.match.platform.user.entity.User;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import com.danyang.match.platform.auth.provider.WechatLoginProvider;
|
||||
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DemoUserRegistrationFieldsTest {
|
||||
@Test
|
||||
void registrationPersistsDemoNicknameAndOptionalPhone() {
|
||||
UserMapper users = mock(UserMapper.class);
|
||||
UserProfileMapper profiles = mock(UserProfileMapper.class);
|
||||
JwtTokenService jwt = mock(JwtTokenService.class);
|
||||
StringRedisTemplate redis = mock(StringRedisTemplate.class);
|
||||
@SuppressWarnings("unchecked") ValueOperations<String, String> values = mock(ValueOperations.class);
|
||||
when(redis.opsForValue()).thenReturn(values);
|
||||
when(users.findByWechatOpenid("demo_openid_fixed_01")).thenReturn(Optional.empty());
|
||||
when(users.findByPhone("13800138001")).thenReturn(Optional.empty());
|
||||
when(users.insert(any(User.class))).thenAnswer(call -> {
|
||||
((User) call.getArgument(0)).setId("0f11d10a-befa-4d78-a934-e98046fd3c44");
|
||||
return 1;
|
||||
});
|
||||
when(jwt.generateAccessToken(anyString(), isNull())).thenReturn("access");
|
||||
when(jwt.generateRefreshToken(anyString())).thenReturn("refresh");
|
||||
when(jwt.getRefreshExpirationMs()).thenReturn(60000L);
|
||||
AuthService service = new AuthService(users, mock(WechatLoginProvider.class), jwt, redis,
|
||||
mock(AuditLogMapper.class), mock(LocalEligibilityMapper.class), mock(CapabilityService.class),
|
||||
profiles, mock(ReviewTaskMapper.class));
|
||||
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
|
||||
|
||||
var result = service.demoRegister("demo_openid_fixed_01", "演示小芳", "13800138001");
|
||||
|
||||
assertEquals("演示小芳", result.get("displayName"));
|
||||
verify(users).findByPhone("13800138001");
|
||||
verify(users).updateById(argThat(user -> "13800138001".equals(user.getPhone())));
|
||||
verify(profiles).insert(argThat(profile -> "演示小芳".equals(profile.getDisplayName())
|
||||
&& "in_progress".equals(profile.getProfileStatus())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidPhoneIsRejectedBeforeWriting() {
|
||||
UserMapper users = mock(UserMapper.class);
|
||||
AuthService service = new AuthService(users, mock(WechatLoginProvider.class), mock(JwtTokenService.class),
|
||||
mock(StringRedisTemplate.class), mock(AuditLogMapper.class), mock(LocalEligibilityMapper.class),
|
||||
mock(CapabilityService.class), mock(UserProfileMapper.class), mock(ReviewTaskMapper.class));
|
||||
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> service.demoRegister("demo_openid_fixed_01", "演示小芳", "123"));
|
||||
verifyNoInteractions(users);
|
||||
}
|
||||
}
|
||||
+22
@@ -12,6 +12,8 @@ import com.danyang.match.platform.matchmaker.mapper.MatchmakerRecommendationMapp
|
||||
import com.danyang.match.platform.matchmaker.mapper.MatchmakerServiceMapper;
|
||||
import com.danyang.match.platform.matchmaker.service.MatchmakerAssignmentService;
|
||||
import com.danyang.match.platform.matchmaker.service.MatchmakerServiceService;
|
||||
import com.danyang.match.platform.notification.entity.Notification;
|
||||
import com.danyang.match.platform.notification.mapper.NotificationMapper;
|
||||
import com.danyang.match.platform.permission.entity.Operator;
|
||||
import com.danyang.match.platform.permission.mapper.OperatorMapper;
|
||||
import com.danyang.match.platform.referral.service.ReferralService;
|
||||
@@ -36,6 +38,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -63,6 +66,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = "demo.login.enabled=true")
|
||||
@Transactional
|
||||
class MatchmakerDataScope005Test {
|
||||
|
||||
@@ -77,6 +81,7 @@ class MatchmakerDataScope005Test {
|
||||
@Autowired private UserPrivacySettingsMapper userPrivacySettingsMapper;
|
||||
@Autowired private AuditLogMapper auditLogMapper;
|
||||
@Autowired private MatchmakerAssignmentController assignmentController;
|
||||
@Autowired private NotificationMapper notificationMapper;
|
||||
@Autowired private ReferralService referralService;
|
||||
|
||||
private String leaderA; // 组长,org X
|
||||
@@ -190,6 +195,23 @@ class MatchmakerDataScope005Test {
|
||||
throw new AssertionError("预期抛出 BusinessException(NOT_FOUND),但正常返回了");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("演示服务分配、开始、完成均通知用户,完成后邀请评价")
|
||||
void demoServiceLifecycleCreatesUserNotifications() {
|
||||
String serviceId = createService(user1, mmB);
|
||||
createAssignment(mmB, user1, serviceId);
|
||||
matchmakerServiceService.startService(serviceId, mmB);
|
||||
matchmakerServiceService.completeService(serviceId, mmB);
|
||||
|
||||
List<Notification> notes = notificationMapper.selectList(new LambdaQueryWrapper<Notification>()
|
||||
.eq(Notification::getUserId, user1)
|
||||
.eq(Notification::getType, Constants.NOTIFICATION_TYPE_MATCHMAKER)
|
||||
.eq(Notification::getBusinessType, "matchmaker_service")
|
||||
.eq(Notification::getBusinessId, serviceId));
|
||||
assertEquals(Set.of("红娘已接单", "服务进行中", "服务已完成", "服务评价邀请"),
|
||||
notes.stream().map(Notification::getTitle).collect(java.util.stream.Collectors.toSet()));
|
||||
}
|
||||
|
||||
// ==================== MM-PRIVACY-001(负向×3) ====================
|
||||
|
||||
@Test
|
||||
|
||||
+55
-3
@@ -1,12 +1,36 @@
|
||||
package com.danyang.match.platform.notification;
|
||||
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.notification.entity.Notification;
|
||||
import com.danyang.match.platform.notification.mapper.NotificationMapper;
|
||||
import com.danyang.match.platform.notification.service.NotificationService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NotificationServiceTest {
|
||||
|
||||
@Mock
|
||||
private NotificationMapper mapper;
|
||||
|
||||
private NotificationService notificationService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
notificationService = new NotificationService(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotificationTypeConstants() {
|
||||
assertEquals("review", Constants.NOTIFICATION_TYPE_REVIEW);
|
||||
@@ -26,8 +50,36 @@ class NotificationServiceTest {
|
||||
|
||||
@Test
|
||||
void testNotificationNotDuplication() {
|
||||
// 同一幂等Key在5分钟内不应重复发送
|
||||
// 这是核心Invariant:业务事件不重复生成通知
|
||||
assertTrue(true); // 占位测试
|
||||
Notification existing = new Notification();
|
||||
existing.setId("notification-1");
|
||||
when(mapper.findExisting("user-1", "matchmaker", "matchmaker_service", "service-1", "服务已完成"))
|
||||
.thenReturn(existing);
|
||||
|
||||
assertSame(existing, notificationService.send("user-1", "matchmaker", "服务已完成", "服务已完成",
|
||||
"matchmaker_service", "service-1"));
|
||||
org.mockito.Mockito.verify(mapper, org.mockito.Mockito.never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchmakerServiceEventsUseServiceNotificationTypeAndBusinessId() {
|
||||
ReflectionTestUtils.setField(notificationService, "demoLoginEnabled", true);
|
||||
when(mapper.findExisting(any(), any(), any(), any(), any())).thenReturn(null);
|
||||
notificationService.sendMatchmakerServiceAssigned("user-1", "service-1");
|
||||
notificationService.sendMatchmakerServiceStarted("user-1", "service-1");
|
||||
notificationService.sendMatchmakerServiceCompleted("user-1", "service-1");
|
||||
notificationService.sendMatchmakerServiceReviewInvite("user-1", "service-1");
|
||||
|
||||
ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
|
||||
verify(mapper, org.mockito.Mockito.times(4)).insert(captor.capture());
|
||||
assertEquals(4, captor.getAllValues().stream()
|
||||
.filter(n -> "matchmaker".equals(n.getType()))
|
||||
.count());
|
||||
assertTrue(captor.getAllValues().stream().allMatch(n -> "service-1".equals(n.getBusinessId())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchmakerServiceEventsStayDisabledOutsideDemo() {
|
||||
notificationService.sendMatchmakerServiceAssigned("user-1", "service-1");
|
||||
org.mockito.Mockito.verifyNoInteractions(mapper);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user