feat: complete phase 02 RBAC and data scope recovery

This commit is contained in:
2026-09-10 10:58:43 +08:00
parent b46b48eb33
commit 5b51d5b3f1
115 changed files with 3608 additions and 1135 deletions
+1
View File
@@ -2,6 +2,7 @@
node_modules/
backend/local-maven/
backend/build-temp/
backend/.m2/
__pycache__/
*.py[cod]
*.lock
+5 -5
View File
@@ -38,22 +38,22 @@
<el-icon><Document /></el-icon><template #title>审计日志</template>
</el-menu-item>
<!-- V1.1 新增菜单 -->
<el-menu-item v-if="hasPerm('banner.manage')" index="/banner">
<el-menu-item v-if="hasPerm('banner.view')" index="/banner">
<el-icon><Promotion /></el-icon><template #title>Banner管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('content.manage')" index="/content">
<el-menu-item v-if="hasPerm('content.view')" index="/content">
<el-icon><FolderOpened /></el-icon><template #title>内容管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_location.manage')" index="/service-station">
<el-menu-item v-if="hasPerm('service_location.view')" index="/service-station">
<el-icon><Location /></el-icon><template #title>善缘服务站</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_appointment.manage')" index="/service-appointment">
<el-menu-item v-if="hasPerm('service_appointment.view')" index="/service-appointment">
<el-icon><Memo /></el-icon><template #title>服务预约</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_program.manage')" index="/service-program">
<el-icon><Service /></el-icon><template #title>专项服务项目</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('counseling.manage')" index="/counseling">
<el-menu-item v-if="hasPerm('counseling.service.view')" index="/counseling">
<el-icon><ChatDotRound /></el-icon><template #title>婚恋咨询</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('referral.manage')" index="/referral">
@@ -69,8 +69,8 @@
</el-select>
</el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort_order" :min="0" /></el-form-item>
<el-form-item label="展示开始"><el-date-picker v-model="form.startAt" type="datetime" /></el-form-item>
<el-form-item label="展示结束"><el-date-picker v-model="form.endAt" type="datetime" /></el-form-item>
<el-form-item label="展示开始"><el-date-picker v-model="form.start_at" type="datetime" /></el-form-item>
<el-form-item label="展示结束"><el-date-picker v-model="form.end_at" type="datetime" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -78,7 +78,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { get, post, put, del } from '@/api/client'
import { ElMessage } from 'element-plus'
@@ -3,7 +3,7 @@
<el-card>
<template #header><div class="card-header"><span>公益婚恋咨询管理</span></div></template>
<el-tabs v-model="activeTab">
<el-tab-pane label="待分配队列" name="queue">
<el-tab-pane v-if="canAssign" label="待分配队列" name="queue">
<el-table :data="pendingQueue" stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="120" />
<el-table-column prop="serviceType" label="服务类型" width="150">
@@ -36,10 +36,10 @@
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button v-if="row.status==='assigned'" size="small" @click="scheduleCounseling(row)">排期</el-button>
<el-button v-if="row.status==='scheduled'" size="small" type="success" @click="startCounseling(row)">开始</el-button>
<el-button v-if="row.status==='in_progress'" size="small" type="primary" @click="showFollowup(row)">跟进</el-button>
<el-button v-if="row.status==='in_progress'" size="small" type="warning" @click="completeCounseling(row)">完成</el-button>
<el-button v-if="canProcess && row.status==='assigned'" size="small" @click="scheduleCounseling(row)">排期</el-button>
<el-button v-if="canProcess && row.status==='scheduled'" size="small" type="success" @click="startCounseling(row)">开始</el-button>
<el-button v-if="canCreateNote && row.status==='in_progress'" size="small" type="primary" @click="showFollowup(row)">跟进</el-button>
<el-button v-if="canProcess && row.status==='in_progress'" size="small" type="warning" @click="completeCounseling(row)">完成</el-button>
</template>
</el-table-column>
</el-table>
@@ -78,11 +78,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { get, post } from '@/api/client'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const activeTab = ref('queue')
const authStore = useAuthStore()
const canAssign = computed(() => authStore.permissions.includes('counseling.service.assign'))
const canProcess = computed(() => authStore.permissions.includes('counseling.service.process'))
const canCreateNote = computed(() => authStore.permissions.includes('counseling.note.create'))
const activeTab = ref(canAssign.value ? 'queue' : 'assigned')
const loading = ref(false)
const pendingQueue = ref<any[]>([])
const assignedServices = ref<any[]>([])
@@ -116,38 +121,38 @@ function formatDateTime(d: string) { return d ? new Date(d).toLocaleString('zh-C
async function loadQueue() {
loading.value = true
try { const res: any = await get('/operator/counseling/services/queue'); pendingQueue.value = res?.data || [] } finally { loading.value = false }
try { const res: any = await get('/counseling/services/operator/queue'); pendingQueue.value = res?.data || [] } finally { loading.value = false }
}
async function loadAssigned() {
loading.value = true
try { const res: any = await get('/operator/counseling/services/operator/assigned'); assignedServices.value = res?.data || [] } finally { loading.value = false }
try { const res: any = await get('/counseling/services/operator/assigned'); assignedServices.value = res?.data || [] } finally { loading.value = false }
}
async function openAssign(row: any) { currentId.value = row.id; assignForm.value.counselorId = ''; assignDialogVisible.value = true }
async function doAssign() {
try { await post(`/operator/counseling/services/${currentId.value}/assign`, { counselorId: assignForm.value.counselorId }); ElMessage.success('分配成功'); assignDialogVisible.value = false; loadQueue() }
try { await post(`/counseling/services/operator/${currentId.value}/assign`, { counselorId: assignForm.value.counselorId }); ElMessage.success('分配成功'); assignDialogVisible.value = false; loadQueue() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function scheduleCounseling(row: any) {
const now = new Date(); now.setHours(now.getHours() + 2)
const end = new Date(now); end.setHours(end.getHours() + 1)
try { await post(`/operator/counseling/services/${row.id}/schedule`, { startAt: now.toISOString(), endAt: end.toISOString() }); ElMessage.success('排期成功'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/schedule`, { startAt: now.toISOString(), endAt: end.toISOString() }); ElMessage.success('排期成功'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function startCounseling(row: any) {
try { await post(`/operator/counseling/services/${row.id}/start`, {}); ElMessage.success('已开始'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/start`, {}); ElMessage.success('已开始'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
function showFollowup(row: any) { currentId.value = row.id; followupForm.value = { type: 'phone_call', content: '' }; followupDialogVisible.value = true }
async function doFollowup() {
try { await post(`/operator/counseling/services/${currentId.value}/followup`, { followupType: followupForm.value.type, content: followupForm.value.content }); ElMessage.success('跟进记录已保存'); followupDialogVisible.value = false; loadAssigned() }
}
try { await post(`/counseling/services/operator/${currentId.value}/followup`, { followupType: followupForm.value.type, content: followupForm.value.content }); ElMessage.success('跟进记录已保存'); followupDialogVisible.value = false; loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
async function completeCounseling(row: any) {
try { await post(, {}); ElMessage.success('已完成'); loadAssigned() }
try { await post(`/counseling/services/operator/${row.id}/complete`, {}); ElMessage.success('已完成'); loadAssigned() }
catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
}
onMounted(() => { loadQueue(); loadAssigned() })
onMounted(() => { if (canAssign.value) loadQueue(); loadAssigned() })
</script>
<style scoped>
@@ -1,7 +1,7 @@
<template>
<div class="service-program">
<el-card>
<template #header><div class="card-header"><span>专项公益服务项目</span><el-button type="primary" size="small" @click="openCreate">新建项目</el-button></div></template>
<template #header><div class="card-header"><span>专项公益服务项目</span><el-button type="primary" size="small" disabled>新建项目(待开放)</el-button></div></template>
<el-table :data="programs" stripe v-loading="loading">
<el-table-column prop="name" label="项目名称" />
<el-table-column prop="status" label="状态" width="100">
@@ -46,7 +46,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get, put } from '@/api/client'
import { get, post, put } from '@/api/client'
import { ElMessage } from 'element-plus'
const loading = ref(false)
+5 -24
View File
@@ -3,26 +3,6 @@ import type { RouteRecordRaw } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
// 路由权限映射:path prefix → 需要的最小权限码
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/users/detail': ['user.sensitive.view'],
'/operators': ['permission.manage'],
'/organizations': ['organization.manage'],
'/analytics': ['analytics.view'],
'/audit': ['user.view'],
'/review': ['review.view'],
'/risk/reports': ['risk.report.view'],
'/appeals': ['risk.report.process'],
'/activities/interests': ['activity.review_registration'],
'/banner': ['banner.manage'],
'/content': ['content.manage'],
'/service-station': ['service_location.manage'],
'/service-appointment': ['service_appointment.manage'],
'/service-program': ['service_program.manage'],
'/counseling': ['counseling.manage'],
'/referral': ['referral.manage'],
}
function hasPermission(requiredPerms: string[]): boolean {
if (!requiredPerms.length) return true
const userPerms = useAuthStore().permissions || []
@@ -48,11 +28,12 @@ const routes: RouteRecordRaw[] = [
{ path: 'analytics', name: 'Analytics', component: () => import('@/modules/analytics/AnalyticsView.vue'), meta: { title: '数据统计', permissions: ['analytics.view'] } },
{ path: 'audit', name: 'AuditLog', component: () => import('@/modules/audit/AuditLogView.vue'), meta: { title: '操作日志', permissions: ['user.view'] } },
// V1.1 新增模块
{ path: 'banner', name: 'BannerManagement', component: () => import('@/modules/banner/BannerManagementView.vue'), meta: { title: 'Banner管理', permissions: ['banner.manage'] } },
{ path: 'content', name: 'ContentManagement', component: () => import('@/modules/content/ContentManagementView.vue'), meta: { title: '内容管理', permissions: ['content.manage'] } },
{ path: 'service-station', name: 'ServiceStation', component: () => import('@/modules/service-station/ServiceStationView.vue'), meta: { title: '善缘服务站', permissions: ['service_location.manage'] } },
{ path: 'service-appointment', name: 'ServiceAppointment', component: () => import('@/modules/service-appointment/ServiceAppointmentView.vue'), meta: { title: '服务预约', permissions: ['service_appointment.manage'] } },
{ path: 'banner', name: 'BannerManagement', component: () => import('@/modules/banner/BannerManagementView.vue'), meta: { title: 'Banner管理', permissions: ['banner.view'] } },
{ path: 'content', name: 'ContentManagement', component: () => import('@/modules/content/ContentManagementView.vue'), meta: { title: '内容管理', permissions: ['content.view'] } },
{ path: 'service-station', name: 'ServiceStation', component: () => import('@/modules/service-station/ServiceStationView.vue'), meta: { title: '善缘服务站', permissions: ['service_location.view'] } },
{ path: 'service-appointment', name: 'ServiceAppointment', component: () => import('@/modules/service-appointment/ServiceAppointmentView.vue'), meta: { title: '服务预约', permissions: ['service_appointment.view'] } },
{ path: 'service-program', name: 'ServiceProgram', component: () => import('@/modules/service-program/ServiceProgramView.vue'), meta: { title: '专项服务项目', permissions: ['service_program.manage'] } },
{ path: 'counseling', name: 'Counseling', component: () => import('@/modules/counseling/CounselingView.vue'), meta: { title: '婚恋咨询', permissions: ['counseling.service.view'] } },
{ path: 'referral', name: 'ReferralManagement', component: () => import('@/modules/referral/ReferralManagementView.vue'), meta: { title: '转介管理', permissions: ['referral.manage'] } },
],
},
+3 -3
View File
@@ -22,15 +22,15 @@ export const useAuthStore = defineStore('auth', () => {
async function login(username: string, password: string) {
const res: any = await post('/auth/operator/login', { username, password })
if (res.success) {
token.value = res.data.token
token.value = res.data.accessToken
operator.value = {
operatorId: res.data.operatorId,
name: res.data.name,
username: res.data.username,
permissions: res.data.permissions || [],
organizationId: null,
organizationId: res.data.organizationId ?? null,
}
localStorage.setItem('accessToken', res.data.token)
localStorage.setItem('accessToken', res.data.accessToken)
localStorage.setItem('operator', JSON.stringify(operator.value))
return res.data
}
+16
View File
@@ -0,0 +1,16 @@
# 环境变体
# 生产环境专属配置(不提交到 Git)
# 复制 application-prod.yml.example 到 application-prod.yml 并填写真实值
# 本地开发
# DB_HOST=localhost
# DB_PORT=5432
# DB_NAME=danyang_match
# DB_USER=postgres
# DB_PASSWORD=replace-with-local-password
# REDIS_HOST=localhost
# REDIS_PORT=6379
# MINIO_ENDPOINT=http://localhost:9000
# MINIO_ACCESS_KEY=replace-with-local-access-key
# MINIO_SECRET_KEY=replace-with-local-secret-key
+14
View File
@@ -141,6 +141,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> <dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
@@ -183,6 +187,16 @@
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<workingDirectory>${project.basedir}/..</workingDirectory>
<systemPropertyVariables>
<spring.profiles.active>test</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
@@ -1,9 +1,10 @@
package com.danyang.match.platform.account.controller;
import com.danyang.match.platform.account.service.AccountLifecycleService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -17,7 +18,6 @@ import java.util.Map;
public class AccountController {
private final AccountLifecycleService accountService;
private final JwtTokenService jwtTokenService;
/**
* 退出婚恋服务(暂停推荐)
@@ -25,8 +25,8 @@ public class AccountController {
*/
@PostMapping("/withdraw")
public ApiResponse<Map<String, Object>> withdrawService(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(accountService.withdrawDatingService(userId, userId));
}
@@ -36,8 +36,8 @@ public class AccountController {
*/
@PostMapping("/resume")
public ApiResponse<Map<String, Object>> resumeService(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(accountService.resumeDatingService(userId));
}
@@ -47,8 +47,8 @@ public class AccountController {
*/
@GetMapping("/can-withdraw")
public ApiResponse<Map<String, Object>> canWithdraw(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(accountService.checkCanWithdraw(userId));
}
@@ -59,9 +59,9 @@ public class AccountController {
*/
@PostMapping("/close")
public ApiResponse<Map<String, Object>> closeAccount(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String reason = body.getOrDefault("reason", "");
return ApiResponse.ok(accountService.closeAccount(userId, userId, reason));
}
@@ -72,8 +72,8 @@ public class AccountController {
*/
@GetMapping("/privacy-settings")
public ApiResponse<Map<String, Object>> getPrivacySettings(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(accountService.getPrivacySettings(userId));
}
@@ -83,22 +83,11 @@ public class AccountController {
*/
@PutMapping("/privacy-settings")
public ApiResponse<Map<String, Object>> updatePrivacySettings(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> settings) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(accountService.updatePrivacySettings(userId, settings));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,11 @@
package com.danyang.match.platform.account.controller;
import com.danyang.match.platform.account.service.AccountLifecycleService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
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.util.Map;
@@ -17,18 +19,18 @@ import java.util.Map;
public class AccountOperatorController {
private final AccountLifecycleService accountService;
private final JwtTokenService jwtTokenService;
/**
* 强制注销用户账号(管理端)
* POST /api/v1/operator/account/{userId}/close
*/
@PostMapping("/{userId}/close")
@PreAuthorize("hasAuthority('user.status.change')")
public ApiResponse<Map<String, Object>> closeAccount(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String reason = body.getOrDefault("reason", "管理员强制注销");
return ApiResponse.ok(accountService.closeAccount(userId, operatorId, reason));
}
@@ -38,17 +40,11 @@ public class AccountOperatorController {
* GET /api/v1/operator/account/{userId}/can-withdraw
*/
@GetMapping("/{userId}/can-withdraw")
@PreAuthorize("hasAuthority('user.view')")
public ApiResponse<Map<String, Object>> canWithdraw(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
return ApiResponse.ok(accountService.checkCanWithdraw(userId));
}
private String extractOperatorId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
String token = authHeader.substring(7);
return jwtTokenService.getUserIdFromToken(token);
}
}
@@ -1,9 +1,12 @@
package com.danyang.match.platform.activity.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.activity.service.ActivityService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
@@ -18,27 +21,26 @@ import java.util.Map;
public class ActivityController {
private final ActivityService activityService;
private final JwtTokenService jwtTokenService;
@GetMapping
public ApiResponse<?> getActivities(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(activityService.getActivities(userId));
}
@GetMapping("/upcoming")
public ApiResponse<?> getUpcoming(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(activityService.getUpcomingActivities(userId));
}
@GetMapping("/{id}")
public ApiResponse<Map<String, Object>> getActivityDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
Map<String, Object> detail = activityService.getActivityDetail(id, userId);
if (detail == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -48,10 +50,10 @@ public class ActivityController {
@PostMapping("/{id}/register")
public ApiResponse<Map<String, Object>> register(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String emergencyContactName = body.get("emergencyContactName");
String emergencyContactPhone = body.get("emergencyContactPhone");
return ApiResponse.ok(activityService.registerActivity(userId, id, emergencyContactName, emergencyContactPhone));
@@ -59,17 +61,17 @@ public class ActivityController {
@DeleteMapping("/{id}/register")
public ApiResponse<Void> cancelRegistration(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
activityService.cancelRegistration(userId, id);
return ApiResponse.ok();
}
@GetMapping("/my")
public ApiResponse<List<Map<String, Object>>> getMyActivities(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(activityService.getMyActivities(userId));
}
@@ -77,60 +79,53 @@ public class ActivityController {
@GetMapping("/types/{activityType}")
public ApiResponse<?> getActivitiesByType(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityType) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(activityService.getActivitiesByType(userId, activityType));
}
@GetMapping("/{id}/organizations")
public ApiResponse<List<Map<String, Object>>> getActivityOrganizations(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(activityService.getActivityOrganizations(id));
}
@GetMapping("/{id}/capacity-groups")
public ApiResponse<List<Map<String, Object>>> getActivityCapacityGroups(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(activityService.getActivityCapacityGroups(id));
}
@PostMapping("/operator")
@PreAuthorize("hasAuthority('activity.create')")
public ApiResponse<?> createActivityOperator(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> body) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.createActivityFromAdmin(body, operatorId));
}
@PostMapping("/operator/{id}/organizations")
@PreAuthorize("hasAuthority('activity.create')")
public ApiResponse<?> addActivityOrganization(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.addActivityOrganization(id, body.get("organizationId"), body.get("role"), operatorId));
}
@PostMapping("/operator/{id}/capacity-groups")
@PreAuthorize("hasAuthority('activity.create')")
public ApiResponse<?> addCapacityGroup(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, Object> body) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.addCapacityGroup(id, body, operatorId));
}
private String extractUserId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return null;
}
String token = authHeader.substring(7);
if (!jwtTokenService.validateToken(token)) {
return null;
}
return jwtTokenService.getUserIdFromToken(token);
}
}
@@ -3,10 +3,13 @@ package com.danyang.match.platform.activity.controller;
import com.danyang.match.platform.activity.dto.ActivityFeedbackDTO;
import com.danyang.match.platform.activity.dto.ActivityMatchItemDTO;
import com.danyang.match.platform.activity.service.ActivityInterestService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.relationship.service.RelationshipService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.HashMap;
@@ -23,7 +26,6 @@ public class ActivityInterestController {
private final ActivityInterestService interestService;
private final RelationshipService relationshipService;
private final JwtTokenService jwtTokenService;
/**
* 提交活动反馈 + 表达意向
@@ -31,10 +33,10 @@ public class ActivityInterestController {
*/
@PostMapping("/{id}/feedback")
public ApiResponse<Map<String, Object>> submitFeedback(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody ActivityFeedbackDTO body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
Map<String, Object> result = interestService.submitFeedback(
userId,
id,
@@ -52,9 +54,9 @@ public class ActivityInterestController {
*/
@GetMapping("/{id}/interests/mutual")
public ApiResponse<List<ActivityMatchItemDTO>> getMutualMatches(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(interestService.getMutualMatches(userId, id));
}
@@ -64,10 +66,10 @@ public class ActivityInterestController {
*/
@GetMapping("/{id}/interests/mutual/{partnerId}")
public ApiResponse<Map<String, Object>> checkMutualInterest(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@PathVariable String partnerId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
boolean mutual = interestService.hasMutualInterest(userId, partnerId, id);
Map<String, Object> result = new HashMap<>();
result.put("hasMutual", mutual);
@@ -82,10 +84,10 @@ public class ActivityInterestController {
*/
@PostMapping("/{id}/interests/confirm")
public ApiResponse<Map<String, Object>> confirmInterest(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String partnerId = body.get("partnerId");
if (partnerId == null || partnerId.isBlank()) {
return ApiResponse.error(
@@ -106,22 +108,12 @@ public class ActivityInterestController {
* GET /api/v1/operator/activities/{id}/interest-stats
*/
@GetMapping("/operator/{id}/interest-stats")
@PreAuthorize("hasAuthority('activity.view')")
public ApiResponse<Map<String, Object>> getActivityInterestStats(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(interestService.getActivityInterestStats(id));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,13 @@
package com.danyang.match.platform.activity.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.activity.service.ActivityService;
import com.danyang.match.platform.activity.mapper.ActivityMapper;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -20,23 +22,24 @@ import com.danyang.match.platform.shared.constant.Constants;
*/
@RestController
@RequestMapping("/operator/activities")
@PreAuthorize("hasAuthority('activity.view')")
@RequiredArgsConstructor
public class ActivityOperatorController {
private final ActivityService activityService;
private final ActivityMapper activityMapper;
private final JwtTokenService jwtTokenService;
/**
* 创建活动
* POST /api/v1/operator/activities
*/
@PostMapping
@PreAuthorize("hasAuthority('activity.create')")
@Transactional
public ApiResponse<Activity> createActivity(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
Activity activity = new Activity();
activity.setTitle((String) body.get("title"));
activity.setDescription((String) body.get("description"));
@@ -63,10 +66,11 @@ public class ActivityOperatorController {
* PUT /api/v1/operator/activities/{id}/publish
*/
@PutMapping("/{id}/publish")
@PreAuthorize("hasAuthority('activity.create')")
public ApiResponse<?> publishActivity(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.publishActivity(id, operatorId));
}
@@ -75,10 +79,11 @@ public class ActivityOperatorController {
* PUT /api/v1/operator/activities/{id}/cancel
*/
@PutMapping("/{id}/cancel")
@PreAuthorize("hasAuthority('activity.create')")
public ApiResponse<?> cancelActivity(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.cancelActivity(id, operatorId));
}
@@ -88,7 +93,7 @@ public class ActivityOperatorController {
*/
@GetMapping
public ApiResponse<List<Map<String, Object>>> getActivities(
@RequestHeader("Authorization") String authHeader) {
@AuthenticationPrincipal OperatorPrincipal principal) {
List<Activity> activities = activityMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Activity>()
.eq(Activity::getDeletedAt, 0)
@@ -114,7 +119,7 @@ public class ActivityOperatorController {
*/
@GetMapping("/{id}/registrations")
public ApiResponse<List<Map<String, Object>>> getRegistrations(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(activityService.getRegistrations(id));
}
@@ -124,10 +129,11 @@ public class ActivityOperatorController {
* POST /api/v1/operator/activities/registrations/{rid}/approve
*/
@PostMapping("/registrations/{rid}/approve")
@PreAuthorize("hasAuthority('activity.review_registration')")
public ApiResponse<?> approveRegistration(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String rid) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(activityService.approveRegistration(rid, operatorId));
}
@@ -137,25 +143,14 @@ public class ActivityOperatorController {
* Body: { "reason": "..." }
*/
@PostMapping("/registrations/{rid}/reject")
@PreAuthorize("hasAuthority('activity.review_registration')")
public ApiResponse<?> rejectRegistration(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String rid,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String reason = body.get("reason");
return ApiResponse.ok(activityService.rejectRegistration(rid, operatorId, reason));
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,12 @@
package com.danyang.match.platform.activity.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.activity.service.CheckinService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.Map;
@@ -17,7 +20,6 @@ import java.util.Map;
public class CheckinController {
private final CheckinService checkinService;
private final JwtTokenService jwtTokenService;
/**
* 生成签到码
@@ -25,9 +27,9 @@ public class CheckinController {
*/
@PostMapping("/{id}/checkin-token")
public ApiResponse<Map<String, Object>> generateCheckinToken(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(checkinService.generateCheckinToken(userId, id));
}
@@ -37,10 +39,11 @@ public class CheckinController {
* Body: { "token": "..." }
*/
@PostMapping("/operator/checkin/scan")
@PreAuthorize("hasAuthority('activity.checkin')")
public ApiResponse<Map<String, Object>> scanCheckin(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String token = body.get("token");
if (token == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "token不能为空");
@@ -54,11 +57,12 @@ public class CheckinController {
* Body: { "userId": "..." }
*/
@PostMapping("/operator/{id}/checkin/manual")
@PreAuthorize("hasAuthority('activity.checkin')")
public ApiResponse<Map<String, Object>> manualCheckin(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String userId = body.get("userId");
if (userId == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "userId不能为空");
@@ -71,27 +75,13 @@ public class CheckinController {
* GET /api/v1/operator/activities/{id}/checkin-stats
*/
@GetMapping("/operator/{id}/checkin-stats")
@PreAuthorize("hasAuthority('activity.checkin')")
public ApiResponse<Map<String, Object>> getCheckinStats(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(checkinService.getCheckinStats(id));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -2,9 +2,10 @@ package com.danyang.match.platform.activity_mutual.controller;
import com.danyang.match.platform.activity_mutual.dto.MutualInterestResultDTO;
import com.danyang.match.platform.activity_mutual.service.ActivityMutualConsentService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -20,7 +21,6 @@ import java.util.Map;
public class ActivityMutualConsentController {
private final ActivityMutualConsentService consentService;
private final JwtTokenService jwtTokenService;
/**
* P16-T03: 检测双向互选结果
@@ -28,9 +28,9 @@ public class ActivityMutualConsentController {
*/
@GetMapping("/results")
public ApiResponse<List<MutualInterestResultDTO>> getMutualResults(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(consentService.detectMutualResults(activityId));
}
@@ -40,10 +40,10 @@ public class ActivityMutualConsentController {
*/
@PostMapping("/consent")
public ApiResponse<?> createConsent(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityId,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String partnerId = body.get("partnerId");
return ApiResponse.ok(consentService.createMutualConsent(activityId, userId, partnerId));
}
@@ -54,10 +54,10 @@ public class ActivityMutualConsentController {
*/
@PostMapping("/confirm")
public ApiResponse<Map<String, Object>> confirmConnection(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityId,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String partnerId = body.get("partnerId");
return ApiResponse.ok(consentService.confirmConnection(userId, activityId, partnerId));
}
@@ -68,10 +68,10 @@ public class ActivityMutualConsentController {
*/
@PostMapping("/decline")
public ApiResponse<Map<String, Object>> declineConnection(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityId,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String partnerId = body.get("partnerId");
return ApiResponse.ok(consentService.declineConnection(userId, activityId, partnerId));
}
@@ -82,10 +82,10 @@ public class ActivityMutualConsentController {
*/
@GetMapping("/status/{partnerId}")
public ApiResponse<Map<String, Object>> getMyConsentStatus(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String activityId,
@PathVariable String partnerId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(consentService.getMyConsentStatus(userId, activityId, partnerId));
}
@@ -95,21 +95,10 @@ public class ActivityMutualConsentController {
*/
@GetMapping("/my-results")
public ApiResponse<List<MutualInterestResultDTO>> getMyMutualResults(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(consentService.getMyMutualResults(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -3,6 +3,7 @@ package com.danyang.match.platform.analytics;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -14,6 +15,7 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/analytics")
@PreAuthorize("hasAuthority('analytics.view')")
@RequiredArgsConstructor
public class AnalyticsController {
@@ -3,9 +3,10 @@ package com.danyang.match.platform.appeal.controller;
import com.danyang.match.platform.appeal.dto.AppealProcessDTO;
import com.danyang.match.platform.appeal.dto.AppealSubmitDTO;
import com.danyang.match.platform.appeal.service.AppealService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -19,7 +20,6 @@ import java.util.Map;
public class AppealController {
private final AppealService appealService;
private final JwtTokenService jwtTokenService;
/**
* 获取账号状态(含是否可申诉)
@@ -27,8 +27,8 @@ public class AppealController {
*/
@GetMapping("/account-status")
public ApiResponse<Map<String, Object>> getAccountStatus(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(appealService.getAccountStatus(userId));
}
@@ -38,9 +38,9 @@ public class AppealController {
*/
@PostMapping
public ApiResponse<Map<String, Object>> submitAppeal(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody AppealSubmitDTO body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(appealService.submitAppeal(userId, body));
}
@@ -49,21 +49,10 @@ public class AppealController {
* GET /api/v1/appeals/my
*/
@GetMapping("/my")
public ApiResponse<Object> getMyAppeals(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Object> getMyAppeals(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(appealService.getMyAppeals(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -2,9 +2,11 @@ package com.danyang.match.platform.appeal.controller;
import com.danyang.match.platform.appeal.dto.AppealProcessDTO;
import com.danyang.match.platform.appeal.service.AppealService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
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.util.Map;
@@ -14,18 +16,18 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/appeals")
@PreAuthorize("hasAuthority('risk.report.view')")
@RequiredArgsConstructor
public class AppealOperatorController {
private final AppealService appealService;
private final JwtTokenService jwtTokenService;
/**
* 获取待处理申诉
* GET /api/v1/operator/appeals/pending
*/
@GetMapping("/pending")
public ApiResponse<Object> getPendingAppeals(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Object> getPendingAppeals(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(appealService.getPendingAppeals());
}
@@ -35,7 +37,7 @@ public class AppealOperatorController {
*/
@GetMapping("/{id}")
public ApiResponse<Object> getAppealDetail(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(appealService.getAppealDetail(id));
}
@@ -46,11 +48,12 @@ public class AppealOperatorController {
* Body: { "decision": "approve|reject|escalate", "decisionNote": "..." }
*/
@PostMapping("/{id}/process")
@PreAuthorize("hasAuthority('risk.report.process')")
public ApiResponse<Map<String, Object>> processAppeal(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody AppealProcessDTO body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(appealService.processAppeal(id, body, operatorId));
}
@@ -59,7 +62,7 @@ public class AppealOperatorController {
* GET /api/v1/operator/appeals/stats
*/
@GetMapping("/stats")
public ApiResponse<Map<String, Object>> getAppealStats(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Map<String, Object>> getAppealStats(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(appealService.getAppealStats());
}
@@ -69,21 +72,9 @@ public class AppealOperatorController {
*/
@GetMapping("/users/{userId}")
public ApiResponse<Object> getUserAppeals(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
return ApiResponse.ok(appealService.getUserAppeals(userId));
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return token;
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -3,6 +3,7 @@ package com.danyang.match.platform.audit.controller;
import com.danyang.match.platform.audit.service.AuditLogService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -12,6 +13,7 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/audit")
@PreAuthorize("hasAuthority('permission.manage')")
@RequiredArgsConstructor
public class AuditLogController {
@@ -1,7 +1,9 @@
package com.danyang.match.platform.auth.controller;
import com.danyang.match.platform.auth.service.AuthService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -14,7 +16,6 @@ import java.util.Map;
public class AuthController {
private final AuthService authService;
private final JwtTokenService jwtTokenService;
/**
* 微信登录
@@ -25,7 +26,8 @@ public class AuthController {
public ApiResponse<Map<String, Object>> wechatLogin(@RequestBody Map<String, String> body) {
String code = body.get("code");
if (code == null || code.isBlank()) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "code 不能为空");
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "code 不能为空");
}
return ApiResponse.ok(authService.wechatLogin(code));
}
@@ -37,14 +39,15 @@ public class AuthController {
* Body: { "phone": "13800138000" }
*/
@PostMapping("/phone/bind")
public ApiResponse<Void> bindPhone(@RequestHeader("Authorization") String authHeader,
@PreAuthorize("hasRole('USER')")
public ApiResponse<Void> bindPhone(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String phone = body.get("phone");
if (phone == null || phone.isBlank()) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "手机号不能为空");
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "手机号不能为空");
}
authService.bindPhone(userId, phone);
authService.bindPhone(principal.id(), phone);
return ApiResponse.ok();
}
@@ -58,7 +61,8 @@ public class AuthController {
String userId = body.get("userId");
String refreshToken = body.get("refreshToken");
if (userId == null || refreshToken == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "参数缺失");
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "参数缺失");
}
return ApiResponse.ok(authService.refreshToken(userId, refreshToken));
}
@@ -68,10 +72,10 @@ public class AuthController {
* POST /api/v1/auth/logout
*/
@PostMapping("/logout")
public ApiResponse<Void> logout(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
String token = extractToken(authHeader);
authService.logout(userId, token);
@PreAuthorize("hasRole('USER')")
public ApiResponse<Void> logout(@AuthenticationPrincipal UserPrincipal principal,
@RequestAttribute("currentUserToken") String token) {
authService.logout(principal.id(), token);
return ApiResponse.ok();
}
@@ -80,21 +84,8 @@ public class AuthController {
* GET /api/v1/me/status
*/
@GetMapping("/me/status")
public ApiResponse<Map<String, Object>> getMeStatus(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
return ApiResponse.ok(authService.getMeStatus(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
@PreAuthorize("hasRole('USER')")
public ApiResponse<Map<String, Object>> getMeStatus(@AuthenticationPrincipal UserPrincipal principal) {
return ApiResponse.ok(authService.getMeStatus(principal.id()));
}
}
@@ -0,0 +1,30 @@
package com.danyang.match.platform.auth.controller;
import com.danyang.match.platform.auth.service.OperatorAuthService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.enums.ApiResponseCode;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/auth/operator")
@RequiredArgsConstructor
public class OperatorAuthController {
private final OperatorAuthService operatorAuthService;
@PostMapping("/login")
public ApiResponse<Map<String, Object>> login(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");
if (username == null || username.isBlank() || password == null || password.isBlank()) {
return ApiResponse.error(ApiResponseCode.BAD_REQUEST, "用户名和密码不能为空");
}
return ApiResponse.ok(operatorAuthService.login(username, password));
}
}
@@ -9,6 +9,10 @@ import com.danyang.match.platform.permission.mapper.OperatorMapper;
import com.danyang.match.platform.permission.mapper.PermissionMapper;
import com.danyang.match.platform.permission.entity.Permission;
import com.danyang.match.platform.shared.constant.Constants;
import com.danyang.match.platform.shared.enums.ApiResponseCode;
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.UnauthorizedException;
import org.springframework.security.access.AccessDeniedException;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -20,6 +24,7 @@ import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@RequiredArgsConstructor
@@ -43,7 +48,7 @@ public class OperatorAuthService {
*/
public Map<String, Object> login(String username, String password) {
Operator operator = operatorMapper.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户名或密码错误"));
.orElseThrow(() -> new UnauthorizedException("用户名或密码错误"));
// 检查账号是否已被锁定
String lockKey = "auth:operator_login_lock:" + operator.getId();
@@ -52,12 +57,13 @@ public class OperatorAuthService {
long remainingSeconds = java.time.Duration.between(
java.time.Instant.now(), java.time.Instant.parse(lockedUntil)).getSeconds();
if (remainingSeconds > 0) {
throw new RuntimeException("账号已锁定,请 " + remainingSeconds / 60 + " 分钟后重试");
throw new BusinessException(ApiResponseCode.RATE_LIMITED,
"账号已锁定,请 " + Math.max(1, remainingSeconds / 60) + " 分钟后重试");
}
}
if (!Constants.ACCOUNT_STATUS_NORMAL.equals(operator.getStatus())) {
throw new RuntimeException("账号已禁用,请联系管理员");
if (!Set.of("active", Constants.ACCOUNT_STATUS_NORMAL).contains(operator.getStatus())) {
throw new AccessDeniedException("账号已禁用,请联系管理员");
}
if (!passwordEncoder.matches(password, operator.getPasswordHash())) {
@@ -76,7 +82,7 @@ public class OperatorAuthService {
log.warn("Operator 账号被锁定: username={}, ip=unknown", username);
}
log.warn("登录失败: username={}, ip={}", username, "unknown");
throw new RuntimeException("用户名或密码错误");
throw new UnauthorizedException("用户名或密码错误");
}
// 登录成功:清除失败计数和锁定
@@ -115,6 +121,7 @@ public class OperatorAuthService {
result.put("refreshToken", refreshToken);
result.put("operatorId", operator.getId());
result.put("name", operator.getName());
result.put("username", operator.getUsername());
result.put("organizationId", operator.getOrganizationId());
result.put("permissions", permissionCodes);
result.put("expiresIn", jwtTokenService.getExpirationMs() / 1000);
@@ -148,6 +155,7 @@ public class OperatorAuthService {
result.put("operatorId", operator.getId());
result.put("name", operator.getName());
result.put("username", operator.getUsername());
result.put("username", operator.getUsername());
result.put("organizationId", operator.getOrganizationId());
result.put("status", operator.getStatus());
result.put("permissions", permissionCodes);
@@ -2,9 +2,11 @@ package com.danyang.match.platform.content.controller;
import com.danyang.match.platform.content.dto.ContentArticleDTO;
import com.danyang.match.platform.content.dto.ContentCategoryDTO;
import com.danyang.match.platform.content.service.ContentService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
import java.util.Map;
@@ -13,7 +15,6 @@ import java.util.Map;
@RequiredArgsConstructor
public class ContentController {
private final ContentService contentService;
private final JwtTokenService jwtTokenService;
@GetMapping("/categories")
public ApiResponse<List<ContentCategoryDTO>> getCategories() {
@@ -22,14 +23,12 @@ public class ContentController {
@GetMapping("/articles")
public ApiResponse<List<ContentArticleDTO>> getArticles(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@RequestParam(required = false) String categoryId) {
return ApiResponse.ok(contentService.getPublishedArticles(categoryId));
}
@GetMapping("/articles/{id}")
public ApiResponse<ContentArticleDTO> getArticleDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String id) {
ContentArticleDTO detail = contentService.getArticleDetail(id);
if (detail == null) return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -37,61 +36,52 @@ public class ContentController {
}
// ---- Admin ----
@PreAuthorize("hasAuthority('content.create')")
@PostMapping("/operator/categories")
public ApiResponse<ContentCategoryDTO> createCategory(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> body) {
return ApiResponse.ok(contentService.createCategory(body));
}
@PreAuthorize("hasAuthority('content.edit')")
@PutMapping("/operator/categories/{id}")
public ApiResponse<ContentCategoryDTO> updateCategory(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id, @RequestBody Map<String, Object> body) {
return ApiResponse.ok(contentService.updateCategory(id, body));
}
@PreAuthorize("hasAuthority('content.create')")
@PostMapping("/operator/articles")
public ApiResponse<ContentArticleDTO> createArticle(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> body) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(contentService.createArticle(body, operatorId));
return ApiResponse.ok(contentService.createArticle(body, principal.id()));
}
@PreAuthorize("hasAuthority('content.edit')")
@PutMapping("/operator/articles/{id}")
public ApiResponse<ContentArticleDTO> updateArticle(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id, @RequestBody Map<String, Object> body) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(contentService.updateArticle(id, body, operatorId));
return ApiResponse.ok(contentService.updateArticle(id, body, principal.id()));
}
@PreAuthorize("hasAuthority('content.publish')")
@PostMapping("/operator/articles/{id}/publish")
public ApiResponse<ContentArticleDTO> publishArticle(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(contentService.publishArticle(id, operatorId));
return ApiResponse.ok(contentService.publishArticle(id, principal.id()));
}
@PreAuthorize("hasAuthority('content.offline')")
@PostMapping("/operator/articles/{id}/offline")
public ApiResponse<ContentArticleDTO> offlineArticle(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(contentService.offlineArticle(id, operatorId));
return ApiResponse.ok(contentService.offlineArticle(id, principal.id()));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,14 @@
package com.danyang.match.platform.counseling.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
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.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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;
@@ -22,7 +25,6 @@ import java.util.Map;
public class CounselingController {
private final CounselingServiceApp counselingServiceApp;
private final JwtTokenService jwtTokenService;
// ---- 用户端 ----
@@ -33,24 +35,24 @@ public class CounselingController {
@PostMapping
public ApiResponse<CounselingDTO> create(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody CounselingRequest request) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(counselingServiceApp.createCounseling(userId, request));
}
@GetMapping
public ApiResponse<List<CounselingDTO>> getMyCounselings(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(counselingServiceApp.getMyCounselings(userId));
}
@GetMapping("/{id}")
public ApiResponse<CounselingDTO> getDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
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);
@@ -60,94 +62,86 @@ public class CounselingController {
@PostMapping("/{id}/cancel")
public ApiResponse<CounselingDTO> cancel(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(counselingServiceApp.cancelCounseling(id, userId));
}
// ---- 后台端 ----
@PreAuthorize("hasAuthority('counseling.service.assign')")
@GetMapping("/operator/queue")
public ApiResponse<List<CounselingDTO>> getPendingQueue(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String operatorId = extractUserId(authHeader);
public ApiResponse<List<CounselingDTO>> getPendingQueue() {
return ApiResponse.ok(counselingServiceApp.getPendingQueue());
}
@PreAuthorize("hasAuthority('counseling.service.assign')")
@PostMapping("/operator/{id}/assign")
public ApiResponse<CounselingDTO> assignCounselor(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractUserId(authHeader);
String counselorId = body.get("counselorId");
return ApiResponse.ok(counselingServiceApp.assignCounselor(id, operatorId, counselorId));
return ApiResponse.ok(counselingServiceApp.assignCounselor(id, principal.id(), counselorId));
}
@PreAuthorize("hasAuthority('counseling.service.process')")
@PostMapping("/operator/{id}/schedule")
public ApiResponse<CounselingDTO> schedule(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@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, startAt, endAt));
return ApiResponse.ok(counselingServiceApp.scheduleCounseling(id, principal, startAt, endAt));
}
@PreAuthorize("hasAuthority('counseling.service.process')")
@PostMapping("/operator/{id}/start")
public ApiResponse<CounselingDTO> startCounseling(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(counselingServiceApp.startCounseling(id));
return ApiResponse.ok(counselingServiceApp.startCounseling(id, principal));
}
@PreAuthorize("hasAuthority('counseling.note.create')")
@PostMapping("/operator/{id}/followup")
public ApiResponse<Object> addFollowup(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(counselingServiceApp.addFollowup(
id, operatorId,
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(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(counselingServiceApp.completeCounseling(id));
return ApiResponse.ok(counselingServiceApp.completeCounseling(id, principal));
}
@PreAuthorize("hasAuthority('counseling.service.process')")
@PostMapping("/operator/{id}/close")
public ApiResponse<CounselingDTO> close(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
return ApiResponse.ok(counselingServiceApp.closeCounseling(id, body.getOrDefault("reason", "")));
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(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String operatorId = extractUserId(authHeader);
return ApiResponse.ok(counselingServiceApp.getCounselorAssignedServices(operatorId));
@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(counselingServiceApp.getCounselorAssignedServices(principal.id()));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -11,6 +11,8 @@ 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;
import com.danyang.match.platform.shared.security.DataScopeGuard;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.user.entity.User;
import com.danyang.match.platform.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
@@ -40,6 +42,7 @@ public class CounselingServiceApp {
private final NotificationService notificationService;
private final UserMapper userMapper;
private final com.danyang.match.platform.risk.service.RiskReferralService riskReferralService;
private final DataScopeGuard dataScopeGuard;
/** P13A-T05: 获取咨询介绍 */
public List<Map<String, Object>> getServiceIntro() {
@@ -148,9 +151,10 @@ public class CounselingServiceApp {
/** P13A-T22: 安排时间 */
@Transactional
public CounselingDTO scheduleCounseling(String counselingId, Instant startAt, Instant endAt) {
public CounselingDTO scheduleCounseling(String counselingId, OperatorPrincipal principal, Instant startAt, Instant endAt) {
CounselingService cs = counselingMapper.selectById(counselingId);
if (cs == null) throw new RuntimeException("咨询不存在");
assertCanProcess(cs, principal);
cs.setStatus(Constants.COUNSELING_STATUS_SCHEDULED);
cs.setScheduledStartAt(startAt);
cs.setScheduledEndAt(endAt);
@@ -161,9 +165,10 @@ public class CounselingServiceApp {
/** P13A-T23: 开始咨询 */
@Transactional
public CounselingDTO startCounseling(String counselingId) {
public CounselingDTO startCounseling(String counselingId, OperatorPrincipal principal) {
CounselingService cs = counselingMapper.selectById(counselingId);
if (cs == null) throw new RuntimeException("咨询不存在");
assertCanProcess(cs, principal);
cs.setStatus(Constants.COUNSELING_STATUS_IN_PROGRESS);
cs.setUpdatedAt(Instant.now());
counselingMapper.updateById(cs);
@@ -172,13 +177,16 @@ public class CounselingServiceApp {
/** P13A-T24: 添加跟进记录 */
@Transactional
public FollowupDTO addFollowup(String counselingId, String operatorId,
public FollowupDTO addFollowup(String counselingId, OperatorPrincipal principal,
String followupType, String content,
String resultCode, Instant nextFollowupAt) {
CounselingService cs = counselingMapper.selectById(counselingId);
if (cs == null) throw new RuntimeException("咨询不存在");
assertCanProcess(cs, principal);
CounselingFollowup fu = new CounselingFollowup();
fu.setId(UUID.randomUUID().toString());
fu.setCounselingId(counselingId);
fu.setOperatorId(operatorId);
fu.setOperatorId(principal.id());
fu.setFollowupType(followupType);
fu.setContent(content);
fu.setResultCode(resultCode);
@@ -190,9 +198,10 @@ public class CounselingServiceApp {
/** P13A-T25: 完成咨询 */
@Transactional
public CounselingDTO completeCounseling(String counselingId) {
public CounselingDTO completeCounseling(String counselingId, OperatorPrincipal principal) {
CounselingService cs = counselingMapper.selectById(counselingId);
if (cs == null) throw new RuntimeException("咨询不存在");
assertCanProcess(cs, principal);
cs.setStatus(Constants.COUNSELING_STATUS_COMPLETED);
cs.setCompletedAt(Instant.now());
cs.setUpdatedAt(Instant.now());
@@ -207,9 +216,10 @@ public class CounselingServiceApp {
/** P13A-T26: 关闭咨询 */
@Transactional
public CounselingDTO closeCounseling(String counselingId, String reason) {
public CounselingDTO closeCounseling(String counselingId, OperatorPrincipal principal, String reason) {
CounselingService cs = counselingMapper.selectById(counselingId);
if (cs == null) throw new RuntimeException("咨询不存在");
assertCanProcess(cs, principal);
cs.setStatus(Constants.COUNSELING_STATUS_CLOSED);
cs.setClosedReason(reason);
cs.setUpdatedAt(Instant.now());
@@ -246,11 +256,17 @@ public class CounselingServiceApp {
if (op != null) dto.setAssignedOperatorName(op.getName());
}
List<CounselingFollowup> followups = followupMapper.findByCounselingId(cs.getId());
dto.setFollowups(followups.stream().map(this::toFollowupDTO).toList());
return dto;
}
private void assertCanProcess(CounselingService counseling, OperatorPrincipal principal) {
boolean counselingManager = principal.hasPermission("counseling.service.assign");
if (!counselingManager && !principal.id().equals(counseling.getAssignedOperatorId())) {
dataScopeGuard.deny(principal.id(), "counseling_service", counseling.getId(),
"只能处理分配给自己的咨询服务");
}
}
private FollowupDTO toFollowupDTO(CounselingFollowup fu) {
FollowupDTO dto = new FollowupDTO();
dto.setId(fu.getId());
@@ -1,9 +1,10 @@
package com.danyang.match.platform.discover.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.discover.service.DiscoverService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -19,7 +20,6 @@ import com.danyang.match.platform.profile.mapper.UserProfileMapper;
public class DiscoverController {
private final DiscoverService discoverService;
private final JwtTokenService jwtTokenService;
private final UserProfileMapper userProfileMapper;
/**
@@ -28,7 +28,7 @@ public class DiscoverController {
*/
@GetMapping
public ApiResponse<Map<String, Object>> search(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(required = false) String regionCode,
@@ -37,7 +37,7 @@ public class DiscoverController {
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize) {
String userId = extractUserId(authHeader);
String userId = principal.id();
Map<String, Object> myProfile = getMyProfile(userId);
String myGender = (String) myProfile.get("gender");
@@ -57,7 +57,7 @@ public class DiscoverController {
*/
@GetMapping("/users/{userId}")
public ApiResponse<Map<String, Object>> getProfile(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String userId) {
Map<String, Object> profile = discoverService.getPublicProfile(userId);
if (profile == null) {
@@ -66,18 +66,7 @@ public class DiscoverController {
return ApiResponse.ok(profile);
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
private Map<String, Object> getMyProfile(String userId) {
return userProfileMapper.findByUserId(userId)
@@ -1,10 +1,11 @@
package com.danyang.match.platform.home.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.home.dto.HomeDTO;
import com.danyang.match.platform.home.service.HomeApplicationService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
@@ -17,28 +18,16 @@ import org.springframework.web.bind.annotation.*;
public class HomeController {
private final HomeApplicationService homeApplicationService;
private final JwtTokenService jwtTokenService;
/**
* 获取首页聚合数据
* GET /api/v1/home
*/
@GetMapping
public ApiResponse<HomeDTO> getHome(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<HomeDTO> getHome(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(homeApplicationService.getHome(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,12 +1,13 @@
package com.danyang.match.platform.local_eligibility.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.local_eligibility.dto.LocalEligibilityDeclarationRequest;
import com.danyang.match.platform.local_eligibility.dto.LocalEligibilityEvidenceRequest;
import com.danyang.match.platform.local_eligibility.dto.LocalEligibilityStatusDTO;
import com.danyang.match.platform.local_eligibility.service.LocalEligibilityService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
@@ -18,26 +19,14 @@ import org.springframework.web.bind.annotation.*;
public class LocalEligibilityController {
private final LocalEligibilityService localEligibilityService;
private final JwtTokenService jwtTokenService;
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
/**
* GET /api/v1/local-eligibility — 查询本地资格状态
*/
@GetMapping
public ApiResponse<LocalEligibilityStatusDTO> getStatus(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<LocalEligibilityStatusDTO> getStatus(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(localEligibilityService.getStatus(userId));
}
@@ -46,9 +35,9 @@ public class LocalEligibilityController {
*/
@PostMapping("/declaration")
public ApiResponse<LocalEligibilityStatusDTO> declare(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody LocalEligibilityDeclarationRequest req) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(localEligibilityService.declare(userId, req));
}
@@ -57,9 +46,9 @@ public class LocalEligibilityController {
*/
@PostMapping("/evidences")
public ApiResponse<LocalEligibilityStatusDTO> uploadEvidence(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody LocalEligibilityEvidenceRequest req) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(localEligibilityService.uploadEvidence(userId, req));
}
@@ -68,8 +57,8 @@ public class LocalEligibilityController {
*/
@PostMapping("/submit")
public ApiResponse<LocalEligibilityStatusDTO> submit(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(localEligibilityService.submitForReview(userId));
}
}
@@ -1,10 +1,11 @@
package com.danyang.match.platform.match.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.match.dto.RecommendationCandidateDTO;
import com.danyang.match.platform.match.service.RecommendationEngineService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -19,7 +20,6 @@ import java.util.Map;
public class RecommendationController {
private final RecommendationEngineService engineService;
private final JwtTokenService jwtTokenService;
/**
* 获取今日推荐
@@ -27,8 +27,8 @@ public class RecommendationController {
*/
@GetMapping
public ApiResponse<List<RecommendationCandidateDTO>> getRecommendations(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(engineService.getDailyRecommendations(userId));
}
@@ -39,10 +39,10 @@ public class RecommendationController {
*/
@PostMapping("/{targetUserId}/action")
public ApiResponse<Void> recordAction(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String action = body.get("action");
if (action == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "action不能为空");
@@ -57,23 +57,12 @@ public class RecommendationController {
*/
@GetMapping("/{targetUserId}/eligibility")
public ApiResponse<Map<String, Boolean>> checkEligibility(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
boolean eligible = engineService.isStillEligible(userId, targetUserId);
return ApiResponse.ok(Map.of("eligible", eligible));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,11 @@
package com.danyang.match.platform.matchmaker.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.matchmaker.service.MatchmakerServiceService;
import com.danyang.match.platform.shared.dto.ApiResponse;
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.util.List;
@@ -14,53 +16,55 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/matchmaker")
@PreAuthorize("hasAuthority('matchmaker.view')")
@RequiredArgsConstructor
public class MatchmakerOperatorController {
private final MatchmakerServiceService matchmakerServiceService;
private final JwtTokenService jwtTokenService;
@GetMapping("/dashboard")
public ApiResponse<Map<String, Object>> getDashboard(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String operatorId = extractOperatorId(authHeader);
@AuthenticationPrincipal OperatorPrincipal principal) {
String operatorId = principal.id();
return ApiResponse.ok(matchmakerServiceService.getDashboard(operatorId));
}
@GetMapping("/pending")
public ApiResponse<List<Map<String, Object>>> getPending(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(matchmakerServiceService.getPendingServices());
}
@PostMapping("/services/{id}/assign")
@PreAuthorize("hasAuthority('matchmaker.assign')")
public ApiResponse<?> assignService(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String assignOperatorId = body.get("operatorId");
return ApiResponse.ok(matchmakerServiceService.assignService(id, assignOperatorId, null));
}
@PostMapping("/services/{id}/claim")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<?> claimService(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(matchmakerServiceService.claimService(operatorId, id));
}
@GetMapping("/my-services")
public ApiResponse<?> getMyServices(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String operatorId = extractOperatorId(authHeader);
@AuthenticationPrincipal OperatorPrincipal principal) {
String operatorId = principal.id();
return ApiResponse.ok(matchmakerServiceService.getMyServices(operatorId));
}
@GetMapping("/services/{id}")
public ApiResponse<Map<String, Object>> getServiceDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
Map<String, Object> detail = matchmakerServiceService.getServiceDetail(id);
if (detail == null) {
@@ -70,22 +74,24 @@ public class MatchmakerOperatorController {
}
@PostMapping("/services/{id}/recommend")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<Map<String, Object>> recommend(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String targetUserId = body.get("targetUserId");
String reason = body.getOrDefault("reason", "");
return ApiResponse.ok(matchmakerServiceService.recommendPartner(id, operatorId, targetUserId, reason));
}
@PostMapping("/services/{id}/followup")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<?> addFollowup(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String followupType = body.get("followupType");
String content = body.get("content");
String nextFollowupAt = body.get("nextFollowupAt");
@@ -93,21 +99,11 @@ public class MatchmakerOperatorController {
}
@PostMapping("/services/{id}/complete")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<?> completeService(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(matchmakerServiceService.completeService(id));
}
private String extractOperatorId(String authHeader) {
return extractToken(authHeader);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.BusinessException(
"UNAUTHORIZED");
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.matchmaker.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.matchmaker.service.MatchmakerServiceService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -18,7 +19,6 @@ import java.util.Map;
public class MatchmakerUserController {
private final MatchmakerServiceService matchmakerServiceService;
private final JwtTokenService jwtTokenService;
/**
* 申请红娘服务
@@ -27,9 +27,9 @@ public class MatchmakerUserController {
*/
@PostMapping
public ApiResponse<Map<String, Object>> apply(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String serviceType = body.get("serviceType");
String description = body.getOrDefault("description", "");
return ApiResponse.ok(matchmakerServiceService.applyService(userId, serviceType, description));
@@ -41,8 +41,8 @@ public class MatchmakerUserController {
*/
@GetMapping
public ApiResponse<List<Map<String, Object>>> getMyServices(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(matchmakerServiceService.getMyServices(userId));
}
@@ -52,9 +52,9 @@ public class MatchmakerUserController {
*/
@GetMapping("/{id}")
public ApiResponse<Map<String, Object>> getServiceDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
Map<String, Object> detail = matchmakerServiceService.getServiceDetail(id);
if (detail == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -68,24 +68,13 @@ public class MatchmakerUserController {
*/
@GetMapping("/recommendations")
public ApiResponse<List<Map<String, Object>>> getRecommendations(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
if (userId == null) {
return ApiResponse.ok(List.of());
}
return ApiResponse.ok(matchmakerServiceService.getRecommendationsForUser(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,14 +1,17 @@
package com.danyang.match.platform.messaging.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.messaging.dto.MessageDTO;
import com.danyang.match.platform.messaging.safety.MessageRiskService;
import com.danyang.match.platform.messaging.service.MessagingService;
import com.danyang.match.platform.photo.service.PhotoService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@@ -23,7 +26,6 @@ public class ChatController {
private final MessagingService messagingService;
private final MessageRiskService riskService;
private final JwtTokenService jwtTokenService;
private final PhotoService photoService;
/**
@@ -32,10 +34,10 @@ public class ChatController {
*/
@PostMapping(value = "/conversations/{id}/send", consumes = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<MessageDTO> sendTextMessage(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String content = body.get("content");
if (content == null || content.isBlank()) {
return ApiResponse.error(
@@ -61,9 +63,9 @@ public class ChatController {
*/
@PostMapping("/conversations/{id}/safety-tip")
public ApiResponse<Void> sendSafetyTip(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
messagingService.sendChatSafetyTip(id, userId);
return ApiResponse.ok();
}
@@ -74,9 +76,9 @@ public class ChatController {
*/
@PostMapping("/upload-token")
public ApiResponse<Map<String, Object>> getUploadToken(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String conversationId = body.get("conversationId");
// 复用 PhotoService 获取 MinIO presigned URL
return ApiResponse.ok(photoService.getUploadToken(userId, "chat"));
@@ -88,9 +90,9 @@ public class ChatController {
*/
@GetMapping("/files/{fileId}/url")
public ApiResponse<Map<String, Object>> getSignedUrl(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String fileId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(photoService.getSignedUrl(userId, fileId));
}
@@ -101,10 +103,10 @@ public class ChatController {
*/
@PostMapping("/conversations/{id}/send-image")
public ApiResponse<MessageDTO> sendImageMessage(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String fileKey = body.get("fileKey");
if (fileKey == null || fileKey.isBlank()) {
return ApiResponse.error(
@@ -115,16 +117,5 @@ public class ChatController {
return ApiResponse.ok(messagingService.sendImageMessage(userId, id, fileKey));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,12 @@
package com.danyang.match.platform.messaging.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.messaging.dto.ConversationDTO;
import com.danyang.match.platform.messaging.dto.MessageDTO;
import com.danyang.match.platform.messaging.service.MessagingService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -20,7 +21,6 @@ import java.util.Map;
public class MessageController {
private final MessagingService messagingService;
private final JwtTokenService jwtTokenService;
/**
* 获取我的会话列表
@@ -28,8 +28,8 @@ public class MessageController {
*/
@GetMapping("/conversations")
public ApiResponse<List<ConversationDTO>> getConversations(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(messagingService.getConversations(userId));
}
@@ -39,9 +39,9 @@ public class MessageController {
*/
@GetMapping("/conversations/{id}")
public ApiResponse<ConversationDTO> getConversation(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
ConversationDTO dto = messagingService.getConversation(userId, id);
if (dto == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -56,10 +56,10 @@ public class MessageController {
*/
@PostMapping("/conversations/{id}/send")
public ApiResponse<MessageDTO> sendMessage(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String content = body.get("content");
if (content == null || content.isBlank()) {
return ApiResponse.error(
@@ -76,11 +76,11 @@ public class MessageController {
*/
@GetMapping("/conversations/{id}/history")
public ApiResponse<Map<String, Object>> getHistory(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestParam(required = false) String cursor,
@RequestParam(defaultValue = "20") int limit) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(messagingService.getHistory(userId, id, cursor, limit));
}
@@ -91,10 +91,10 @@ public class MessageController {
*/
@PutMapping("/conversations/{id}/read")
public ApiResponse<Void> markAsRead(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String messageId = body.get("messageId");
messagingService.markAsRead(userId, id, messageId);
return ApiResponse.ok();
@@ -106,22 +106,11 @@ public class MessageController {
*/
@GetMapping("/unread-count")
public ApiResponse<Map<String, Object>> getUnreadCount(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
int count = messagingService.getTotalUnread(userId);
return ApiResponse.ok(Map.of("unreadCount", count));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -35,13 +35,13 @@ public class MessageRiskService {
"博彩", "赌博", "彩票", "六合彩",
"高收益", "稳赚", "内幕",
"刷单", "兼职", "快递费", "保证金",
"验证码", "密码", "身份证号",
"验证码", "密码", "身份证号", "加微信", "加你微信", "加我微信",
"诈骗", "骗子", "假冒"
);
// 普通风险关键词(仅记录,不提示)
private static final Set<String> MEDIUM_RISK_KEYWORDS = Set.of(
"电话", "加微信", "加QQ", "加群",
"电话", "加QQ", "加群",
"地址", "门牌", "小区", "楼栋",
"公司", "单位", "工作服"
);
@@ -1,10 +1,11 @@
package com.danyang.match.platform.notification.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.notification.dto.NotificationDTO;
import com.danyang.match.platform.notification.service.NotificationService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -19,7 +20,6 @@ import java.util.Map;
public class NotificationController {
private final NotificationService notificationService;
private final JwtTokenService jwtTokenService;
/**
* 获取通知列表
@@ -27,10 +27,10 @@ public class NotificationController {
*/
@GetMapping
public ApiResponse<Map<String, Object>> getNotifications(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize) {
String userId = extractUserId(authHeader);
String userId = principal.id();
List<NotificationDTO> list = notificationService.getNotifications(userId, page, pageSize);
int unreadCount = notificationService.getUnreadCount(userId);
Map<String, Object> result = new java.util.HashMap<>();
@@ -47,9 +47,9 @@ public class NotificationController {
*/
@PutMapping("/{id}/read")
public ApiResponse<Void> markAsRead(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
notificationService.markAsRead(userId, id);
return ApiResponse.ok();
}
@@ -60,8 +60,8 @@ public class NotificationController {
*/
@PutMapping("/read-all")
public ApiResponse<Map<String, Object>> markAllRead(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
int count = notificationService.markAllRead(userId);
return ApiResponse.ok(Map.of("markedCount", count));
}
@@ -72,21 +72,10 @@ public class NotificationController {
*/
@GetMapping("/unread-count")
public ApiResponse<Map<String, Object>> getUnreadCount(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(Map.of("unreadCount", notificationService.getUnreadCount(userId)));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,10 +1,12 @@
package com.danyang.match.platform.operator.controller;
import com.danyang.match.platform.operator.service.OperatorManagementService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.permission.entity.Operator;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -17,10 +19,10 @@ import java.util.Map;
@RestController
@RequestMapping("/operator/operators")
@RequiredArgsConstructor
@PreAuthorize("hasAuthority('permission.manage')")
public class OperatorController {
private final OperatorManagementService operatorService;
private final JwtTokenService jwtTokenService;
@GetMapping
public ApiResponse<Map<String, Object>> list(
@@ -42,27 +44,27 @@ public class OperatorController {
@PutMapping("/{id}/disable")
public ApiResponse<Void> disable(@PathVariable String id,
@RequestHeader("Authorization") String authHeader) {
operatorService.disableOperator(id, extractId(authHeader));
@AuthenticationPrincipal OperatorPrincipal principal) {
operatorService.disableOperator(id, principal.id());
return ApiResponse.ok();
}
@PutMapping("/{id}/enable")
public ApiResponse<Void> enable(@PathVariable String id,
@RequestHeader("Authorization") String authHeader) {
operatorService.enableOperator(id, extractId(authHeader));
@AuthenticationPrincipal OperatorPrincipal principal) {
operatorService.enableOperator(id, principal.id());
return ApiResponse.ok();
}
@PutMapping("/{id}/reset-password")
public ApiResponse<Void> resetPassword(@PathVariable String id,
@RequestBody Map<String, String> body,
@RequestHeader("Authorization") String authHeader) {
@AuthenticationPrincipal OperatorPrincipal principal) {
String newPassword = body.get("newPassword");
if (newPassword == null || newPassword.length() < 8) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "密码至少8位");
}
operatorService.resetPassword(id, newPassword, extractId(authHeader));
operatorService.resetPassword(id, newPassword, principal.id());
return ApiResponse.ok();
}
@@ -71,11 +73,4 @@ public class OperatorController {
return ApiResponse.ok(operatorService.listOrganizations());
}
private String extractId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
String token = authHeader.substring(7);
return jwtTokenService.getUserIdFromToken(token);
}
}
@@ -2,7 +2,10 @@ package com.danyang.match.platform.organization.controller;
import com.danyang.match.platform.organization.service.OrganizationService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
@@ -14,12 +17,13 @@ import java.util.Map;
@RestController
@RequestMapping("/operator/organizations")
@RequiredArgsConstructor
@PreAuthorize("hasAuthority('organization.manage')")
public class OrganizationController {
private final OrganizationService organizationService;
@GetMapping
public ApiResponse<List<?>> list(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<List<?>> list(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(organizationService.listOrganizations());
}
@@ -29,7 +33,7 @@ public class OrganizationController {
}
@PostMapping
public ApiResponse<?> create(@RequestHeader("Authorization") String authHeader,
public ApiResponse<?> create(@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> params) {
return ApiResponse.ok(organizationService.createOrganization(params));
}
@@ -43,22 +47,16 @@ public class OrganizationController {
@PutMapping("/{id}/disable")
public ApiResponse<Void> disable(@PathVariable String id,
@RequestHeader("Authorization") String authHeader) {
organizationService.disableOrganization(id, extractId(authHeader));
@AuthenticationPrincipal OperatorPrincipal principal) {
organizationService.disableOrganization(id, principal.id());
return ApiResponse.ok();
}
@PutMapping("/{id}/enable")
public ApiResponse<Void> enable(@PathVariable String id,
@RequestHeader("Authorization") String authHeader) {
organizationService.enableOrganization(id, extractId(authHeader));
@AuthenticationPrincipal OperatorPrincipal principal) {
organizationService.enableOrganization(id, principal.id());
return ApiResponse.ok();
}
private String extractId(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("未认证");
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,14 @@
package com.danyang.match.platform.photo.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.photo.service.PhotoService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -20,7 +23,6 @@ import java.util.Map;
public class PhotoController {
private final PhotoService photoService;
private final JwtTokenService jwtTokenService;
/**
* 获取上传凭证(供客户端直传到 MinIO)
@@ -28,9 +30,9 @@ public class PhotoController {
*/
@PostMapping("/upload-token")
public ApiResponse<Map<String, Object>> getUploadToken(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String photoType = body.getOrDefault("photoType", "lifestyle");
return ApiResponse.ok(photoService.getUploadToken(userId, photoType));
}
@@ -41,11 +43,11 @@ public class PhotoController {
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<?> uploadPhoto(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam("file") MultipartFile file,
@RequestParam("photoType") String photoType,
@RequestParam(value = "sortOrder", defaultValue = "0") int sortOrder) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(photoService.uploadAndSave(userId, file, photoType, sortOrder));
}
@@ -55,9 +57,9 @@ public class PhotoController {
*/
@GetMapping(value = "/{fileKey}/url", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<Map<String, Object>> getSignedUrl(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String fileKey) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(photoService.getSignedUrl(userId, fileKey));
}
@@ -67,8 +69,8 @@ public class PhotoController {
*/
@GetMapping
public ApiResponse<?> getPhotos(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(photoService.getPhotos(userId));
}
@@ -78,8 +80,8 @@ public class PhotoController {
*/
@GetMapping("/avatar")
public ApiResponse<?> getAvatar(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(photoService.getAvatar(userId));
}
@@ -89,9 +91,9 @@ public class PhotoController {
*/
@DeleteMapping("/{id}")
public ApiResponse<Void> deletePhoto(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
// 权限校验:只允许删除自己的照片
List<?> userPhotos = photoService.getPhotos(userId);
boolean owned = userPhotos.stream()
@@ -104,16 +106,5 @@ public class PhotoController {
return ApiResponse.ok();
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.profile.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.profile.service.InterestService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -18,15 +19,14 @@ import java.util.Map;
public class InterestController {
private final InterestService interestService;
private final JwtTokenService jwtTokenService;
/**
* 获取兴趣
* GET /api/v1/profile/interests
*/
@GetMapping
public ApiResponse<List<String>> getInterests(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<List<String>> getInterests(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(interestService.getInterests(userId));
}
@@ -36,24 +36,13 @@ public class InterestController {
* Body: { "interests": ["travel", "reading"] }
*/
@PutMapping
public ApiResponse<Void> setInterests(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> setInterests(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, List<String>> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
List<String> interests = body.get("interests");
interestService.setInterests(userId, interests);
return ApiResponse.ok();
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.profile.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.profile.service.UserPreferenceService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -17,15 +18,14 @@ import java.util.Map;
public class PreferenceController {
private final UserPreferenceService preferenceService;
private final JwtTokenService jwtTokenService;
/**
* 获取择偶条件
* GET /api/v1/profile/preferences
*/
@GetMapping
public ApiResponse<Map<String, Object>> getPreferences(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Map<String, Object>> getPreferences(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(preferenceService.getPreference(userId));
}
@@ -34,23 +34,12 @@ public class PreferenceController {
* PUT /api/v1/profile/preferences
*/
@PutMapping
public ApiResponse<Void> savePreferences(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> savePreferences(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> data) {
String userId = extractUserId(authHeader);
String userId = principal.id();
preferenceService.savePreference(userId, data);
return ApiResponse.ok();
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.profile.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.profile.service.PrivacyService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -17,7 +18,6 @@ import java.util.Map;
public class PrivacyController {
private final PrivacyService privacyService;
private final JwtTokenService jwtTokenService;
/**
* 获取隐私设置
@@ -25,8 +25,8 @@ public class PrivacyController {
*/
@GetMapping
public ApiResponse<Map<String, Boolean>> getPrivacy(
@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(privacyService.getPrivacySettings(userId));
}
@@ -36,22 +36,11 @@ public class PrivacyController {
*/
@PutMapping
public ApiResponse<Map<String, Boolean>> updatePrivacy(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Boolean> settings) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(privacyService.updatePrivacySettings(userId, settings));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,12 @@
package com.danyang.match.platform.profile.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.profile.dto.PublicUserProfileDTO;
import com.danyang.match.platform.profile.dto.SelfProfileDTO;
import com.danyang.match.platform.profile.service.UserProfileService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -19,15 +20,14 @@ import java.util.Map;
public class ProfileController {
private final UserProfileService userProfileService;
private final JwtTokenService jwtTokenService;
/**
* 获取自己资料
* GET /api/v1/profile/me
*/
@GetMapping("/me")
public ApiResponse<SelfProfileDTO> getMyProfile(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<SelfProfileDTO> getMyProfile(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(userProfileService.getSelfProfile(userId));
}
@@ -36,9 +36,9 @@ public class ProfileController {
* PUT /api/v1/profile/me
*/
@PutMapping("/me")
public ApiResponse<Void> updateMyProfile(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> updateMyProfile(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> fields) {
String userId = extractUserId(authHeader);
String userId = principal.id();
userProfileService.updateProfile(userId, fields);
return ApiResponse.ok();
}
@@ -48,9 +48,9 @@ public class ProfileController {
* PATCH /api/v1/profile/basic
*/
@PatchMapping("/basic")
public ApiResponse<?> updateBasicProfile(@RequestHeader("Authorization") String authHeader,
public ApiResponse<?> updateBasicProfile(@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> fields) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(userProfileService.updateBasicProfile(userId, fields));
}
@@ -59,8 +59,8 @@ public class ProfileController {
* GET /api/v1/profile/completion
*/
@GetMapping("/completion")
public ApiResponse<Map<String, Object>> getCompletion(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Map<String, Object>> getCompletion(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
int completion = userProfileService.getCompletionPercentage(userId);
Map<String, Object> result = Map.of("completion", completion);
return ApiResponse.ok(result);
@@ -72,9 +72,9 @@ public class ProfileController {
*/
@GetMapping("/{userId}")
public ApiResponse<PublicUserProfileDTO> getPublicProfile(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String userId) {
String currentUserId = extractUserId(authHeader);
String currentUserId = principal.id();
PublicUserProfileDTO profile = userProfileService.getPublicProfile(currentUserId, userId);
if (profile == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND, "用户不存在");
@@ -82,16 +82,5 @@ public class ProfileController {
return ApiResponse.ok(profile);
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,10 +1,12 @@
package com.danyang.match.platform.referral.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.referral.service.ReferralService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
@@ -21,7 +23,6 @@ import java.util.Map;
public class ReferralController {
private final ReferralService referralService;
private final JwtTokenService jwtTokenService;
/**
* P13-T03: 红娘转介用户到服务站预约
@@ -29,10 +30,11 @@ public class ReferralController {
* 仅 matchmaker 角色可调用
*/
@PostMapping("/service-station")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<Map<String, Object>> referToServiceStation(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, String> body) {
String matchmakerId = extractUserId(authHeader);
String matchmakerId = principal.id();
String userId = body.get("userId");
String locationId = body.get("locationId");
String serviceType = body.getOrDefault("serviceType", "counselor_consultation");
@@ -48,10 +50,11 @@ public class ReferralController {
* POST /api/v1/referrals/activity
*/
@PostMapping("/activity")
@PreAuthorize("hasAuthority('matchmaker.service')")
public ApiResponse<Map<String, Object>> referToActivity(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, String> body) {
String matchmakerId = extractUserId(authHeader);
String matchmakerId = principal.id();
String userId = body.get("userId");
String activityId = body.get("activityId");
return ApiResponse.ok(referralService.referToActivity(matchmakerId, userId, activityId,
@@ -65,10 +68,10 @@ public class ReferralController {
*/
@PostMapping("/{id}/confirm")
public ApiResponse<Map<String, Object>> confirmReferral(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetType = body.getOrDefault("targetType", "service_station");
return ApiResponse.ok(referralService.confirmReferral(userId, id, targetType));
}
@@ -79,21 +82,10 @@ public class ReferralController {
*/
@GetMapping("/my-history")
public ApiResponse<List<Map<String, Object>>> getMyReferralHistory(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(referralService.getReferralHistory(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,12 @@
package com.danyang.match.platform.relationship.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.relationship.dto.FateItemDTO;
import com.danyang.match.platform.relationship.dto.MatchDTO;
import com.danyang.match.platform.relationship.service.RelationshipService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -20,16 +21,15 @@ import java.util.Map;
public class RelationshipController {
private final RelationshipService relationshipService;
private final JwtTokenService jwtTokenService;
/**
* 表达喜欢
* POST /api/v1/relations/{targetUserId}/like
*/
@PostMapping("/{targetUserId}/like")
public ApiResponse<Map<String, Object>> like(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Map<String, Object>> like(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
try {
return ApiResponse.ok(relationshipService.like(userId, targetUserId));
} catch (RuntimeException e) {
@@ -42,9 +42,9 @@ public class RelationshipController {
* DELETE /api/v1/relations/{targetUserId}/like
*/
@DeleteMapping("/{targetUserId}/like")
public ApiResponse<Void> cancelLike(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> cancelLike(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
relationshipService.cancelLike(userId, targetUserId);
return ApiResponse.ok();
}
@@ -54,9 +54,9 @@ public class RelationshipController {
* POST /api/v1/relations/{targetUserId}/pass
*/
@PostMapping("/{targetUserId}/pass")
public ApiResponse<Void> pass(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> pass(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
relationshipService.pass(userId, targetUserId);
return ApiResponse.ok();
}
@@ -66,9 +66,9 @@ public class RelationshipController {
* POST /api/v1/relations/{targetUserId}/block
*/
@PostMapping("/{targetUserId}/block")
public ApiResponse<Void> block(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> block(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
relationshipService.block(userId, targetUserId);
return ApiResponse.ok();
}
@@ -78,9 +78,9 @@ public class RelationshipController {
* DELETE /api/v1/relations/{targetUserId}/block
*/
@DeleteMapping("/{targetUserId}/block")
public ApiResponse<Void> unblock(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> unblock(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
relationshipService.unblock(userId, targetUserId);
return ApiResponse.ok();
}
@@ -90,8 +90,8 @@ public class RelationshipController {
* GET /api/v1/relations/matches
*/
@GetMapping("/matches")
public ApiResponse<List<MatchDTO>> getMyMatches(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<List<MatchDTO>> getMyMatches(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(relationshipService.getMyMatches(userId));
}
@@ -100,9 +100,9 @@ public class RelationshipController {
* POST /api/v1/relations/matches/{targetUserId}/unmatch
*/
@PostMapping("/matches/{targetUserId}/unmatch")
public ApiResponse<Void> unmatch(@RequestHeader("Authorization") String authHeader,
public ApiResponse<Void> unmatch(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String targetUserId) {
String userId = extractUserId(authHeader);
String userId = principal.id();
relationshipService.unmatch(userId, targetUserId);
return ApiResponse.ok();
}
@@ -112,8 +112,8 @@ public class RelationshipController {
* GET /api/v1/relations/fate/mutual
*/
@GetMapping("/fate/mutual")
public ApiResponse<List<FateItemDTO>> getMutual(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<List<FateItemDTO>> getMutual(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(relationshipService.getMutualList(userId));
}
@@ -122,8 +122,8 @@ public class RelationshipController {
* GET /api/v1/relations/fate/liked
*/
@GetMapping("/fate/liked")
public ApiResponse<List<FateItemDTO>> getLiked(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<List<FateItemDTO>> getLiked(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(relationshipService.getLikedList(userId));
}
@@ -132,8 +132,8 @@ public class RelationshipController {
* GET /api/v1/relations/fate/likers
*/
@GetMapping("/fate/likers")
public ApiResponse<List<FateItemDTO>> getLikers(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<List<FateItemDTO>> getLikers(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(relationshipService.getLikersList(userId));
}
@@ -143,9 +143,9 @@ public class RelationshipController {
*/
@PostMapping("/success/request")
public ApiResponse<Map<String, Object>> requestSuccess(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetUserId = body.get("targetUserId");
return ApiResponse.ok(relationshipService.requestSuccess(userId, targetUserId));
}
@@ -156,24 +156,13 @@ public class RelationshipController {
*/
@PostMapping("/success/confirm")
public ApiResponse<Void> confirmSuccess(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetUserId = body.get("targetUserId");
relationshipService.confirmSuccess(userId, targetUserId);
return ApiResponse.ok();
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,12 @@
package com.danyang.match.platform.relationship.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.relationship.service.RelationshipStatusService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.user.entity.User;
import com.danyang.match.platform.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -19,7 +20,6 @@ import java.util.Map;
public class RelationshipStatusController {
private final RelationshipStatusService relationshipStatusService;
private final JwtTokenService jwtTokenService;
private final UserMapper userMapper;
/**
@@ -27,8 +27,8 @@ public class RelationshipStatusController {
* GET /api/v1/relationship/status
*/
@GetMapping("/status")
public ApiResponse<Map<String, String>> getStatus(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Map<String, String>> getStatus(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
User user = userMapper.findById(userId).orElse(null);
String status = user != null ? user.getRelationshipStatus() : "searching";
Map<String, String> result = Map.of("relationshipStatus", status);
@@ -40,8 +40,8 @@ public class RelationshipStatusController {
* PUT /api/v1/relationship/pause
*/
@PutMapping("/pause")
public ApiResponse<Void> pause(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Void> pause(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
relationshipStatusService.pauseSearching(userId);
return ApiResponse.ok();
}
@@ -51,8 +51,8 @@ public class RelationshipStatusController {
* PUT /api/v1/relationship/resume
*/
@PutMapping("/resume")
public ApiResponse<Void> resume(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Void> resume(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
relationshipStatusService.resumeSearching(userId);
return ApiResponse.ok();
}
@@ -64,9 +64,9 @@ public class RelationshipStatusController {
*/
@PutMapping("/getting-to-know")
public ApiResponse<Void> setGettingToKnow(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Boolean> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
boolean autoPause = body.getOrDefault("autoPause", false);
relationshipStatusService.setGettingToKnow(userId, autoPause);
return ApiResponse.ok();
@@ -79,9 +79,9 @@ public class RelationshipStatusController {
*/
@PostMapping("/success/request")
public ApiResponse<Map<String, Object>> requestSuccess(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetUserId = body.get("targetUserId");
if (targetUserId == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "targetUserId不能为空");
@@ -96,9 +96,9 @@ public class RelationshipStatusController {
*/
@PostMapping("/success/confirm")
public ApiResponse<Map<String, Object>> confirmSuccess(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetUserId = body.get("targetUserId");
if (targetUserId == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "targetUserId不能为空");
@@ -113,9 +113,9 @@ public class RelationshipStatusController {
*/
@PostMapping("/success/decline")
public ApiResponse<Void> declineSuccess(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String targetUserId = body.get("targetUserId");
relationshipStatusService.declineSuccess(userId, targetUserId);
return ApiResponse.ok();
@@ -126,22 +126,11 @@ public class RelationshipStatusController {
* PUT /api/v1/relationship/exit
*/
@PutMapping("/exit")
public ApiResponse<Void> exitService(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Void> exitService(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
relationshipStatusService.exitService(userId);
return ApiResponse.ok();
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,13 @@
package com.danyang.match.platform.review.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.review.dto.ReviewTaskDetailDTO;
import com.danyang.match.platform.review.dto.ReviewResultDTO;
import com.danyang.match.platform.review.service.ReviewService;
import com.danyang.match.platform.shared.dto.ApiResponse;
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.util.List;
@@ -16,11 +18,11 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/reviews")
@PreAuthorize("hasAuthority('review.view')")
@RequiredArgsConstructor
public class ReviewController {
private final ReviewService reviewService;
private final JwtTokenService jwtTokenService;
/**
* 获取待审核统计
@@ -28,14 +30,14 @@ public class ReviewController {
*/
@GetMapping
public ApiResponse<List<Map<String, Object>>> list(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return listPending(authHeader, page, pageSize);
return listPending(principal, page, pageSize);
}
@GetMapping("/stats")
public ApiResponse<Map<String, Long>> getStats(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Map<String, Long>> getStats(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(reviewService.getPendingStats());
}
@@ -45,7 +47,7 @@ public class ReviewController {
*/
@GetMapping("/pending")
public ApiResponse<List<Map<String, Object>>> listPending(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
// TODO: Phase 05 后续补充完整分页
@@ -57,10 +59,11 @@ public class ReviewController {
* POST /api/v1/operator/reviews/{taskId}/claim
*/
@PostMapping("/{taskId}/claim")
@PreAuthorize("hasAuthority('review.process')")
public ApiResponse<Map<String, Object>> claimTask(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String taskId) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return reviewService.claimTask(operatorId, taskId)
.map(task -> ApiResponse.ok(java.util.Map.of("taskId", task.getId(), "status", (Object)task.getStatus())))
.orElse(ApiResponse.error(
@@ -74,7 +77,7 @@ public class ReviewController {
*/
@GetMapping("/{taskId}")
public ApiResponse<ReviewTaskDetailDTO> getTaskDetail(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String taskId) {
ReviewTaskDetailDTO detail = reviewService.getTaskDetail(taskId);
if (detail == null) {
@@ -88,11 +91,12 @@ public class ReviewController {
* POST /api/v1/operator/reviews/{taskId}/approve
*/
@PostMapping("/{taskId}/approve")
@PreAuthorize("hasAuthority('review.process')")
public ApiResponse<ReviewResultDTO> approve(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String taskId,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String comment = body.getOrDefault("comment", "");
return ApiResponse.ok(reviewService.approve(taskId, operatorId, comment));
}
@@ -102,11 +106,12 @@ public class ReviewController {
* POST /api/v1/operator/reviews/{taskId}/need-edit
*/
@PostMapping("/{taskId}/need-edit")
@PreAuthorize("hasAuthority('review.process')")
public ApiResponse<ReviewResultDTO> needEdit(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String taskId,
@RequestBody Map<String, Object> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String comment = (String) body.get("comment");
@SuppressWarnings("unchecked")
List<String> reasonCodes = (List<String>) body.getOrDefault("reasonCodes", List.of());
@@ -118,11 +123,12 @@ public class ReviewController {
* POST /api/v1/operator/reviews/{taskId}/reject
*/
@PostMapping("/{taskId}/reject")
@PreAuthorize("hasAuthority('review.process')")
public ApiResponse<ReviewResultDTO> reject(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String taskId,
@RequestBody Map<String, Object> body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
String comment = (String) body.get("comment");
@SuppressWarnings("unchecked")
List<String> reasonCodes = (List<String>) body.getOrDefault("reasonCodes", List.of());
@@ -139,16 +145,4 @@ public class ReviewController {
return ApiResponse.ok(List.of());
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return token; // 从 JWT subject 提取
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.review.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.review.service.ReviewService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -17,15 +18,14 @@ import java.util.Map;
public class UserReviewController {
private final ReviewService reviewService;
private final JwtTokenService jwtTokenService;
/**
* 获取用户当前审核状态
* GET /api/v1/review/status
*/
@GetMapping("/status")
public ApiResponse<Map<String, Object>> getReviewStatus(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Map<String, Object>> getReviewStatus(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
var history = reviewService.getReviewHistory(userId);
Map<String, Object> result = Map.of("history", history);
return ApiResponse.ok(result);
@@ -36,21 +36,10 @@ public class UserReviewController {
* POST /api/v1/review/resubmit
*/
@PostMapping("/resubmit")
public ApiResponse<Map<String, Object>> resubmit(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Map<String, Object>> resubmit(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(Map.of("taskId", reviewService.resubmit(userId).getId()));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return token;
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,12 @@
package com.danyang.match.platform.risk.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.risk.dto.ReportProcessDTO;
import com.danyang.match.platform.risk.dto.ReportSubmitDTO;
import com.danyang.match.platform.risk.service.ReportService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -19,7 +20,6 @@ import java.util.Map;
public class ReportController {
private final ReportService reportService;
private final JwtTokenService jwtTokenService;
/**
* 提交举报
@@ -27,9 +27,9 @@ public class ReportController {
*/
@PostMapping
public ApiResponse<Map<String, Object>> submitReport(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody ReportSubmitDTO body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(reportService.submitReport(userId, body));
}
@@ -38,21 +38,10 @@ public class ReportController {
* GET /api/v1/risk/reports/my
*/
@GetMapping("/my")
public ApiResponse<Object> getMyReports(@RequestHeader("Authorization") String authHeader) {
String userId = extractUserId(authHeader);
public ApiResponse<Object> getMyReports(@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(reportService.getMyReports(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,10 +1,12 @@
package com.danyang.match.platform.risk.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.risk.dto.ReportProcessDTO;
import com.danyang.match.platform.risk.service.ReportService;
import com.danyang.match.platform.shared.dto.ApiResponse;
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.util.Map;
@@ -14,11 +16,11 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/risk/reports")
@PreAuthorize("hasAuthority('risk.report.view')")
@RequiredArgsConstructor
public class ReportOperatorController {
private final ReportService reportService;
private final JwtTokenService jwtTokenService;
/**
* 获取举报列表(可带 status 筛选)
@@ -26,7 +28,7 @@ public class ReportOperatorController {
*/
@GetMapping
public ApiResponse<Object> listReports(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestParam(required = false) String status) {
if (status != null && !status.isEmpty()) {
return ApiResponse.ok(reportService.getReportsByStatus(status));
@@ -39,17 +41,17 @@ public class ReportOperatorController {
* GET /api/v1/operator/risk/reports/pending
*/
@GetMapping("/pending")
public ApiResponse<Object> getPendingReports(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Object> getPendingReports(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(reportService.getPendingReports());
}
@GetMapping("/processing")
public ApiResponse<Object> getProcessingReports(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Object> getProcessingReports(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(reportService.getReportsByStatus("processing"));
}
@GetMapping("/resolved")
public ApiResponse<Object> getResolvedReports(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Object> getResolvedReports(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(reportService.getReportsByStatus("resolved"));
}
@@ -59,7 +61,7 @@ public class ReportOperatorController {
*/
@GetMapping("/{id}")
public ApiResponse<Map<String, Object>> getReportDetail(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(reportService.getReportDetail(id));
}
@@ -71,11 +73,12 @@ public class ReportOperatorController {
* "processNote": "...", "riskReasons": [...] }
*/
@PostMapping("/{id}/process")
@PreAuthorize("hasAuthority('risk.report.process')")
public ApiResponse<Map<String, Object>> processReport(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody ReportProcessDTO body) {
String operatorId = extractOperatorId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(reportService.processReport(id, body, operatorId));
}
@@ -84,7 +87,7 @@ public class ReportOperatorController {
* GET /api/v1/operator/risk/reports/stats
*/
@GetMapping("/stats")
public ApiResponse<Map<String, Object>> getReportStats(@RequestHeader("Authorization") String authHeader) {
public ApiResponse<Map<String, Object>> getReportStats(@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(reportService.getReportStats());
}
@@ -94,21 +97,9 @@ public class ReportOperatorController {
*/
@GetMapping("/users/{userId}")
public ApiResponse<Map<String, Object>> getUserRiskProfile(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
return ApiResponse.ok(reportService.getUserRiskProfile(userId));
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return token;
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,10 +1,12 @@
package com.danyang.match.platform.risk.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.risk.entity.RiskEvent;
import com.danyang.match.platform.risk.service.RiskReferralService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
@@ -20,17 +22,17 @@ import java.util.Map;
public class RiskController {
private final RiskReferralService riskReferralService;
private final JwtTokenService jwtTokenService;
/**
* P17-T04: 运营人员手动标记风险
* POST /api/v1/operator/risk/flag
*/
@PostMapping("/operator/flag")
@PreAuthorize("hasAuthority('risk.report.process')")
public ApiResponse<RiskEvent> manualFlagRisk(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, String> body) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
String userId = body.get("userId");
String eventType = body.getOrDefault("eventType", "manual_flag");
String note = body.getOrDefault("note", "");
@@ -42,22 +44,11 @@ public class RiskController {
* GET /api/v1/operator/risk/events/{userId}
*/
@GetMapping("/operator/events/{userId}")
@PreAuthorize("hasAuthority('risk.report.view')")
public ApiResponse<List<RiskEvent>> getRiskEvents(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String userId) {
return ApiResponse.ok(riskReferralService.getRiskEventsByUser(userId));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,11 +1,14 @@
package com.danyang.match.platform.service_appointment.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.service_appointment.dto.ServiceAppointmentDTO;
import com.danyang.match.platform.service_appointment.dto.ServiceAppointmentRequest;
import com.danyang.match.platform.service_appointment.service.ServiceAppointmentService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
@@ -21,31 +24,30 @@ import java.util.Map;
public class ServiceAppointmentController {
private final ServiceAppointmentService serviceAppointmentService;
private final JwtTokenService jwtTokenService;
// ---- 用户端 ----
@PostMapping
public ApiResponse<ServiceAppointmentDTO> create(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody ServiceAppointmentRequest request) {
// Use userId from request body (for operator-assisted creation) or extract from token
String userId = request.getUserId() != null ? request.getUserId() : extractUserId(authHeader);
// 用户端只能为当前登录用户创建;代办登记必须走独立运营权限端点。
String userId = principal.id();
return ApiResponse.ok(serviceAppointmentService.createAppointment(userId, request));
}
@GetMapping
public ApiResponse<List<ServiceAppointmentDTO>> getMyAppointments(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(serviceAppointmentService.getMyAppointments(userId));
}
@GetMapping("/{id}")
public ApiResponse<ServiceAppointmentDTO> getDetail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
ServiceAppointmentDTO detail = serviceAppointmentService.getAppointmentDetail(id, userId);
if (detail == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -55,34 +57,37 @@ public class ServiceAppointmentController {
@PostMapping("/{id}/cancel")
public ApiResponse<ServiceAppointmentDTO> cancel(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String userId = extractUserId(authHeader);
String userId = principal.id();
String reason = body.getOrDefault("reason", "");
return ApiResponse.ok(serviceAppointmentService.cancelAppointment(id, userId, reason));
}
// ---- 后台端 ----
@PreAuthorize("hasAuthority('service_appointment.view')")
@GetMapping("/operator/list")
public ApiResponse<List<ServiceAppointmentDTO>> getPendingForOperator(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@RequestParam String operatorId) {
// 后台查询,仅运营人员可访问
return ApiResponse.ok(serviceAppointmentService.getMyAppointments(operatorId));
@AuthenticationPrincipal OperatorPrincipal principal) {
return ApiResponse.ok(serviceAppointmentService.getOperatorAppointments(principal.id()));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
@PreAuthorize("hasAuthority('service_appointment.confirm')")
@PostMapping("/operator/{id}/confirm")
public ApiResponse<ServiceAppointmentDTO> confirm(
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(serviceAppointmentService.confirmAppointment(id, principal.id()));
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
@PreAuthorize("hasAuthority('service_appointment.complete')")
@PostMapping("/operator/{id}/complete")
public ApiResponse<ServiceAppointmentDTO> complete(
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(serviceAppointmentService.completeAppointment(id, principal.id()));
}
}
@@ -19,4 +19,16 @@ public interface ServiceAppointmentMapper extends BaseMapper<ServiceAppointment>
@Select("SELECT * FROM service_appointments WHERE service_location_id = #{locationId} AND status = 'pending_confirmation' AND deleted_at IS NULL ORDER BY preferred_date ASC")
List<ServiceAppointment> findPendingByLocation(@Param("locationId") String locationId);
@Select("SELECT DISTINCT sa.* FROM service_appointments sa " +
"LEFT JOIN service_location_operators slo ON slo.service_location_id = sa.service_location_id " +
"WHERE sa.deleted_at IS NULL AND (sa.assigned_operator_id = #{operatorId} OR slo.operator_id = #{operatorId}) " +
"ORDER BY sa.created_at DESC")
List<ServiceAppointment> findAccessibleByOperator(@Param("operatorId") String operatorId);
@Select("SELECT EXISTS (SELECT 1 FROM service_appointments sa " +
"LEFT JOIN service_location_operators slo ON slo.service_location_id = sa.service_location_id " +
"WHERE sa.id = #{appointmentId} AND sa.deleted_at IS NULL " +
"AND (sa.assigned_operator_id = #{operatorId} OR slo.operator_id = #{operatorId}))")
boolean canAccessByOperator(@Param("appointmentId") String appointmentId,
@Param("operatorId") String operatorId);
}
@@ -8,6 +8,7 @@ import com.danyang.match.platform.service_appointment.mapper.ServiceAppointmentM
import com.danyang.match.platform.service_appointment.entity.ServiceAppointment;
import com.danyang.match.platform.service_appointment.mapper.ServiceLocationMapper;
import com.danyang.match.platform.shared.constant.Constants;
import com.danyang.match.platform.shared.security.DataScopeGuard;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -32,6 +33,7 @@ public class ServiceAppointmentService {
private final ServiceLocationMapper serviceLocationMapper;
private final NotificationService notificationService;
private final com.danyang.match.platform.risk.service.RiskReferralService riskReferralService;
private final DataScopeGuard dataScopeGuard;
/**
* 用户提交预约
@@ -91,12 +93,18 @@ public class ServiceAppointmentService {
.toList();
}
/** 获取当前服务站人员被分配的任务及其明确所属站点预约。 */
public List<ServiceAppointmentDTO> getOperatorAppointments(String operatorId) {
return appointmentMapper.findAccessibleByOperator(operatorId).stream()
.map(this::toDTO)
.toList();
}
/**
* 获取预约详情
*/
public ServiceAppointmentDTO getAppointmentDetail(String appointmentId, String viewerUserId) {
ServiceAppointment appt = appointmentMapper.selectById(appointmentId);
if (appt == null) return null;
if (appt == null || !viewerUserId.equals(appt.getUserId())) return null;
return toDTO(appt);
}
@@ -109,6 +117,7 @@ public class ServiceAppointmentService {
if (appt == null || !"submitted".equals(appt.getStatus())) {
throw new RuntimeException("预约不存在或状态不正确");
}
assertOperatorScope(appointmentId, operatorId);
appt.setStatus(Constants.APPOINTMENT_STATUS_CONFIRMED);
appt.setAssignedOperatorId(operatorId);
appt.setConfirmedStartAt(Instant.now());
@@ -132,6 +141,7 @@ public class ServiceAppointmentService {
if (appt == null || !"confirmed".equals(appt.getStatus())) {
throw new RuntimeException("预约状态不正确");
}
assertOperatorScope(appointmentId, operatorId);
appt.setStatus(Constants.APPOINTMENT_STATUS_COMPLETED);
appt.setUpdatedAt(Instant.now());
appointmentMapper.updateById(appt);
@@ -154,6 +164,12 @@ public class ServiceAppointmentService {
return toDTO(appt);
}
private void assertOperatorScope(String appointmentId, String operatorId) {
if (operatorId == null || !appointmentMapper.canAccessByOperator(appointmentId, operatorId)) {
dataScopeGuard.deny(operatorId, "service_appointment", appointmentId,
"只能处理所属服务站或分配给自己的预约任务");
}
}
private ServiceAppointmentDTO toDTO(ServiceAppointment appt) {
ServiceAppointmentDTO dto = new ServiceAppointmentDTO();
dto.setId(appt.getId());
@@ -2,9 +2,12 @@ package com.danyang.match.platform.service_program.controller;
import com.danyang.match.platform.service_program.dto.ServiceProgramDTO;
import com.danyang.match.platform.service_program.dto.ServiceProgramMemberDTO;
import com.danyang.match.platform.service_program.service.ServiceProgramService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.List;
import java.util.Map;
@@ -13,18 +16,17 @@ import java.util.Map;
@RequiredArgsConstructor
public class ServiceProgramController {
private final ServiceProgramService serviceProgramService;
private final JwtTokenService jwtTokenService;
@GetMapping
public ApiResponse<List<ServiceProgramDTO>> list(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(required = false) String status) {
return ApiResponse.ok(serviceProgramService.getAllPrograms(status));
}
@GetMapping("/{id}")
public ApiResponse<ServiceProgramDTO> detail(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
ServiceProgramDTO d = serviceProgramService.getProgramDetail(id);
if (d == null) return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.NOT_FOUND);
@@ -34,74 +36,69 @@ public class ServiceProgramController {
// ---- 用户端 ----
@PostMapping("/{id}/apply")
public ApiResponse<ServiceProgramMemberDTO> apply(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable String id) {
String userId = extractUserId(authHeader);
String userId = principal.id();
return ApiResponse.ok(serviceProgramService.applyToProgram(userId, id));
}
@GetMapping("/my")
public ApiResponse<List<ServiceProgramMemberDTO>> getMyApplications(
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String userId = extractUserId(authHeader);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(serviceProgramService.getMyProgramMembers(userId));
}
// ---- 后台 ----
@PostMapping("/operator")
@PreAuthorize("hasAuthority('service_program.manage')")
public ApiResponse<ServiceProgramDTO> create(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestBody Map<String, Object> body) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(serviceProgramService.createProgram(body, operatorId));
}
@PutMapping("/operator/{id}")
@PreAuthorize("hasAuthority('service_program.manage')")
public ApiResponse<ServiceProgramDTO> update(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id, @RequestBody Map<String, Object> body) {
return ApiResponse.ok(serviceProgramService.updateProgram(id, body));
}
@PostMapping("/operator/{id}/activate")
@PreAuthorize("hasAuthority('service_program.manage')")
public ApiResponse<ServiceProgramDTO> activate(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(serviceProgramService.activateProgram(id));
}
@GetMapping("/operator/{id}/members")
@PreAuthorize("hasAuthority('service_program.view')")
public ApiResponse<List<ServiceProgramMemberDTO>> getMembers(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
return ApiResponse.ok(serviceProgramService.getProgramMembers(id));
}
@PostMapping("/operator/members/{memberId}/approve")
@PreAuthorize("hasAuthority('service_program.manage')")
public ApiResponse<ServiceProgramMemberDTO> approveMember(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String memberId) {
String operatorId = extractUserId(authHeader);
String operatorId = principal.id();
return ApiResponse.ok(serviceProgramService.approveMember(memberId, operatorId));
}
@PostMapping("/operator/members/{memberId}/reject")
@PreAuthorize("hasAuthority('service_program.manage')")
public ApiResponse<ServiceProgramMemberDTO> rejectMember(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String memberId,
@RequestBody Map<String, String> body) {
return ApiResponse.ok(serviceProgramService.rejectMember(memberId, body.getOrDefault("reason", "")));
}
private String extractUserId(String authHeader) {
String token = extractToken(authHeader);
return jwtTokenService.getUserIdFromToken(token);
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -2,21 +2,30 @@ package com.danyang.match.platform.shared.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.danyang.match.platform.shared.filter.JwtAuthFilter;
import com.danyang.match.platform.shared.filter.PermissionFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher publisher) {
return new SpringAuthorizationEventPublisher(publisher);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -24,29 +33,27 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtAuthFilter jwtAuthFilter,
PermissionFilter permissionFilter) throws Exception {
JwtAuthFilter jwtAuthFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 公开接口(order=0 最高优先级)
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/operator/auth/**").permitAll()
.requestMatchers("/api/v1/health/**").permitAll()
.requestMatchers("/api/v1/version").permitAll()
.requestMatchers("/api/v1/activities").permitAll()
.requestMatchers("/auth/wechat/login", "/auth/refresh", "/auth/operator/login").permitAll()
.requestMatchers("/health/**").permitAll()
.requestMatchers("/version").permitAll()
.requestMatchers("/activities").permitAll()
.requestMatchers("/auth/**").authenticated()
// 操作台接口需要认证
.requestMatchers("/api/v1/operator/**").authenticated()
.requestMatchers("/operator/**").hasRole("OPERATOR")
// 管理接口需要认证
.requestMatchers("/api/v1/admin/**").authenticated()
.requestMatchers("/admin/**").hasRole("OPERATOR")
// 其他所有用户接口需要认证(覆盖 /api/v1/** 中未匹配的)
.requestMatchers("/api/v1/**").authenticated()
.requestMatchers("/**").authenticated()
// 其他所有请求放行(避免 denyAll 导致未知路径也被拦截)
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(permissionFilter, JwtAuthFilter.class);
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@@ -104,6 +104,15 @@ public final class Constants {
public static final String REVIEW_RESULT_REJECTED = "rejected";
public static final String REVIEW_RESULT_NEED_EDIT = "need_edit";
// 红娘服务类型(与 V009 数据库约束一致)
public static final String MATCHMAKER_SERVICE_TYPE_RECOMMEND_PARTNER = "recommend_partner";
public static final String MATCHMAKER_SERVICE_TYPE_REVIEW_PREFERENCE = "review_preference";
public static final String MATCHMAKER_SERVICE_TYPE_CONTACT_INTENTION = "contact_intention";
public static final String MATCHMAKER_SERVICE_TYPE_JOIN_ACTIVITY = "join_activity";
public static final String MATCHMAKER_SERVICE_TYPE_NO_MATCH_HELP = "no_match_help";
public static final String MATCHMAKER_SERVICE_TYPE_CHAT_HELP = "chat_help";
public static final String MATCHMAKER_SERVICE_TYPE_OTHER = "other";
// 红娘服务状态
public static final String MATCHMAKER_SERVICE_STATUS_PENDING = "pending";
public static final String MATCHMAKER_SERVICE_STATUS_ASSIGNED = "assigned";
@@ -157,6 +166,12 @@ public final class Constants {
public static final String APPOINTMENT_STATUS_COMPLETED = "completed";
public static final String APPOINTMENT_STATUS_CANCELLED = "cancelled";
// 咨询服务类型(与 V022 数据库约束一致)
public static final String COUNSELING_TYPE_RELATIONSHIP_GUIDANCE = "relationship_guidance";
public static final String COUNSELING_TYPE_EMOTIONAL_SUPPORT = "emotional_support";
public static final String COUNSELING_TYPE_PREMARITAL_GUIDANCE = "premarital_guidance";
public static final String COUNSELING_TYPE_ANTI_FRAUD_GUIDANCE = "anti_fraud_guidance";
// 咨询状态
public static final String COUNSELING_STATUS_PENDING = "pending";
public static final String COUNSELING_STATUS_ASSIGNED = "assigned";
@@ -223,6 +238,11 @@ public final class Constants {
public static final String CHECKIN_STATUS_NOT_CHECKED_IN = "not_checked_in";
// 本地资格类型(与 V1.1 API 契约一致)
public static final String LOCAL_ELIGIBILITY_TYPE_HUKOU = "household_registration";
public static final String LOCAL_ELIGIBILITY_TYPE_WORK = "employment";
public static final String LOCAL_ELIGIBILITY_TYPE_RESIDENCE = "residence";
// 实名认证状态
public static final String VERIFICATION_STATUS_PENDING = "pending";
public static final String VERIFICATION_STATUS_SUBMITTED = "submitted";
@@ -233,6 +253,8 @@ public final class Constants {
// 风险等级
public static final String RISK_LEVEL_NORMAL = "normal";
/** 消息内容风控等级;账号风险画像使用 WATCH。 */
public static final String RISK_LEVEL_WARNING = "warning";
public static final String RISK_LEVEL_WATCH = "watch";
public static final String RISK_LEVEL_HIGH = "high";
public static final String RISK_LEVEL_CRITICAL = "critical";
@@ -244,6 +266,7 @@ public final class Constants {
// 风控动作状态
public static final String RISK_ACTION_STATUS_ACTIVE = "active";
public static final String RISK_ACTION_STATUS_RESOLVED = "resolved";
public static final String RISK_ACTION_STATUS_EXPIRED = "expired";
public static final String RISK_ACTION_STATUS_REVOKED = "revoked";
@@ -2,7 +2,10 @@ package com.danyang.match.platform.shared.controller;
import com.danyang.match.platform.shared.config.FeatureFlagConfig;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
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.util.Map;
@@ -13,6 +16,7 @@ import java.util.Map;
*/
@RestController
@RequestMapping("/operator/feature-flags")
@PreAuthorize("hasAuthority('permission.manage')")
@RequiredArgsConstructor
public class FeatureFlagController {
@@ -34,7 +38,7 @@ public class FeatureFlagController {
*/
@PutMapping("/{flagName}")
public ApiResponse<Map<String, Object>> updateFlag(
@RequestHeader(value = "Authorization", required = false) String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String flagName,
@RequestBody Map<String, Boolean> body) {
boolean value = body.getOrDefault("value", false);
@@ -115,7 +115,8 @@ public class EncryptionUtil {
* 例:320***********1234
*/
public String maskIdNumber(String idNumber) {
if (idNumber == null || idNumber.length() < 10) {
if (idNumber == null) return null;
if (idNumber.length() < 10) {
return "****";
}
return idNumber.substring(0, 6) + "**********" + idNumber.substring(idNumber.length() - 4);
@@ -126,7 +127,8 @@ public class EncryptionUtil {
* 例:138****1234
*/
public String maskPhone(String phone) {
if (phone == null || phone.length() < 7) {
if (phone == null) return null;
if (phone.length() < 7) {
return "****";
}
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
@@ -3,9 +3,13 @@ package com.danyang.match.platform.shared.exception;
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String message) {
super(message);
this.code = null;
public BusinessException(String code) {
super(java.util.Arrays.stream(com.danyang.match.platform.shared.enums.ApiResponseCode.values())
.filter(value -> value.getCode().equals(code))
.map(com.danyang.match.platform.shared.enums.ApiResponseCode::getDescription)
.findFirst()
.orElse(code));
this.code = code;
}
public BusinessException(String code, String message) {
@@ -47,6 +47,13 @@ public class GlobalExceptionHandler {
}
}
@Getter
public static class UnauthorizedException extends BusinessException {
public UnauthorizedException(String message) {
super(ApiResponseCode.UNAUTHORIZED, message);
}
}
@Getter
public static class PermissionDeniedException extends BusinessException {
public PermissionDeniedException() {
@@ -76,6 +83,13 @@ public class GlobalExceptionHandler {
return ApiResponse.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ApiResponse<Void> handleUnauthorized(UnauthorizedException e) {
log.warn("Authentication failed: {}", e.getMessage());
return ApiResponse.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(PermissionDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ApiResponse<Void> handlePermissionDenied(PermissionDeniedException e) {
@@ -2,6 +2,9 @@ package com.danyang.match.platform.shared.filter;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.permission.service.PermissionService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.shared.security.principal.PlatformPrincipal;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -9,12 +12,17 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@Component
@RequiredArgsConstructor
@@ -51,7 +59,7 @@ public class JwtAuthFilter extends OncePerRequestFilter {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
sendUnauthorized(response);
chain.doFilter(request, response);
return;
}
@@ -63,8 +71,27 @@ public class JwtAuthFilter extends OncePerRequestFilter {
String subject = jwtTokenService.getUserIdFromToken(token);
String tokenType = jwtTokenService.parseToken(token).get("type", String.class);
boolean operatorToken = "operator_access".equals(tokenType);
List<String> permissions = operatorToken
? permissionService.getPermissions(subject)
: List.of();
PlatformPrincipal principal = operatorToken
? new OperatorPrincipal(subject, Set.copyOf(permissions))
: new UserPrincipal(subject);
var authorities = operatorToken
? Stream.concat(
Stream.of(new SimpleGrantedAuthority("ROLE_OPERATOR")),
permissions.stream().map(SimpleGrantedAuthority::new))
.toList()
: List.of(new SimpleGrantedAuthority("ROLE_USER"));
var authentication = UsernamePasswordAuthenticationToken.authenticated(principal, null, authorities);
var securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
// 临时保留请求属性,供尚未迁移的旧控制器兼容;新代码统一使用 Authentication Principal。
request.setAttribute("currentUserId", subject);
request.setAttribute("currentOperatorId", "operator_access".equals(tokenType) ? subject : null);
request.setAttribute("currentOperatorId", operatorToken ? subject : null);
request.setAttribute("currentUserToken", token);
chain.doFilter(request, response);
@@ -1,97 +0,0 @@
package com.danyang.match.platform.shared.filter;
import com.danyang.match.platform.permission.service.PermissionService;
import com.danyang.match.platform.shared.exception.BusinessException;
import com.danyang.match.platform.shared.enums.ApiResponseCode;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 权限校验过滤器
* 检查当前 Operator 是否有访问该接口的权限
*/
@Component
@RequiredArgsConstructor
public class PermissionFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(PermissionFilter.class);
private static final AntPathMatcher pathMatcher = new AntPathMatcher();
private final PermissionService permissionService;
// 需要权限校验的路径前缀
private static final Map<String, String> PATH_PERMISSION_MAP = Map.of(
"/api/v1/admin/**", "super_admin",
"/api/v1/operator/**", "operator_login"
);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String operatorId = (String) request.getAttribute("currentOperatorId");
// 如果没有 Operator 信息(普通用户请求),跳过权限检查
if (operatorId == null) {
chain.doFilter(request, response);
return;
}
// 检查是否需要权限
boolean needsPermission = PATH_PERMISSION_MAP.keySet().stream()
.anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI()));
if (!needsPermission) {
chain.doFilter(request, response);
return;
}
// Operator 登录接口本身不需要额外权限检查
if (request.getRequestURI().contains("/operator/auth/")) {
chain.doFilter(request, response);
return;
}
// 从 PATH_PERMISSION_MAP 获取路径对应的必需权限码
String requiredPermission = null;
for (Map.Entry<String, String> entry : PATH_PERMISSION_MAP.entrySet()) {
if (pathMatcher.match(entry.getKey(), request.getRequestURI())) {
requiredPermission = entry.getValue();
break;
}
}
if (requiredPermission == null) {
chain.doFilter(request, response);
return;
}
// 从 request attribute 获取 operatorId(由 JwtAuthFilter 设置)
if (operatorId == null) {
chain.doFilter(request, response);
return;
}
// 获取 Operator 的权限列表
List<String> permissions = permissionService.getPermissions(operatorId);
if (permissions == null || !permissions.contains(requiredPermission)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"success\":false,\"code\":\"FORBIDDEN\",\"message\":\"权限不足\",\"data\":null}");
return;
}
chain.doFilter(request, response);
}
}
@@ -0,0 +1,58 @@
package com.danyang.match.platform.shared.security;
import com.danyang.match.platform.audit.entity.AuditLog;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.Instant;
import java.util.UUID;
/** 将服务端授权拒绝写入不可静默绕过的安全审计。 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthorizationAuditListener {
private final AuditLogMapper auditLogMapper;
@EventListener
public void onAuthorizationDenied(AuthorizationDeniedEvent<?> event) {
var authentication = event.getAuthentication().get();
if (authentication == null || !(authentication.getPrincipal() instanceof OperatorPrincipal principal)) {
return;
}
AuditLog audit = new AuditLog();
audit.setId(UUID.randomUUID().toString());
audit.setOperatorId(principal.id());
audit.setAction("AUTHORIZATION_DENIED");
audit.setResourceType("authorization");
audit.setResourceId(limit(String.valueOf(event.getObject()), 128));
audit.setCreatedAt(Instant.now());
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) {
audit.setIp(attributes.getRequest().getRemoteAddr());
audit.setUserAgent(limit(attributes.getRequest().getHeader("User-Agent"), 512));
}
try {
auditLogMapper.insert(audit);
} catch (RuntimeException exception) {
log.error("写入授权拒绝审计失败: operatorId={}", principal.id(), exception);
}
}
private String limit(String value, int maxLength) {
if (value == null || value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength);
}
}
@@ -0,0 +1,37 @@
package com.danyang.match.platform.shared.security;
import com.danyang.match.platform.audit.entity.AuditLog;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.UUID;
/** Denies out-of-scope operator access and records the attempt before returning 403. */
@Slf4j
@Component
@RequiredArgsConstructor
public class DataScopeGuard {
private final AuditLogMapper auditLogMapper;
public void deny(String operatorId, String resourceType, String resourceId, String message) {
AuditLog audit = new AuditLog();
audit.setId(UUID.randomUUID().toString());
audit.setOperatorId(operatorId);
audit.setAction("DATA_SCOPE_DENIED");
audit.setResourceType(resourceType);
audit.setResourceId(resourceId);
audit.setCreatedAt(Instant.now());
try {
auditLogMapper.insert(audit);
} catch (RuntimeException exception) {
log.error("写入数据范围拒绝审计失败: operatorId={}, resourceType={}, resourceId={}",
operatorId, resourceType, resourceId, exception);
}
throw new AccessDeniedException(message);
}
}
@@ -0,0 +1,21 @@
package com.danyang.match.platform.shared.security.principal;
import java.util.Set;
/** 后台运营主体,权限集合只能由服务端数据库装载。 */
public record OperatorPrincipal(String id, Set<String> permissions) implements PlatformPrincipal {
public OperatorPrincipal {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("运营主体ID不能为空");
}
permissions = permissions == null ? Set.of() : Set.copyOf(permissions);
}
public OperatorPrincipal(String id) {
this(id, Set.of());
}
public boolean hasPermission(String permission) {
return permissions.contains(permission);
}
}
@@ -0,0 +1,6 @@
package com.danyang.match.platform.shared.security.principal;
/** 当前请求中已经通过 JWT 验证的主体。 */
public sealed interface PlatformPrincipal permits UserPrincipal, OperatorPrincipal {
String id();
}
@@ -0,0 +1,10 @@
package com.danyang.match.platform.shared.security.principal;
/** 普通用户主体。 */
public record UserPrincipal(String id) implements PlatformPrincipal {
public UserPrincipal {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("用户主体ID不能为空");
}
}
}
@@ -1,9 +1,11 @@
package com.danyang.match.platform.user.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.user.service.UserService;
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.util.Map;
@@ -17,15 +19,15 @@ import java.util.Map;
public class UserController {
private final UserService userService;
private final JwtTokenService jwtTokenService;
/**
* 获取用户列表(分页+筛选)
* GET /api/v1/operator/users?keyword=&accountStatus=&profileStatus=&page=0&pageSize=20
*/
@PreAuthorize("hasAuthority('user.view')")
@GetMapping
public ApiResponse<Map<String, Object>> listUsers(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String accountStatus,
@RequestParam(required = false) String profileStatus,
@@ -38,12 +40,12 @@ public class UserController {
* 获取用户详情
* GET /api/v1/operator/users/{id}
*/
@PreAuthorize("hasAuthority('user.view')")
@GetMapping("/{id}")
public ApiResponse<Object> getUserDetail(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id) {
String operatorId = extractOperatorId(authHeader);
return ApiResponse.ok(userService.getUserDetail(id, operatorId));
return ApiResponse.ok(userService.getUserDetail(id, principal.id()));
}
/**
@@ -51,30 +53,18 @@ public class UserController {
* PUT /api/v1/operator/users/{id}/status
* Body: { "accountStatus": "banned" }
*/
@PreAuthorize("hasAuthority('user.status.change')")
@PutMapping("/{id}/status")
public ApiResponse<Void> updateAccountStatus(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal OperatorPrincipal principal,
@PathVariable String id,
@RequestBody Map<String, String> body) {
String operatorId = extractOperatorId(authHeader);
String newStatus = body.get("accountStatus");
if (newStatus == null) {
return ApiResponse.error(com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST, "accountStatus不能为空");
}
userService.updateAccountStatus(id, newStatus, operatorId);
userService.updateAccountStatus(id, newStatus, principal.id());
return ApiResponse.ok();
}
private String extractOperatorId(String authHeader) {
String token = extractToken(authHeader);
return token;
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -1,9 +1,10 @@
package com.danyang.match.platform.verification.controller;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import com.danyang.match.platform.shared.dto.ApiResponse;
import com.danyang.match.platform.verification.service.IdentityVerificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -18,7 +19,6 @@ import java.util.Map;
public class IdentityVerificationController {
private final IdentityVerificationService identityVerificationService;
private final JwtTokenService jwtTokenService;
/**
* 提交实名认证
@@ -28,11 +28,9 @@ public class IdentityVerificationController {
*/
@PostMapping("/submit")
public ApiResponse<Map<String, Object>> submitIdentity(
@RequestHeader("Authorization") String authHeader,
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> body) {
String token = extractToken(authHeader);
String userId = jwtTokenService.getUserIdFromToken(token);
String userId = principal.id();
String realName = body.get("realName");
String idNumber = body.get("idNumber");
@@ -57,10 +55,8 @@ public class IdentityVerificationController {
*/
@GetMapping("/status")
public ApiResponse<Map<String, Object>> getIdentityStatus(
@RequestHeader("Authorization") String authHeader) {
String token = extractToken(authHeader);
String userId = jwtTokenService.getUserIdFromToken(token);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
return ApiResponse.ok(identityVerificationService.getIdentityStatus(userId));
}
@@ -70,19 +66,10 @@ public class IdentityVerificationController {
*/
@PostMapping("/cancel")
public ApiResponse<Void> cancelIdentity(
@RequestHeader("Authorization") String authHeader) {
String token = extractToken(authHeader);
String userId = jwtTokenService.getUserIdFromToken(token);
@AuthenticationPrincipal UserPrincipal principal) {
String userId = principal.id();
identityVerificationService.cancelIdentity(userId);
return ApiResponse.ok();
}
private String extractToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException(
com.danyang.match.platform.shared.enums.ApiResponseCode.UNAUTHORIZED);
}
return authHeader.substring(7);
}
}
@@ -0,0 +1,37 @@
# Danyang Match Platform - Environment Variables Example
# Copy this file to application-dev.yml and fill in real values.
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=danyang_match
DB_USER=postgres
DB_PASSWORD=replace-with-local-password
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# MinIO (Object Storage)
MINIO_ENDPOINT=http://localhost:9000
MINIO_ACCESS_KEY=replace-with-local-access-key
MINIO_SECRET_KEY=replace-with-local-secret-key
MINIO_BUCKET_PUBLIC=public
MINIO_BUCKET_PRIVATE=private
# WeChat Mini Program
WECHAT_APP_ID=your-wechat-app-id
WECHAT_APP_SECRET=your-wechat-app-secret
# Encryption (AES-256 key, 64 hex chars)
ENCRYPTION_KEY=replace-with-64-hex-character-key
# JWT Secrets (use strong random values in production)
JWT_ACCESS_SECRET=replace-with-strong-access-secret
JWT_REFRESH_SECRET=replace-with-strong-refresh-secret
# Real-name Verification Provider (mock for dev)
VERIFICATION_PROVIDER=mock
VERIFICATION_API_URL=
VERIFICATION_API_KEY=
@@ -0,0 +1,78 @@
spring:
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:danyang_match}
username: ${DB_USER:postgres}
password: ${DB_PASSWORD}
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
wechat:
app-id: ${WECHAT_APP_ID:}
app-secret: ${WECHAT_APP_SECRET:}
encryption:
key: ${ENCRYPTION_KEY}
jwt:
access-token-secret: ${JWT_ACCESS_SECRET}
access-token-expiration-ms: 7200000
refresh-token-secret: ${JWT_REFRESH_SECRET}
refresh-token-expiration-ms: 1209600000
object-storage:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000}
access-key: ${MINIO_ACCESS_KEY}
secret-key: ${MINIO_SECRET_KEY}
bucket-public: ${MINIO_BUCKET_PUBLIC:public}
bucket-private: ${MINIO_BUCKET_PRIVATE:private}
region: us-east-1
signature-version: s3v4
verification:
provider: mock
api-url: ${VERIFICATION_API_URL:}
api-key: ${VERIFICATION_API_KEY:}
match:
rule-version: 1
default-daily-limit: 6
default-score-threshold: 60
default-cooldown-days: 180
activity:
default-need-review: true
default-mutual-max-selection: 3
default-mutual-deadline-hours: 48
rate-limit:
enabled: true
window-seconds: 60
max-requests-per-window: 100
features:
# V1.0
activity-enabled: true
matchmaker-enabled: true
chat-image-enabled: false
appeal-enabled: false
# V1.1
counseling-enabled: true
premarital-enabled: true
content-enabled: true
service-location-enabled: true
local-eligibility-enabled: true
referral-enabled: true
mutual-consent-enabled: true
service-program-enabled: true
liveness-enabled: false
liveness-provider: none
local-evidence-upload-enabled: true
local-evidence-max-files: 5
local-evidence-max-file-size-mb: 10
local-evidence-allowed-types: jpg,png,pdf
counseling-attachments-private: true
counseling-attachments-bucket: counseling-attachments
@@ -0,0 +1,12 @@
-- V028: Phase 02 服务站运营人员数据范围
-- 组织关系不授予数据权限;只有显式站点分配或预约任务分配可访问。
CREATE TABLE service_location_operators (
service_location_id UUID NOT NULL REFERENCES service_locations(id),
operator_id UUID NOT NULL REFERENCES operators(id),
created_by UUID REFERENCES operators(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (service_location_id, operator_id)
);
CREATE INDEX idx_service_location_operators_operator
ON service_location_operators(operator_id, service_location_id);
@@ -0,0 +1,14 @@
-- V1.1 Phase 02: complete the service-station permission contract from the customer baseline.
INSERT INTO permissions (id, code, name, module, description) VALUES
('20000000-0000-0000-0000-000000000800', 'activity.checkin', '活动签到', 'activity', '执行活动扫码或人工签到并查看签到统计'),
('20000000-0000-0000-0000-000000000801', 'service.assisted_registration', '现场协助登记', 'service', '在服务站现场协助用户完成服务登记');
INSERT INTO role_permissions (role_id, permission_id)
SELECT '00000000-0000-0000-0000-000000000010', id
FROM permissions
WHERE code IN ('activity.checkin', 'service.assisted_registration');
INSERT INTO role_permissions (role_id, permission_id)
SELECT '00000000-0000-0000-0000-000000000002', id
FROM permissions
WHERE code IN ('activity.checkin', 'service.assisted_registration');
@@ -0,0 +1,64 @@
package com.danyang.match.platform.auth;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.auth.service.OperatorAuthService;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.permission.entity.Operator;
import com.danyang.match.platform.permission.mapper.OperatorMapper;
import com.danyang.match.platform.permission.mapper.PermissionMapper;
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.UnauthorizedException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OperatorAuthServiceTest {
@Mock OperatorMapper operatorMapper;
@Mock PermissionMapper permissionMapper;
@Mock JwtTokenService jwtTokenService;
@Mock StringRedisTemplate redisTemplate;
@Mock ValueOperations<String, String> valueOperations;
@Mock AuditLogMapper auditLogMapper;
@Mock PasswordEncoder passwordEncoder;
private OperatorAuthService service;
@BeforeEach
void setUp() {
service = new OperatorAuthService(operatorMapper, permissionMapper, jwtTokenService,
redisTemplate, auditLogMapper, passwordEncoder);
}
@Test
void existingOperatorWithWrongPasswordCannotReceiveToken() {
Operator operator = new Operator();
operator.setId("operator-1");
operator.setUsername("admin");
operator.setPasswordHash("bcrypt-hash-from-database");
operator.setStatus("active");
when(operatorMapper.findByUsername("admin")).thenReturn(Optional.of(operator));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(passwordEncoder.matches("not-the-password", operator.getPasswordHash())).thenReturn(false);
when(valueOperations.increment("auth:operator_login_attempts:operator-1")).thenReturn(1L);
assertThrows(UnauthorizedException.class,
() -> service.login("admin", "not-the-password"));
verify(passwordEncoder).matches("not-the-password", "bcrypt-hash-from-database");
verify(jwtTokenService, never()).generateOperatorToken(
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyMap());
}
}
@@ -24,7 +24,7 @@ class AuthControllerTest {
@Test
void testWechatLoginWithInvalidCode() throws Exception {
mockMvc.perform(post("/api/v1/auth/wechat/login")
mockMvc.perform(post("/api/v1/auth/wechat/login").contextPath("/api/v1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"code\": \"\"}"))
.andExpect(status().isBadRequest())
@@ -33,7 +33,7 @@ class AuthControllerTest {
@Test
void testWechatLoginMissingCode() throws Exception {
mockMvc.perform(post("/api/v1/auth/wechat/login")
mockMvc.perform(post("/api/v1/auth/wechat/login").contextPath("/api/v1")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest())
@@ -42,8 +42,13 @@ class AuthControllerTest {
@Test
void testHealthLiveNotAffectedByAuth() throws Exception {
mockMvc.perform(get("/api/v1/health/live"))
mockMvc.perform(get("/api/v1/health/live").contextPath("/api/v1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
@Test
void testProtectedEndpointRejectsAnonymousRequest() throws Exception {
mockMvc.perform(get("/api/v1/users/me").contextPath("/api/v1"))
.andExpect(status().isForbidden());
}
}
@@ -0,0 +1,33 @@
package com.danyang.match.platform.content;
import com.danyang.match.platform.content.controller.ContentController;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class ContentControllerPermissionContractTest {
@Test
void everyContentMutationRequiresItsV11Permission() {
Map<String, String> expected = Map.of(
"createCategory", "hasAuthority('content.create')",
"updateCategory", "hasAuthority('content.edit')",
"createArticle", "hasAuthority('content.create')",
"updateArticle", "hasAuthority('content.edit')",
"publishArticle", "hasAuthority('content.publish')",
"offlineArticle", "hasAuthority('content.offline')");
expected.forEach((methodName, expression) -> {
var method = java.util.Arrays.stream(ContentController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(methodName))
.findFirst()
.orElseThrow();
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
assertNotNull(annotation, methodName + " 必须由服务端权限保护");
assertEquals(expression, annotation.value(), methodName + " 权限码不符合 V015");
});
}
}
@@ -0,0 +1,93 @@
package com.danyang.match.platform.counseling;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.counseling.entity.CounselingService;
import com.danyang.match.platform.counseling.mapper.CounselingFollowupMapper;
import com.danyang.match.platform.counseling.mapper.CounselingServiceMapper;
import com.danyang.match.platform.counseling.service.CounselingServiceApp;
import com.danyang.match.platform.notification.service.NotificationService;
import com.danyang.match.platform.permission.mapper.OperatorMapper;
import com.danyang.match.platform.risk.service.RiskReferralService;
import com.danyang.match.platform.shared.security.DataScopeGuard;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.user.mapper.UserMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.AccessDeniedException;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CounselingDataScopeTest {
@Mock CounselingServiceMapper counselingMapper;
@Mock CounselingFollowupMapper followupMapper;
@Mock OperatorMapper operatorMapper;
@Mock NotificationService notificationService;
@Mock UserMapper userMapper;
@Mock RiskReferralService riskReferralService;
@Mock AuditLogMapper auditLogMapper;
private CounselingServiceApp service;
@BeforeEach
void setUp() {
service = new CounselingServiceApp(counselingMapper, followupMapper, operatorMapper,
notificationService, userMapper, riskReferralService, new DataScopeGuard(auditLogMapper));
}
@Test
void counselorCannotScheduleServiceAssignedToAnotherCounselor() {
CounselingService counseling = assignedService("service-1", "counselor-a");
when(counselingMapper.selectById("service-1")).thenReturn(counseling);
assertThrows(AccessDeniedException.class, () -> service.scheduleCounseling(
"service-1", new OperatorPrincipal("counselor-b", java.util.Set.of("counseling.service.process")), Instant.parse("2026-09-10T01:00:00Z"),
Instant.parse("2026-09-10T02:00:00Z")));
verify(counselingMapper, never()).updateById(any());
verify(auditLogMapper).insert(argThat(audit ->
"DATA_SCOPE_DENIED".equals(audit.getAction())
&& "counselor-b".equals(audit.getOperatorId())));
}
@Test
void assignedCounselorCanScheduleOwnService() {
CounselingService counseling = assignedService("service-1", "counselor-a");
when(counselingMapper.selectById("service-1")).thenReturn(counseling);
var result = service.scheduleCounseling("service-1", new OperatorPrincipal("counselor-a", java.util.Set.of("counseling.service.process")),
Instant.parse("2026-09-10T01:00:00Z"), Instant.parse("2026-09-10T02:00:00Z"));
assertEquals("scheduled", result.getStatus());
verify(counselingMapper).updateById(counseling);
}
@Test
void assignedListDoesNotExposeSensitiveFollowupContentByDefault() {
CounselingService counseling = assignedService("service-1", "counselor-a");
when(counselingMapper.findByAssignedOperator("counselor-a")).thenReturn(List.of(counseling));
var result = service.getCounselorAssignedServices("counselor-a");
assertEquals(1, result.size());
assertTrue(result.getFirst().getFollowups() == null || result.getFirst().getFollowups().isEmpty());
verifyNoInteractions(followupMapper);
}
private CounselingService assignedService(String id, String operatorId) {
CounselingService service = new CounselingService();
service.setId(id);
service.setUserId("user-1");
service.setAssignedOperatorId(operatorId);
service.setStatus("assigned");
service.setCreatedAt(Instant.now());
return service;
}
}
@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* P25-T02: Migration 最终验证
* 验证所有 V001-V014 迁移文件存在、命名规范、语法正确
* 验证所有 V001-V029 迁移文件存在、命名规范、语法正确
*/
class MigrationValidationTest {
@@ -22,8 +22,8 @@ class MigrationValidationTest {
@Test
void testAllMigrationsExist() {
// 预期 V001-V014
for (int i = 1; i <= 14; i++) {
// 预期 V001-V029
for (int i = 1; i <= 29; i++) {
String expected = String.format("V%03d__", i);
assertTrue(filesStartWith(expected),
"迁移文件缺失: " + expected + "*.sql");
@@ -47,10 +47,10 @@ class MigrationValidationTest {
"命名不规范: " + name);
// 版本号递增
int version = Integer.parseInt(name.substring(1, 4));
assertTrue(version >= 1 && version <= 14,
"版本号应在1-14之间: " + name);
assertTrue(version >= 1 && version <= 29,
"版本号应在1-29之间: " + name);
}
assertEquals(14, count, "应有14个迁移文件,实际" + count + "个");
assertEquals(29, count, "应有29个迁移文件,实际" + count + "个");
}
}
@@ -1,5 +1,8 @@
package com.danyang.match.platform.discover;
import com.danyang.match.platform.discover.service.DiscoverService;
import org.springframework.test.util.ReflectionTestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@@ -16,6 +19,8 @@ class DiscoverServiceTest {
@Test
void testAgeNull() {
assertEquals(0, java.time.Period.between(null, java.time.LocalDate.now()).getYears());
DiscoverService service = new DiscoverService(null);
Integer age = ReflectionTestUtils.invokeMethod(service, "calculateAge", (java.time.LocalDate) null);
assertEquals(0, age);
}
}
@@ -148,7 +148,7 @@ class CoreInvariantsTest {
// 风险等级
assertNotNull(Constants.RISK_LEVEL_HIGH);
assertNotNull(Constants.RISK_LEVEL_WARNING);
assertNotNull(Constants.RISK_LEVEL_LOW);
assertNotNull(Constants.RISK_LEVEL_NORMAL);
// 本地资格类型
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_HUKOU);
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_WORK);
@@ -42,6 +42,7 @@ class P0FixVerificationTest {
@Autowired private RelationshipService relationshipService;
@Autowired private RelationshipStatusService statusService;
@Autowired private AuditLogMapper auditLogMapper;
@Autowired private com.danyang.match.platform.account.service.AccountLifecycleService accountLifecycleService;
private String userA, userB;
@@ -204,15 +205,9 @@ class P0FixVerificationTest {
@DisplayName("P0-3: 无进行中业务时允许注销")
void testWithdrawAllowedWhenNoActiveServices() {
// 用户A无任何活动报名,应允许注销
assertEquals(true,
((java.util.Map<?, ?>) com.danyang.match.platform.account.service.AccountLifecycleService.class
.getDeclaredFields()[0]).getType().equals(java.lang.Class.class));
// 简化验证:直接检查数据库
var result = new com.danyang.match.platform.account.service.AccountLifecycleService(
userMapper, null, null, null, null, null, null, null, null) {
// 这个测试主要是验证查询逻辑不再全表扫描
};
assertTrue(true); // 通过代码审查已验证查询条件修复
var result = accountLifecycleService.checkCanWithdraw(userA);
assertEquals(Boolean.TRUE, result.get("canWithdraw"));
assertTrue(((java.util.Map<?, ?>) result.get("blockers")).isEmpty());
}
@Test
@@ -1,9 +1,9 @@
package com.danyang.match.platform.match.service;
import com.danyang.match.platform.match.dto.RecommendationCandidateDTO;
import com.danyang.match.platform.match.entity.UserPreference;
import com.danyang.match.platform.match.mapper.UserPreferenceMapper;
import com.danyang.match.platform.match.mapper.UserProfileMapper;
import com.danyang.match.platform.profile.entity.UserPreference;
import com.danyang.match.platform.profile.mapper.UserPreferenceMapper;
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
import com.danyang.match.platform.shared.constant.Constants;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1,7 +1,6 @@
package com.danyang.match.platform.match.service;
import com.danyang.match.platform.match.config.MatchRuleConfigProperties;
import com.danyang.match.platform.match.entity.ScoredCandidate;
import com.danyang.match.platform.profile.entity.UserProfile;
import com.danyang.match.platform.profile.entity.UserPreference;
import com.fasterxml.jackson.databind.JsonNode;
@@ -39,8 +38,6 @@ class RecommendationEngineBidirectionalRequiredTest {
@BeforeEach
void setUp() {
config.setMinDailyLimit(1);
config.setMaxDailyLimit(20);
config.setDefaultScoreThreshold(60);
config.setDefaultCooldownDays(180);
}
@@ -55,8 +52,8 @@ class RecommendationEngineBidirectionalRequiredTest {
myPref.minAge = 25;
myPref.maxAge = 30;
UserProfile candidate = buildProfile(1995, 170, "321181", "never_married"); // 30岁(2025-1995=30)→ 刚好边界
UserProfile myProfile = buildProfile(1990, 175, "321181", "never_married"); // 35岁
UserProfile candidate = buildProfile(LocalDate.now().getYear() - 30, 170, "321181", "never_married"); // 30岁,刚好边界
UserProfile myProfile = buildProfile(LocalDate.now().getYear() - 35, 175, "321181", "never_married"); // 35岁
// 候选B的年龄偏好也是Required:B要求A在28-40之间 → A(35)符合
TestPreference targetPref = new TestPreference();
@@ -71,7 +68,7 @@ class RecommendationEngineBidirectionalRequiredTest {
);
// 现在B年龄=31,超出A要求的[25,30]
UserProfile tooOld = buildProfile(1994, 170, "321181", "never_married"); // 31岁
UserProfile tooOld = buildProfile(LocalDate.now().getYear() - 31, 170, "321181", "never_married"); // 31岁
assertFalse(
evaluateRequired(myPref, tooOld, targetPref, myProfile),
"B年龄31超出A要求的[25,30],应被过滤"
@@ -11,7 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class RecommendationEngineServiceTest {
class RecommendationEngineServiceMockTest {
@InjectMocks
private RecommendationEngineService engineService;
@@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.*;
class MessageRiskServiceTest {
private final MessageRiskService riskService = new MessageRiskService();
private final MessageRiskService riskService = new MessageRiskService(null);
@Test
void testNormalContent() {
@@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.*;
class PhotoServiceTest {
private final PhotoService photoService = new PhotoService();
private final PhotoService photoService = new PhotoService(null, null);
@Test
void testValidateMagicNumberJpeg() {
@@ -0,0 +1,38 @@
package com.danyang.match.platform.security;
import com.danyang.match.platform.audit.entity.AuditLog;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.shared.security.AuthorizationAuditListener;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AuthorizationAuditListenerTest {
@Test
void deniedOperatorAuthorizationIsWrittenToAuditLog() {
AuditLogMapper mapper = mock(AuditLogMapper.class);
AuthorizationAuditListener listener = new AuthorizationAuditListener(mapper);
OperatorPrincipal principal = new OperatorPrincipal("operator-1", Set.of("content.view"));
var authentication = UsernamePasswordAuthenticationToken.authenticated(principal, null, java.util.List.of());
var event = new AuthorizationDeniedEvent<>(() -> authentication,
"ContentController.publishArticle", new AuthorizationDecision(false));
listener.onAuthorizationDenied(event);
ArgumentCaptor<AuditLog> captor = ArgumentCaptor.forClass(AuditLog.class);
verify(mapper).insert(captor.capture());
assertEquals("operator-1", captor.getValue().getOperatorId());
assertEquals("AUTHORIZATION_DENIED", captor.getValue().getAction());
assertEquals("authorization", captor.getValue().getResourceType());
assertNotNull(captor.getValue().getCreatedAt());
}
}
@@ -0,0 +1,80 @@
package com.danyang.match.platform.security;
import com.danyang.match.platform.account.controller.AccountOperatorController;
import com.danyang.match.platform.activity.controller.*;
import com.danyang.match.platform.analytics.AnalyticsController;
import com.danyang.match.platform.appeal.controller.AppealOperatorController;
import com.danyang.match.platform.audit.controller.AuditLogController;
import com.danyang.match.platform.content.controller.ContentController;
import com.danyang.match.platform.counseling.controller.CounselingController;
import com.danyang.match.platform.matchmaker.controller.MatchmakerOperatorController;
import com.danyang.match.platform.operator.controller.OperatorController;
import com.danyang.match.platform.organization.controller.OrganizationController;
import com.danyang.match.platform.referral.controller.ReferralController;
import com.danyang.match.platform.review.controller.ReviewController;
import com.danyang.match.platform.risk.controller.ReportOperatorController;
import com.danyang.match.platform.risk.controller.RiskController;
import com.danyang.match.platform.service_appointment.controller.ServiceAppointmentController;
import com.danyang.match.platform.service_program.controller.ServiceProgramController;
import com.danyang.match.platform.shared.controller.FeatureFlagController;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.user.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class OperatorEndpointPermissionCoverageTest {
private static final List<Class<?>> CONTROLLERS = List.of(
AccountOperatorController.class, ActivityController.class,
ActivityInterestController.class, ActivityOperatorController.class,
CheckinController.class, AnalyticsController.class,
AppealOperatorController.class, AuditLogController.class,
ContentController.class, CounselingController.class,
MatchmakerOperatorController.class, OperatorController.class,
OrganizationController.class, ReviewController.class,
ReferralController.class,
ReportOperatorController.class, RiskController.class,
ServiceAppointmentController.class, ServiceProgramController.class,
FeatureFlagController.class, UserController.class);
@Test
void everyOperatorEndpointHasExplicitPermissionContract() {
List<String> missing = new ArrayList<>();
for (Class<?> controller : CONTROLLERS) {
boolean operatorController = paths(controller).stream().anyMatch(this::isOperatorPath);
for (Method method : controller.getDeclaredMethods()) {
boolean operatorMethod = paths(method).stream().anyMatch(this::isOperatorPath);
boolean operatorPrincipal = Arrays.stream(method.getParameterTypes())
.anyMatch(OperatorPrincipal.class::equals);
if (!operatorController && !operatorMethod && !operatorPrincipal) {
continue;
}
boolean secured = AnnotatedElementUtils.hasAnnotation(controller, PreAuthorize.class)
|| AnnotatedElementUtils.hasAnnotation(method, PreAuthorize.class);
if (!secured) {
missing.add(controller.getSimpleName() + "." + method.getName());
}
}
}
assertEquals(List.of(), missing, "以下运营端接口缺少显式 Permission;不能依赖 /operator/** 粗过滤器");
}
private List<String> paths(java.lang.reflect.AnnotatedElement element) {
RequestMapping mapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
return mapping == null ? List.of() : Arrays.asList(mapping.path().length > 0
? mapping.path() : mapping.value());
}
private boolean isOperatorPath(String path) {
return path.equals("/operator") || path.startsWith("/operator/");
}
}
@@ -0,0 +1,41 @@
package com.danyang.match.platform.security;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PrincipalInjectionContractTest {
@Test
void controllersMustUseSecurityPrincipalsInsteadOfParsingAuthorizationHeaders() throws IOException {
Path controllers = Path.of("backend/src/main/java/com/danyang/match/platform");
List<String> violations = new ArrayList<>();
try (var files = Files.walk(controllers)) {
files.filter(path -> path.getFileName().toString().endsWith("Controller.java"))
.forEach(path -> inspect(path, violations));
}
assertEquals(List.of(), violations,
"控制器必须使用 UserPrincipal/OperatorPrincipal,不能自行读取或解析 Authorization/JWT");
}
private void inspect(Path path, List<String> violations) {
try {
String source = Files.readString(path);
if (source.contains("JwtTokenService")
|| source.contains("RequestHeader(\"Authorization\")")
|| source.contains("RequestHeader(value = \"Authorization\"")) {
violations.add(path.toString());
}
} catch (IOException error) {
throw new IllegalStateException("无法读取控制器源码: " + path, error);
}
}
}
@@ -11,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.util.HtmlUtils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -90,7 +91,7 @@ class SecurityTests {
// 验证所有 Mapper 使用 @Param 注解的参数化查询
String[] dangerousPatterns = {"DROP TABLE", "'; --", "1=1 OR 1=1"};
for (String pattern : dangerousPatterns) {
assertFalse(pattern.matches(".*[;']", "SQL注入模式不应出现在正常输入中"));
assertFalse(pattern.matches(".*[;']"), "SQL注入模式不应出现在正常输入中");
}
}
@@ -108,7 +109,9 @@ class SecurityTests {
// 用户输入的 description 字段在展示时应被转义
// 前端框架(Vue/小程序)默认转义,后端存储原始值
String xssPayload = "<script>alert('xss')</script>";
assertFalse(xssPayload.contains("<script>"), "XSS payload should be detected");
String escaped = HtmlUtils.htmlEscape(xssPayload);
assertFalse(escaped.contains("<script>"), "转义结果不应包含可执行 script 标签");
assertTrue(escaped.contains("&lt;script&gt;"), "script 标签必须被 HTML 转义");
}
@Test
@@ -0,0 +1,109 @@
package com.danyang.match.platform.security;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.shared.security.AuthorizationAuditListener;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
class V11MethodSecurityMatrixTest {
@AfterEach
void clearContext() {
SecurityContextHolder.clearContext();
}
@Test
void v11RolesEnforcePositiveAndNegativePermissionsAndAuditEveryDenial() {
try (var context = new AnnotationConfigApplicationContext(TestSecurityConfig.class)) {
SecuredOperations secured = context.getBean(SecuredOperations.class);
AuditLogMapper auditLogMapper = context.getBean(AuditLogMapper.class);
authenticate("content-editor", Set.of("content.edit"));
assertDoesNotThrow(secured::editContent);
assertThrows(AccessDeniedException.class, secured::viewUserData);
authenticate("counselor", Set.of("counseling.service.view", "counseling.service.process"));
assertDoesNotThrow(secured::processAssignedCounseling);
assertThrows(AccessDeniedException.class, secured::assignCounseling);
authenticate("station-operator", Set.of("service_appointment.confirm", "activity.checkin"));
assertDoesNotThrow(secured::confirmStationAppointment);
assertThrows(AccessDeniedException.class, secured::viewUserData);
// Organization/Sponsor relationship is deliberately absent from authorities.
authenticate("sponsor-organization-operator", Set.of());
assertThrows(AccessDeniedException.class, secured::viewUserData);
verify(auditLogMapper, times(4)).insert(org.mockito.ArgumentMatchers.any());
}
}
private void authenticate(String operatorId, Set<String> permissions) {
OperatorPrincipal principal = new OperatorPrincipal(operatorId, permissions);
var authorities = permissions.stream().map(SimpleGrantedAuthority::new).toList();
var authentication = UsernamePasswordAuthenticationToken.authenticated(principal, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
@Configuration
@EnableMethodSecurity
static class TestSecurityConfig {
@Bean
AuditLogMapper auditLogMapper() {
return mock(AuditLogMapper.class);
}
@Bean
AuthorizationAuditListener authorizationAuditListener(AuditLogMapper mapper) {
return new AuthorizationAuditListener(mapper);
}
@Bean
AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher publisher) {
return new SpringAuthorizationEventPublisher(publisher);
}
@Bean
SecuredOperations securedOperations() {
return new SecuredOperations();
}
}
static class SecuredOperations {
@PreAuthorize("hasAuthority('content.edit')")
void editContent() {}
@PreAuthorize("hasAuthority('user.view')")
void viewUserData() {}
@PreAuthorize("hasAuthority('counseling.service.process')")
void processAssignedCounseling() {}
@PreAuthorize("hasAuthority('counseling.service.assign')")
void assignCounseling() {}
@PreAuthorize("hasAuthority('service_appointment.confirm')")
void confirmStationAppointment() {}
}
}
@@ -0,0 +1,66 @@
package com.danyang.match.platform.security;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class V11PermissionMatrixContractTest {
private static final String STATION = "00000000-0000-0000-0000-000000000010";
private static final String COUNSELOR = "00000000-0000-0000-0000-000000000011";
private static final String COUNSELING_MANAGER = "00000000-0000-0000-0000-000000000012";
private static final String CONTENT_EDITOR = "00000000-0000-0000-0000-000000000013";
@Test
void v11RolesMatchCustomerPermissionBaseline() throws IOException {
String migrations = Files.readString(Path.of("backend/src/main/resources/db/migration/V015__add_v11_roles_and_permissions.sql"))
+ Files.readString(Path.of("backend/src/main/resources/db/migration/V029__complete_service_station_permissions.sql"));
assertEquals(Set.of(
"service_location.view", "service_appointment.view",
"service_appointment.confirm", "service_appointment.complete",
"activity.checkin", "service.assisted_registration"), permissionsFor(migrations, STATION));
assertEquals(Set.of(
"counseling.service.view", "counseling.service.process",
"counseling.note.create"), permissionsFor(migrations, COUNSELOR));
assertEquals(Set.of(
"counseling.service.view", "counseling.service.assign",
"counseling.service.process", "counseling.note.view"),
permissionsFor(migrations, COUNSELING_MANAGER));
assertEquals(Set.of(
"content.view", "content.create", "content.edit",
"content.publish", "content.offline"), permissionsFor(migrations, CONTENT_EDITOR));
for (String role : Set.of(STATION, COUNSELOR, COUNSELING_MANAGER, CONTENT_EDITOR)) {
Set<String> permissions = permissionsFor(migrations, role);
assertFalse(permissions.contains("user.view"));
assertFalse(permissions.contains("identity.sensitive.view"));
assertFalse(permissions.contains("message.content.view"));
}
}
private Set<String> permissionsFor(String sql, String roleId) {
Pattern grant = Pattern.compile("SELECT\\s+'" + Pattern.quote(roleId)
+ "',\\s*id\\s+FROM\\s+permissions\\s+WHERE\\s+code\\s+IN\\s*\\(([^)]*)\\)",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
Matcher grants = grant.matcher(sql);
Set<String> permissions = new HashSet<>();
Pattern code = Pattern.compile("'([^']+)'");
while (grants.find()) {
Matcher codes = code.matcher(grants.group(1));
while (codes.find()) {
permissions.add(codes.group(1));
}
}
return permissions;
}
}
@@ -0,0 +1,85 @@
package com.danyang.match.platform.service_appointment;
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
import com.danyang.match.platform.notification.service.NotificationService;
import com.danyang.match.platform.risk.service.RiskReferralService;
import com.danyang.match.platform.service_appointment.entity.ServiceAppointment;
import com.danyang.match.platform.service_appointment.mapper.ServiceAppointmentMapper;
import com.danyang.match.platform.service_appointment.mapper.ServiceLocationMapper;
import com.danyang.match.platform.service_appointment.service.ServiceAppointmentService;
import com.danyang.match.platform.shared.security.DataScopeGuard;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.AccessDeniedException;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ServiceAppointmentDataScopeTest {
@Mock ServiceAppointmentMapper appointmentMapper;
@Mock ServiceLocationMapper locationMapper;
@Mock NotificationService notificationService;
@Mock RiskReferralService riskReferralService;
@Mock AuditLogMapper auditLogMapper;
private ServiceAppointmentService service;
@BeforeEach
void setUp() {
service = new ServiceAppointmentService(appointmentMapper, locationMapper,
notificationService, riskReferralService, new DataScopeGuard(auditLogMapper));
}
@Test
void userCannotReadAnotherUsersAppointmentById() {
ServiceAppointment appointment = appointment("appointment-1", "user-a", "operator-a");
when(appointmentMapper.selectById("appointment-1")).thenReturn(appointment);
assertNull(service.getAppointmentDetail("appointment-1", "user-b"));
}
@Test
void stationOperatorListUsesServerSideAccessibleScope() {
ServiceAppointment appointment = appointment("appointment-1", "user-a", "operator-a");
when(appointmentMapper.findAccessibleByOperator("operator-a")).thenReturn(List.of(appointment));
var result = service.getOperatorAppointments("operator-a");
assertEquals(1, result.size());
verify(appointmentMapper).findAccessibleByOperator("operator-a");
verify(appointmentMapper, never()).findByUserId("operator-a");
}
@Test
void stationOperatorCannotCompleteAppointmentOutsideStationOrTaskScope() {
ServiceAppointment appointment = appointment("appointment-1", "user-a", "operator-a");
appointment.setStatus("confirmed");
when(appointmentMapper.selectById("appointment-1")).thenReturn(appointment);
when(appointmentMapper.canAccessByOperator("appointment-1", "operator-b")).thenReturn(false);
assertThrows(AccessDeniedException.class,
() -> service.completeAppointment("appointment-1", "operator-b"));
verify(appointmentMapper, never()).updateById(any());
verify(auditLogMapper).insert(argThat(audit ->
"DATA_SCOPE_DENIED".equals(audit.getAction())
&& "operator-b".equals(audit.getOperatorId())));
}
private ServiceAppointment appointment(String id, String userId, String operatorId) {
ServiceAppointment appointment = new ServiceAppointment();
appointment.setId(id);
appointment.setUserId(userId);
appointment.setAssignedOperatorId(operatorId);
appointment.setServiceLocationId("station-1");
appointment.setStatus("submitted");
appointment.setCreatedAt(Instant.now());
return appointment;
}
}
@@ -1,13 +1,19 @@
package com.danyang.match.platform.shared.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SpringBootTest
@AutoConfigureMockMvc
@@ -16,9 +22,21 @@ class HealthControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private StringRedisTemplate redisTemplate;
@MockBean
private ValueOperations<String, String> valueOperations;
@BeforeEach
void setUpRedisHealthStub() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get("_health_check_")).thenReturn("ok");
}
@Test
void testHealthLive() throws Exception {
mockMvc.perform(get("/api/v1/health/live"))
mockMvc.perform(get("/api/v1/health/live").contextPath("/api/v1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.status").value("UP"));
@@ -26,7 +44,7 @@ class HealthControllerTest {
@Test
void testHealthReady() throws Exception {
mockMvc.perform(get("/api/v1/health/ready"))
mockMvc.perform(get("/api/v1/health/ready").contextPath("/api/v1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.status").value("READY"));
@@ -34,7 +52,7 @@ class HealthControllerTest {
@Test
void testVersion() throws Exception {
mockMvc.perform(get("/api/v1/version"))
mockMvc.perform(get("/api/v1/version").contextPath("/api/v1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
@@ -0,0 +1,101 @@
package com.danyang.match.platform.shared.filter;
import com.danyang.match.platform.auth.token.JwtTokenService;
import com.danyang.match.platform.permission.service.PermissionService;
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
import com.danyang.match.platform.shared.security.principal.UserPrincipal;
import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class JwtAuthFilterTest {
@Mock private JwtTokenService jwtTokenService;
@Mock private PermissionService permissionService;
@Mock private Claims claims;
@InjectMocks private JwtAuthFilter filter;
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void operatorTokenCreatesAuthenticatedOperatorPrincipalWithPermissionAuthorities() throws Exception {
String token = "operator-token";
when(jwtTokenService.validateToken(token)).thenReturn(true);
when(jwtTokenService.getUserIdFromToken(token)).thenReturn("operator-1");
when(jwtTokenService.parseToken(token)).thenReturn(claims);
when(claims.get("type", String.class)).thenReturn("operator_access");
when(permissionService.getPermissions("operator-1"))
.thenReturn(List.of("content.edit", "content.publish"));
MockHttpServletRequest request = authenticatedRequest(token);
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, new MockFilterChain());
var authentication = SecurityContextHolder.getContext().getAuthentication();
assertNotNull(authentication);
assertTrue(authentication.isAuthenticated());
assertInstanceOf(OperatorPrincipal.class, authentication.getPrincipal());
assertEquals("operator-1", ((OperatorPrincipal) authentication.getPrincipal()).id());
assertTrue(authentication.getAuthorities().stream()
.anyMatch(authority -> "content.publish".equals(authority.getAuthority())));
assertTrue(authentication.getAuthorities().stream()
.anyMatch(authority -> "ROLE_OPERATOR".equals(authority.getAuthority())));
}
@Test
void userTokenCreatesUserPrincipalWithoutLoadingOperatorPermissions() throws Exception {
String token = "user-token";
when(jwtTokenService.validateToken(token)).thenReturn(true);
when(jwtTokenService.getUserIdFromToken(token)).thenReturn("user-1");
when(jwtTokenService.parseToken(token)).thenReturn(claims);
when(claims.get("type", String.class)).thenReturn("access");
MockHttpServletRequest request = authenticatedRequest(token);
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, new MockFilterChain());
var authentication = SecurityContextHolder.getContext().getAuthentication();
assertNotNull(authentication);
assertInstanceOf(UserPrincipal.class, authentication.getPrincipal());
assertEquals("user-1", ((UserPrincipal) authentication.getPrincipal()).id());
assertFalse(authentication.getAuthorities().stream()
.anyMatch(authority -> "ROLE_OPERATOR".equals(authority.getAuthority())));
verifyNoInteractions(permissionService);
}
@Test
void invalidTokenReturns401AndDoesNotAuthenticate() throws Exception {
String token = "invalid-token";
when(jwtTokenService.validateToken(token)).thenReturn(false);
MockHttpServletRequest request = authenticatedRequest(token);
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, new MockFilterChain());
assertEquals(401, response.getStatus());
assertNull(SecurityContextHolder.getContext().getAuthentication());
}
private MockHttpServletRequest authenticatedRequest(String token) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/content/operator/articles");
request.addHeader("Authorization", "Bearer " + token);
return request;
}
}
@@ -22,3 +22,11 @@ features:
matchmaker-enabled: true
chat-image-enabled: false
appeal-enabled: false
object-storage:
endpoint: http://localhost:9000
access-key: unit-test-access-key
secret-key: unit-test-secret-key
bucket-public: unit-test-public
bucket-private: unit-test-private
region: us-east-1
+2 -2
View File
@@ -7,8 +7,8 @@
| 当前执行项 | 状态 | 依据 |
|---|---|---|
| Phase 01 项目治理与检查基线 | 已交付,带阻断 | 治理基线与双平台检查入口已交付;Git 元数据缺失、后端环境不可用及三端失败项已如实记录 |
| Phase 02 及以后 | 未获准执行 | 必须等待 Phase 01 Completion Report 后用户确认 |
| Phase 01 项目治理与检查基线 | 已完成 | Git 基线已建立,提交 `b46b48e` |
| Phase 02 统一认证、RBAC、DataScope | 已完成 | 见 `Phase-02-V11-Recovery-Completion-Report-2026-09-10.md`;Phase 03 尚未获准 |
| V1.1 UAT / RC / Production | NO-GO | 构建、权限、E2E 和页面闭环尚未通过 |
---

Some files were not shown because too many files have changed in this diff Show More