feat: complete phase 02 RBAC and data scope recovery
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
backend/local-maven/
|
||||
backend/build-temp/
|
||||
backend/.m2/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.lock
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'] } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
+14
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-34
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-24
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-25
@@ -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 {
|
||||
|
||||
|
||||
+8
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-20
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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 {
|
||||
|
||||
|
||||
+19
-28
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -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));
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -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);
|
||||
|
||||
+19
-29
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-41
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-8
@@ -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());
|
||||
|
||||
+5
-16
@@ -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)
|
||||
|
||||
+4
-15
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-21
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-28
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,13 +35,13 @@ public class MessageRiskService {
|
||||
"博彩", "赌博", "彩票", "六合彩",
|
||||
"高收益", "稳赚", "内幕",
|
||||
"刷单", "兼职", "快递费", "保证金",
|
||||
"验证码", "密码", "身份证号",
|
||||
"验证码", "密码", "身份证号", "加微信", "加你微信", "加我微信",
|
||||
"诈骗", "骗子", "假冒"
|
||||
);
|
||||
|
||||
// 普通风险关键词(仅记录,不提示)
|
||||
private static final Set<String> MEDIUM_RISK_KEYWORDS = Set.of(
|
||||
"电话", "加微信", "加QQ", "加群",
|
||||
"电话", "加QQ", "加群",
|
||||
"地址", "门牌", "小区", "楼栋",
|
||||
"公司", "单位", "工作服"
|
||||
);
|
||||
|
||||
+10
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-37
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-29
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-27
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-24
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -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);
|
||||
}
|
||||
|
||||
+17
-1
@@ -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());
|
||||
|
||||
+24
-27
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -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";
|
||||
|
||||
|
||||
+5
-1
@@ -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);
|
||||
|
||||
+7
-3
@@ -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) {
|
||||
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.danyang.match.platform.shared.security.principal;
|
||||
|
||||
/** 当前请求中已经通过 JWT 验证的主体。 */
|
||||
public sealed interface PlatformPrincipal permits UserPrincipal, OperatorPrincipal {
|
||||
String id();
|
||||
}
|
||||
+10
@@ -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不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-21
@@ -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);
|
||||
+14
@@ -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());
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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);
|
||||
|
||||
+4
-9
@@ -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
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+3
-6
@@ -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],应被过滤"
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
+38
@@ -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());
|
||||
}
|
||||
}
|
||||
+80
@@ -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/");
|
||||
}
|
||||
}
|
||||
+41
@@ -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("<script>"), "script 标签必须被 HTML 转义");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+109
@@ -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() {}
|
||||
}
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
+85
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
-3
@@ -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));
|
||||
}
|
||||
|
||||
+101
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user