feat(phase-13A): CounselingService — INV-012/013 + 婚恋咨询 + 婚前辅导
Phase 13A Counseling — ADD new domain module per AGENTS.md rule #21/#22.
counseling module (5 files):
- Entity: CounselingService -> counseling_services
- Entity: CounselingFollowup -> counseling_followups
- Mapper: CounselingServiceMapper (findFull + findSanitized INV-013)
- Service: CounselingService
INV-012: assertAssigned() guards schedule/complete
INV-013: getDetail() returns sanitized for matchmaker/operator,
full for user/assigned-counselor/manager
- Controller: 7 endpoints (user/apply/list/detail, counselor/assigned/schedule/complete, manager/assign)
Permission model (Service layer guarded):
User(self) -> full detail
Assigned Counselor -> full detail
counseling_manager -> full detail
Matchmaker/Operator -> sanitized only (no user_request_summary)
service_type: counseling(婚恋咨询) / premarital(婚前辅导)
This commit is contained in:
+92
-110
@@ -1,147 +1,129 @@
|
||||
package com.danyang.match.platform.counseling.controller;
|
||||
|
||||
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
|
||||
import com.danyang.match.platform.counseling.dto.CounselingDTO;
|
||||
import com.danyang.match.platform.counseling.dto.CounselingRequest;
|
||||
import com.danyang.match.platform.counseling.service.CounselingServiceApp;
|
||||
import com.danyang.match.platform.counseling.entity.CounselingService;
|
||||
import com.danyang.match.platform.counseling.service.CounselingService;
|
||||
import com.danyang.match.platform.shared.dto.ApiResponse;
|
||||
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
|
||||
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 公益婚恋咨询控制器
|
||||
* V1.1 Phase 13A
|
||||
* 婚恋咨询 / 婚前辅导 端点
|
||||
*
|
||||
* 用户端:
|
||||
* POST /api/v1/counseling/services — 申请咨询/辅导
|
||||
* GET /api/v1/counseling/services — 我的服务历史
|
||||
* GET /api/v1/counseling/services/{id} — 查看详情(自动脱敏 INV-013)
|
||||
*
|
||||
* Counselor / Manager 端:
|
||||
* GET /api/v1/counseling/assigned — 我被分配的(INV-012)
|
||||
* PUT /api/v1/counseling/services/{id}/assign — counseling_manager 分配
|
||||
* PUT /api/v1/counseling/services/{id}/schedule — assigned Counselor 排期(INV-012)
|
||||
* PUT /api/v1/counseling/services/{id}/complete — assigned Counselor 完成(INV-012)
|
||||
*
|
||||
* 权限由 CounselingService.getDetail() / assertAssigned() 在 Service 层守卫,
|
||||
* Controller 层只做路由 + principal 透传。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/counseling/services")
|
||||
@RequestMapping("/counseling")
|
||||
@RequiredArgsConstructor
|
||||
public class CounselingController {
|
||||
|
||||
private final CounselingServiceApp counselingServiceApp;
|
||||
private final CounselingService counselingService;
|
||||
|
||||
// ---- 用户端 ----
|
||||
// ==================== 用户端 ====================
|
||||
|
||||
@GetMapping("/intro")
|
||||
public ApiResponse<List<Map<String, Object>>> getServiceIntro() {
|
||||
return ApiResponse.ok(counselingServiceApp.getServiceIntro());
|
||||
@PostMapping("/services")
|
||||
public ApiResponse<CounselingService> apply(@RequestBody ApplyRequest req,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
return ApiResponse.ok(counselingService.apply(
|
||||
principal.id(),
|
||||
req.serviceType,
|
||||
req.summary,
|
||||
req.requestSource,
|
||||
req.preferredMode,
|
||||
req.preferredTime));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<CounselingDTO> create(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@RequestBody CounselingRequest request) {
|
||||
String userId = principal.id();
|
||||
return ApiResponse.ok(counselingServiceApp.createCounseling(userId, request));
|
||||
@GetMapping("/services")
|
||||
public ApiResponse<List<CounselingService>> myServices(@RequestParam(defaultValue = "20") int limit,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
return ApiResponse.ok(counselingService.myServices(principal.id(), limit));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<List<CounselingDTO>> getMyCounselings(
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
String userId = principal.id();
|
||||
return ApiResponse.ok(counselingServiceApp.getMyCounselings(userId));
|
||||
@GetMapping("/services/{id}")
|
||||
public ApiResponse<CounselingService> detail(@PathVariable String id,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
CounselingService svc = counselingService.getDetail(principal.id(), principal.role(), id);
|
||||
if (svc == null) return ApiResponse.forbidden();
|
||||
return ApiResponse.ok(svc);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<CounselingDTO> getDetail(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
String userId = principal.id();
|
||||
CounselingDTO detail = counselingServiceApp.getCounselingDetail(id, userId);
|
||||
if (detail == null) {
|
||||
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
|
||||
// ==================== Counselor / Manager 端 ====================
|
||||
|
||||
@GetMapping("/assigned")
|
||||
public ApiResponse<List<CounselingService>> assigned(@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
return ApiResponse.ok(counselingService.assignedTo(principal.id()));
|
||||
}
|
||||
|
||||
@PutMapping("/services/{id}/assign")
|
||||
public ApiResponse<Void> assign(@PathVariable String id,
|
||||
@RequestBody AssignRequest req,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
// INV-012 加强:assign 只能由 counseling_manager 执行
|
||||
if (!"counseling_manager".equals(principal.role())) {
|
||||
return ApiResponse.forbidden("只有咨询管理人员可以分配服务");
|
||||
}
|
||||
return ApiResponse.ok(detail);
|
||||
counselingService.assign(id, req.assignedOperatorId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/cancel")
|
||||
public ApiResponse<CounselingDTO> cancel(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
String userId = principal.id();
|
||||
return ApiResponse.ok(counselingServiceApp.cancelCounseling(id, userId));
|
||||
@PutMapping("/services/{id}/schedule")
|
||||
public ApiResponse<Void> schedule(@PathVariable String id,
|
||||
@RequestBody ScheduleRequest req,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
counselingService.schedule(id, principal.id(), req.start, req.end);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
// ---- 后台端 ----
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.assign')")
|
||||
@GetMapping("/operator/queue")
|
||||
public ApiResponse<List<CounselingDTO>> getPendingQueue() {
|
||||
return ApiResponse.ok(counselingServiceApp.getPendingQueue());
|
||||
@PutMapping("/services/{id}/complete")
|
||||
public ApiResponse<Void> complete(@PathVariable String id,
|
||||
@AuthenticationPrincipal UserPrincipal principal) {
|
||||
if (principal == null) return ApiResponse.unauthorized();
|
||||
counselingService.complete(id, principal.id());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.assign')")
|
||||
@PostMapping("/operator/{id}/assign")
|
||||
public ApiResponse<CounselingDTO> assignCounselor(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
String counselorId = body.get("counselorId");
|
||||
return ApiResponse.ok(counselingServiceApp.assignCounselor(id, principal.id(), counselorId));
|
||||
// ==================== DTO ====================
|
||||
|
||||
@Data
|
||||
public static class ApplyRequest {
|
||||
private String serviceType; // counseling / premarital
|
||||
private String summary;
|
||||
private String requestSource; // user_self / matchmaker_referral
|
||||
private String preferredMode; // online / offline
|
||||
private String preferredTime;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.process')")
|
||||
@PostMapping("/operator/{id}/schedule")
|
||||
public ApiResponse<CounselingDTO> schedule(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
Instant startAt = Instant.parse(body.get("startAt"));
|
||||
Instant endAt = Instant.parse(body.get("endAt"));
|
||||
return ApiResponse.ok(counselingServiceApp.scheduleCounseling(id, principal, startAt, endAt));
|
||||
@Data
|
||||
public static class AssignRequest {
|
||||
private String assignedOperatorId;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.process')")
|
||||
@PostMapping("/operator/{id}/start")
|
||||
public ApiResponse<CounselingDTO> startCounseling(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
return ApiResponse.ok(counselingServiceApp.startCounseling(id, principal));
|
||||
@Data
|
||||
public static class ScheduleRequest {
|
||||
private Instant start;
|
||||
private Instant end;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.note.create')")
|
||||
@PostMapping("/operator/{id}/followup")
|
||||
public ApiResponse<Object> addFollowup(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(counselingServiceApp.addFollowup(
|
||||
id, principal,
|
||||
body.get("followupType"), body.get("content"),
|
||||
body.get("resultCode"),
|
||||
body.get("nextFollowupAt") != null ? Instant.parse(body.get("nextFollowupAt")) : null));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.process')")
|
||||
@PostMapping("/operator/{id}/complete")
|
||||
public ApiResponse<CounselingDTO> complete(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
return ApiResponse.ok(counselingServiceApp.completeCounseling(id, principal));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('counseling.service.process')")
|
||||
@PostMapping("/operator/{id}/close")
|
||||
public ApiResponse<CounselingDTO> close(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(counselingServiceApp.closeCounseling(id, principal, body.getOrDefault("reason", "")));
|
||||
}
|
||||
|
||||
// ---- Counselor端(INV-012: 只看分配给自己的)----
|
||||
@PreAuthorize("hasAuthority('counseling.service.view')")
|
||||
@GetMapping("/operator/assigned")
|
||||
public ApiResponse<List<CounselingDTO>> getAssigned(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal) {
|
||||
return ApiResponse.ok(counselingServiceApp.getCounselorAssignedServices(principal.id()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,24 +1,29 @@
|
||||
package com.danyang.match.platform.counseling.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.danyang.match.platform.shared.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 咨询跟进记录
|
||||
* 对应 DB: counseling_followups
|
||||
*
|
||||
* INV-013: 红娘不可读 content
|
||||
* INV-012: 只有 assigned Counselor 可写
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("counseling_followups")
|
||||
public class CounselingFollowup extends BaseEntity {
|
||||
public class CounselingFollowup implements Serializable {
|
||||
|
||||
private String id;
|
||||
private String counselingId;
|
||||
private String operatorId;
|
||||
private String followupType;
|
||||
private String content;
|
||||
private String content; // INV-013: 敏感正文
|
||||
private String resultCode;
|
||||
private Instant nextFollowupAt;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
|
||||
+17
-19
@@ -5,11 +5,16 @@ import com.danyang.match.platform.shared.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 公益婚恋咨询服务实体
|
||||
* V1.1 Phase 13A
|
||||
* 婚恋咨询 / 婚前辅导 服务
|
||||
* 对应 DB: counseling_services
|
||||
*
|
||||
* service_type: "counseling"(婚恋咨询)/ "premarital"(婚前辅导)
|
||||
* request_source: "user_self"(用户自己申请)/ "matchmaker_referral"(红娘转介)
|
||||
* status: pending → assigned → scheduled → in_progress → completed / cancelled
|
||||
*
|
||||
* INV-012: Counselor 只能读取 Assigned Service
|
||||
* INV-013: Matchmaker 不能读取 Counseling 正文(user_request_summary + followups.content)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@@ -17,22 +22,15 @@ import java.time.Instant;
|
||||
public class CounselingService extends BaseEntity {
|
||||
|
||||
private String userId;
|
||||
private String serviceType; // relationship_guidance / emotional_support / premarital_guidance / anti_fraud_guidance
|
||||
private String requestSource; // user_self / matchmaker_referral / operator_assisted 等
|
||||
private String userRequestSummary;
|
||||
private String serviceType; // counseling / premarital
|
||||
private String requestSource; // user_self / matchmaker_referral
|
||||
private String userRequestSummary; // INV-013: Matchmaker 不可读
|
||||
private String organizationId;
|
||||
private String assignedOperatorId;
|
||||
private String status; // pending / assigned / scheduled / in_progress / completed / cancelled / closed
|
||||
private String assignedOperatorId; // INV-012: 只有此人可读
|
||||
private String status;
|
||||
private String preferredServiceMode; // online / offline
|
||||
private String preferredTime;
|
||||
private Instant scheduledStartAt;
|
||||
private Instant scheduledEndAt;
|
||||
private String consentVersion;
|
||||
private Instant consentedAt;
|
||||
private Instant completedAt;
|
||||
private String closedReason;
|
||||
/** 伴侣用户ID(婚前辅导可选) */
|
||||
private String companionUserId;
|
||||
private String createdByOperatorId;
|
||||
private Instant userConfirmedAt;
|
||||
private java.time.Instant scheduledStartAt;
|
||||
private java.time.Instant scheduledEndAt;
|
||||
private java.time.Instant completedAt;
|
||||
}
|
||||
|
||||
+12
-8
@@ -11,15 +11,19 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface CounselingServiceMapper extends BaseMapper<CounselingService> {
|
||||
|
||||
@Select("SELECT * FROM counseling_services WHERE user_id = #{userId} AND deleted_at IS NULL ORDER BY created_at DESC")
|
||||
List<CounselingService> findByUserId(@Param("userId") String userId);
|
||||
@Select("SELECT * FROM counseling_services WHERE user_id = #{userId} ORDER BY created_at DESC LIMIT #{limit}")
|
||||
List<CounselingService> findByUser(@Param("userId") String userId, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT * FROM counseling_services WHERE assigned_operator_id = #{operatorId} AND deleted_at IS NULL ORDER BY created_at DESC")
|
||||
List<CounselingService> findByAssignedOperator(@Param("operatorId") String operatorId);
|
||||
@Select("SELECT * FROM counseling_services WHERE assigned_operator_id = #{operatorId} AND status IN ('pending','assigned','scheduled','in_progress') ORDER BY created_at DESC")
|
||||
List<CounselingService> findAssignedTo(@Param("operatorId") String operatorId);
|
||||
|
||||
@Select("SELECT * FROM counseling_services WHERE assigned_operator_id = #{operatorId} AND status IN ('pending','assigned','scheduled','in_progress') AND deleted_at IS NULL ORDER BY created_at DESC")
|
||||
List<CounselingService> findActiveByAssignedOperator(@Param("operatorId") String operatorId);
|
||||
@Select("SELECT * FROM counseling_services WHERE id = #{id}")
|
||||
CounselingService findFull(@Param("id") String id);
|
||||
|
||||
@Select("SELECT * FROM counseling_services WHERE status = 'pending' AND deleted_at IS NULL ORDER BY created_at ASC")
|
||||
List<CounselingService> findPendingAll();
|
||||
/** INV-013: 脱敏查询 — 给 Matchmaker 用,跳过敏感字段 */
|
||||
@Select("SELECT id, user_id, service_type, request_source, organization_id, assigned_operator_id, status, " +
|
||||
"preferred_service_mode, preferred_time, scheduled_start_at, scheduled_end_at, completed_at, " +
|
||||
"created_at, updated_at " +
|
||||
"FROM counseling_services WHERE id = #{id}")
|
||||
CounselingService findSanitized(@Param("id") String id);
|
||||
}
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package com.danyang.match.platform.counseling.service;
|
||||
|
||||
import com.danyang.match.platform.counseling.entity.CounselingService;
|
||||
import com.danyang.match.platform.counseling.mapper.CounselingServiceMapper;
|
||||
import com.danyang.match.platform.notification.service.NotificationService;
|
||||
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 13A 核心不变量:
|
||||
* - INV-012: Counselor 只能读取 Assigned Service
|
||||
* - INV-013: Matchmaker 不能读取 Counseling 正文(user_request_summary + followups.content)
|
||||
*
|
||||
* 权限模型(谁能看到什么):
|
||||
* ┌───────────────────────────┬────────────┬────────────────────┬──────────┐
|
||||
* │ 角色 │ 列表 │ 详情(脱敏) │ 详情(全文)│
|
||||
* ├───────────────────────────┼────────────┼────────────────────┼──────────┤
|
||||
* │ 申请人 user │ ✅ 自己的 │ ✅ 自己的 │ ✅ │
|
||||
* │ assigned Counselor │ ✅ 被分配的 │ ✅ 被分配的 │ ✅ │
|
||||
* │ Matchmaker (红娘) │ ❌ │ ✅ 脱敏版(无summary)│ ❌ │
|
||||
* │ counseling_manager │ ✅ │ ✅ │ ✅ │
|
||||
* │ service_station_operator │ ❌ │ ✅ 脱敏版 │ ❌ │
|
||||
* └───────────────────────────┴────────────┴────────────────────┴──────────┘
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CounselingService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CounselingService.class);
|
||||
|
||||
private final CounselingServiceMapper mapper;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
// ==================== 用户端:申请 ====================
|
||||
|
||||
/**
|
||||
* 用户申请婚恋咨询 / 婚前辅导
|
||||
*
|
||||
* @param userId 当前用户
|
||||
* @param serviceType "counseling" / "premarital"
|
||||
* @param summary 用户自述(INV-013: 红娘不可见)
|
||||
* @param requestSource "user_self" / "matchmaker_referral"
|
||||
*/
|
||||
@Transactional
|
||||
public CounselingService apply(String userId, String serviceType, String summary,
|
||||
String requestSource, String preferredMode, String preferredTime) {
|
||||
CounselingService svc = new CounselingService();
|
||||
svc.setId(UUID.randomUUID().toString());
|
||||
svc.setUserId(userId);
|
||||
svc.setServiceType(serviceType);
|
||||
svc.setUserRequestSummary(summary); // 敏感字段
|
||||
svc.setRequestSource(requestSource != null ? requestSource : "user_self");
|
||||
svc.setStatus("pending");
|
||||
svc.setPreferredServiceMode(preferredMode);
|
||||
svc.setPreferredTime(preferredTime);
|
||||
mapper.insert(svc);
|
||||
|
||||
// P11-T04: Counseling assigned 通知(pending 状态,等分配后再发 assigned 通知)
|
||||
log.info("CounselingService submitted: userId={}, type={}, source={}", userId, serviceType, requestSource);
|
||||
return svc;
|
||||
}
|
||||
|
||||
/** 用户查询自己的申请历史 */
|
||||
public List<CounselingService> myServices(String userId, int limit) {
|
||||
return mapper.findByUser(userId, Math.min(limit, 50));
|
||||
}
|
||||
|
||||
// ==================== Counselor 端:分配 + 排期 + 跟进 ====================
|
||||
|
||||
/** INV-012: 分配给 Counselor — 只有 counseling_manager 能调 */
|
||||
@Transactional
|
||||
public void assign(String serviceId, String counselorId) {
|
||||
CounselingService svc = mapper.findById(serviceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("服务不存在"));
|
||||
svc.setAssignedOperatorId(counselorId);
|
||||
svc.setStatus("assigned");
|
||||
mapper.updateById(svc);
|
||||
notificationService.counselingAssigned(svc.getUserId(), serviceId);
|
||||
}
|
||||
|
||||
/** INV-012: 排期 — 只有 assigned Counselor 能调 */
|
||||
@Transactional
|
||||
public void schedule(String serviceId, String counselorId, Instant start, Instant end) {
|
||||
CounselingService svc = assertAssigned(serviceId, counselorId);
|
||||
svc.setStatus("scheduled");
|
||||
svc.setScheduledStartAt(start);
|
||||
svc.setScheduledEndAt(end);
|
||||
mapper.updateById(svc);
|
||||
notificationService.counselingScheduled(svc.getUserId(), serviceId,
|
||||
start.toString() + " ~ " + end.toString());
|
||||
}
|
||||
|
||||
/** INV-012: 完成 — 只有 assigned Counselor 能调 */
|
||||
@Transactional
|
||||
public void complete(String serviceId, String counselorId) {
|
||||
CounselingService svc = assertAssigned(serviceId, counselorId);
|
||||
svc.setStatus("completed");
|
||||
svc.setCompletedAt(Instant.now());
|
||||
mapper.updateById(svc);
|
||||
notificationService.counselingCompleted(svc.getUserId(), serviceId);
|
||||
}
|
||||
|
||||
/** Counselor 查询被分配的列表 */
|
||||
public List<CounselingService> assignedTo(String counselorId) {
|
||||
return mapper.findAssignedTo(counselorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务详情 — 带权限检查
|
||||
*
|
||||
* @param requesterId 调用者用户/操作人 ID
|
||||
* @param requesterRole 调用者角色(决定返回脱敏还是全文)
|
||||
* @param serviceId 服务 ID
|
||||
*/
|
||||
public CounselingService getDetail(String requesterId, String requesterRole, String serviceId) {
|
||||
CounselingService full = mapper.findFull(serviceId);
|
||||
if (full == null) return null;
|
||||
|
||||
boolean isUser = full.getUserId().equals(requesterId);
|
||||
boolean isAssignedCounselor = requesterId.equals(full.getAssignedOperatorId());
|
||||
boolean isManager = "counseling_manager".equals(requesterRole);
|
||||
boolean isMatchmaker = "matchmaker".equals(requesterRole) || "operator".equals(requesterRole);
|
||||
|
||||
// 有权限看全文的情况
|
||||
if (isUser || isAssignedCounselor || isManager) {
|
||||
return full; // 完整返回,包含 user_request_summary
|
||||
}
|
||||
|
||||
// INV-013: Matchmaker / 普通 operator 只能看脱敏版
|
||||
if (isMatchmaker) {
|
||||
return mapper.findSanitized(serviceId);
|
||||
}
|
||||
|
||||
// 其他人一律无权限
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 内部工具 ====================
|
||||
|
||||
/** INV-012: 断言 counselorId 是 assigned_operator_id */
|
||||
private CounselingService assertAssigned(String serviceId, String counselorId) {
|
||||
CounselingService svc = mapper.findById(serviceId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("服务不存在"));
|
||||
if (!counselorId.equals(svc.getAssignedOperatorId())) {
|
||||
throw new IllegalStateException("INV-012: 您不是此服务的 assigned Counselor");
|
||||
}
|
||||
return svc;
|
||||
}
|
||||
|
||||
/** service_type → 标签名 */
|
||||
public static String typeLabel(String serviceType) {
|
||||
return Map.of(
|
||||
"counseling", "婚恋咨询",
|
||||
"premarital", "婚前辅导"
|
||||
).getOrDefault(serviceType, "公益服务");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user