From d6f5f25c35e9bd7830d7c26e7e9870cc0928aad8 Mon Sep 17 00:00:00 2001
From: g1307092382 <1307092382@qq.com>
Date: Thu, 10 Sep 2026 21:31:38 +0800
Subject: [PATCH] fix: repair admin page api contracts
---
.../modules/activity/ActivityInterestView.vue | 18 +++----
.../src/modules/analytics/AnalyticsView.vue | 53 ++++++++++---------
.../src/modules/appeal/AppealCenterView.vue | 15 +++---
.../modules/banner/BannerManagementView.vue | 38 ++++++-------
.../operator/OperatorManagementView.vue | 6 +--
.../modules/organization/OrganizationView.vue | 2 +-
.../src/modules/risk/ReportCenterView.vue | 13 ++---
.../ServiceAppointmentView.vue | 15 +++---
.../service-station/ServiceStationView.vue | 16 +++---
.../src/modules/user/UserManagementView.vue | 4 +-
.../ActivityInterestController.java | 23 ++++++++
.../ActivityOperatorController.java | 2 +-
.../service/ActivityInterestService.java | 4 ++
.../HomeBannerOperatorController.java | 49 +++++++++++++++++
.../discover/service/DiscoverService.java | 2 +-
.../home/service/HomeApplicationService.java | 3 +-
.../service/RecommendationEngineService.java | 2 +-
.../messaging/service/MessagingService.java | 2 +-
.../platform/photo/service/PhotoService.java | 5 +-
.../platform/profile/entity/UserProfile.java | 2 +-
.../service/RelationshipStatusService.java | 8 +--
.../ServiceAppointmentController.java | 10 ++++
.../ServiceLocationOperatorController.java | 41 ++++++++++++++
.../service/ServiceAppointmentService.java | 15 ++++++
.../platform/shared/base/AutoFillHandler.java | 5 +-
.../platform/shared/base/BaseEntity.java | 5 +-
.../match/platform/user/entity/User.java | 2 -
.../platform/user/service/UserService.java | 19 +++++--
.../V032__align_admin_soft_delete_columns.sql | 5 ++
ui-tests/ui-contracts.mjs | 12 +++++
30 files changed, 286 insertions(+), 110 deletions(-)
create mode 100644 backend/src/main/java/com/danyang/match/platform/banner/controller/HomeBannerOperatorController.java
create mode 100644 backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceLocationOperatorController.java
create mode 100644 backend/src/main/resources/db/migration/V032__align_admin_soft_delete_columns.sql
diff --git a/admin-web/src/modules/activity/ActivityInterestView.vue b/admin-web/src/modules/activity/ActivityInterestView.vue
index da8840d..5f024d2 100644
--- a/admin-web/src/modules/activity/ActivityInterestView.vue
+++ b/admin-web/src/modules/activity/ActivityInterestView.vue
@@ -31,8 +31,8 @@
{{ currentActivity.title }} — 双向互选列表
-
-
+
+
@@ -57,7 +57,10 @@ async function loadData() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/activities/operator/activity-interest-summary', {}, { Authorization: `Bearer ${token}` })
- tableData.value = res || []
+ const rows = res.data || []
+ tableData.value = searchActivityId.value
+ ? rows.filter((row: any) => row.activityId.includes(searchActivityId.value.trim()))
+ : rows
} catch (e) {
console.error(e)
} finally {
@@ -67,14 +70,11 @@ async function loadData() {
async function viewMutuals(activityId: string) {
drawerVisible.value = true
+ currentActivity.value = tableData.value.find(row => row.activityId === activityId)
try {
const token = localStorage.getItem('accessToken')
- const [actRes, mutualRes]: any = await Promise.all([
- get(`/activities/${activityId}`, {}, { Authorization: `Bearer ${token}` }),
- get(`/activities/${activityId}/interests/mutual`, {}, { Authorization: `Bearer ${token}` })
- ])
- currentActivity.value = actRes
- mutualList.value = mutualRes || []
+ const mutualRes: any = await get(`/activities/operator/${activityId}/mutuals`, {}, { Authorization: `Bearer ${token}` })
+ mutualList.value = mutualRes.data || []
} catch (e) {
console.error(e)
}
diff --git a/admin-web/src/modules/analytics/AnalyticsView.vue b/admin-web/src/modules/analytics/AnalyticsView.vue
index 4958797..1b80163 100644
--- a/admin-web/src/modules/analytics/AnalyticsView.vue
+++ b/admin-web/src/modules/analytics/AnalyticsView.vue
@@ -122,34 +122,37 @@ async function loadFunnel() {
}, { Authorization: `Bearer ${token}` }),
get('/operator/analytics/safety', {}, { Authorization: `Bearer ${token}` })
])
- funnelStats.value = userRes
- matchFunnelStats.value = matchRes
- safetyStats.value = safetyRes
+ const userData = userRes.data || {}
+ const matchData = matchRes.data || {}
+ const safetyData = safetyRes.data || {}
+ funnelStats.value = userData
+ matchFunnelStats.value = matchData
+ safetyStats.value = safetyData
// 更新漏斗显示
- funnelStages[0].value = userRes.totalRegistered || 0
- funnelStages[1].value = userRes.verifiedCount || 0
- funnelStages[1].rate = userRes.verifyRate || '-'
- funnelStages[2].value = userRes.profileApproved || 0
- funnelStages[2].rate = userRes.approvalRate || '-'
- funnelStages[3].value = userRes.activeUsers || 0
- funnelStages[3].rate = userRes.activeRate || '-'
+ funnelStages[0].value = userData.totalRegistered || 0
+ funnelStages[1].value = userData.verifiedCount || 0
+ funnelStages[1].rate = userData.verifyRate || '-'
+ funnelStages[2].value = userData.profileApproved || 0
+ funnelStages[2].rate = userData.approvalRate || '-'
+ funnelStages[3].value = userData.activeUsers || 0
+ funnelStages[3].rate = userData.activeRate || '-'
- matchStages[0].value = matchRes.totalRecommended || 0
- matchStages[1].value = matchRes.totalLiked || 0
- matchStages[1].rate = matchRes.likeRate || '-'
- matchStages[2].value = matchRes.totalMatches || 0
- matchStages[2].rate = matchRes.matchRate || '-'
- matchStages[3].value = matchRes.chatsStarted || 0
- matchStages[3].rate = matchRes.chatRate || '-'
- matchStages[4].value = matchRes.gettingToKnow || 0
- matchStages[5].value = matchRes.successPairs || 0
- matchStages[5].rate = matchRes.successRate || '-'
+ matchStages[0].value = matchData.totalRecommended || 0
+ matchStages[1].value = matchData.totalLiked || 0
+ matchStages[1].rate = matchData.likeRate || '-'
+ matchStages[2].value = matchData.totalMatches || 0
+ matchStages[2].rate = matchData.matchRate || '-'
+ matchStages[3].value = matchData.chatsStarted || 0
+ matchStages[3].rate = matchData.chatRate || '-'
+ matchStages[4].value = matchData.gettingToKnow || 0
+ matchStages[5].value = matchData.successPairs || 0
+ matchStages[5].rate = matchData.successRate || '-'
- safetyCards[0].value = safetyRes.pendingReports || 0
- safetyCards[1].value = safetyRes.resolvedReports || 0
- safetyCards[2].value = safetyRes.highRiskUsers || 0
- safetyCards[3].value = safetyRes.bannedUsers || 0
+ safetyCards[0].value = safetyData.pendingReports || 0
+ safetyCards[1].value = safetyData.resolvedReports || 0
+ safetyCards[2].value = safetyData.highRiskUsers || 0
+ safetyCards[3].value = safetyData.bannedUsers || 0
}
} catch (e) { console.error(e) }
}
@@ -158,7 +161,7 @@ async function loadDailySummary() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/analytics/daily-summary', { days: 30 }, { Authorization: `Bearer ${token}` })
- dailySummary.value = res || []
+ dailySummary.value = res.data || []
} catch (e) { console.error(e) }
}
diff --git a/admin-web/src/modules/appeal/AppealCenterView.vue b/admin-web/src/modules/appeal/AppealCenterView.vue
index 00ce6ca..f7e6e52 100644
--- a/admin-web/src/modules/appeal/AppealCenterView.vue
+++ b/admin-web/src/modules/appeal/AppealCenterView.vue
@@ -110,7 +110,7 @@ async function loadData() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/appeals/pending', {}, { Authorization: `Bearer ${token}` })
- tableData.value = res || []
+ tableData.value = res.data || []
loadStats()
} catch (e) { console.error(e) }
finally { loading.value = false }
@@ -120,12 +120,13 @@ async function loadStats() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/appeals/stats', {}, { Authorization: `Bearer ${token}` })
+ const data = res.data || {}
stats.value = [
- { key: 'pending', label: '待审核', value: res.pendingCount || 0, color: '#E96B6B' },
- { key: 'reviewing', label: '审核中', value: res.reviewingCount || 0, color: '#E59A32' },
- { key: 'approved', label: '已通过', value: res.approvedCount || 0, color: '#67C23A' },
- { key: 'rejected', label: '已驳回', value: res.rejectedCount || 0, color: '#909399' },
- { key: 'escalated', label: '已升级', value: res.escalatedCount || 0, color: '#F56C6C' }
+ { key: 'pending', label: '待审核', value: data.pendingCount || 0, color: '#E96B6B' },
+ { key: 'reviewing', label: '审核中', value: data.reviewingCount || 0, color: '#E59A32' },
+ { key: 'approved', label: '已通过', value: data.approvedCount || 0, color: '#67C23A' },
+ { key: 'rejected', label: '已驳回', value: data.rejectedCount || 0, color: '#909399' },
+ { key: 'escalated', label: '已升级', value: data.escalatedCount || 0, color: '#F56C6C' }
]
} catch {}
}
@@ -138,7 +139,7 @@ async function openDetail(id: string) {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get(`/operator/appeals/${id}`, {}, { Authorization: `Bearer ${token}` })
- detail.value = res
+ detail.value = res.data
} catch (e) { console.error(e) }
}
diff --git a/admin-web/src/modules/banner/BannerManagementView.vue b/admin-web/src/modules/banner/BannerManagementView.vue
index 576853f..5d01b45 100644
--- a/admin-web/src/modules/banner/BannerManagementView.vue
+++ b/admin-web/src/modules/banner/BannerManagementView.vue
@@ -9,14 +9,14 @@
-
+
- {{ bannerTargetTypeLabel(row.target_type) }}
+ {{ bannerTargetTypeLabel(row.targetType) }}
-
+
- {{ audienceScopeLabel(row.audience_scope) }}
+ {{ audienceScopeLabel(row.audienceScope) }}
@@ -24,10 +24,10 @@
{{ statusLabel(row.status) }}
-
+
- {{ formatDateTime(row.start_at) }} ~ {{ formatDateTime(row.end_at) }}
+ {{ formatDateTime(row.startAt) }} ~ {{ formatDateTime(row.endAt) }}
@@ -47,7 +47,7 @@
-
+
@@ -58,9 +58,9 @@
-
+
-
+
@@ -68,9 +68,9 @@
-
-
-
+
+
+
取消
@@ -89,7 +89,7 @@ const loading = ref(false)
const banners = ref([])
const dialogVisible = ref(false)
const editingId = ref(null)
-const form = ref({ title: '', subtitle: '', target_type: 'none', target_value: '', audience_scope: 'all', sort_order: 0, start_at: '', end_at: '' })
+const form = ref({ title: '', subtitle: '', targetType: 'none', targetValue: '', audienceScope: 'all', sortOrder: 0, startAt: '', endAt: '' })
const TARGET_TYPE_LABEL: Record = { activity: '活动', matchmaker: '红娘', counseling: '咨询', premarital: '婚前辅导', service_location: '服务站', article: '文章', internal_page: '内部页', none: '无' }
const AUDIENCE_SCOPE_LABEL: Record = { all: '全部', onboarding: '未注册', searching: '寻找中', getting_to_know: '初步了解', successful: '成功牵手' }
@@ -106,21 +106,21 @@ function formatDateTime(d: string) { return d ? new Date(d).toLocaleString('zh-C
async function loadBanners() {
loading.value = true
try {
- const res: any = await get('/home/banners')
+ const res: any = await get('/operator/banners')
banners.value = res.data || []
} catch (e) { console.error(e) } finally { loading.value = false }
}
-function openCreate() { editingId.value = null; Object.assign(form.value, { title: '', subtitle: '', target_type: 'none', target_value: '', audience_scope: 'all', sort_order: 0, start_at: '', end_at: '' }); dialogVisible.value = true }
-function openEdit(row: any) { editingId.value = row.id; Object.assign(form.value, { title: row.title, subtitle: row.subtitle, target_type: row.target_type, target_value: row.target_value, audience_scope: row.audience_scope, sort_order: row.sort_order, start_at: row.start_at, end_at: row.end_at }); dialogVisible.value = true }
+function openCreate() { editingId.value = null; Object.assign(form.value, { title: '', subtitle: '', targetType: 'none', targetValue: '', audienceScope: 'all', sortOrder: 0, startAt: '', endAt: '' }); dialogVisible.value = true }
+function openEdit(row: any) { editingId.value = row.id; Object.assign(form.value, { title: row.title, subtitle: row.subtitle, targetType: row.targetType, targetValue: row.targetValue, audienceScope: row.audienceScope, sortOrder: row.sortOrder, startAt: row.startAt, endAt: row.endAt }); dialogVisible.value = true }
async function saveBanner() {
try {
if (editingId.value) {
- await put(`/home/banners/${editingId.value}`, form.value)
+ await put(`/operator/banners/${editingId.value}`, form.value)
ElMessage.success('更新成功')
} else {
- await post('/home/banners', form.value)
+ await post('/operator/banners', form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
@@ -131,7 +131,7 @@ async function saveBanner() {
async function toggleStatus(row: any) {
try {
const newStatus = row.status === 'active' ? 'inactive' : 'active'
- await put(`/home/banners/${row.id}`, { ...row, status: newStatus })
+ await put(`/operator/banners/${row.id}`, { ...row, status: newStatus })
await loadBanners()
} catch (e) { ElMessage.error('操作失败') }
}
diff --git a/admin-web/src/modules/operator/OperatorManagementView.vue b/admin-web/src/modules/operator/OperatorManagementView.vue
index ee5e8b5..b0ddb8a 100644
--- a/admin-web/src/modules/operator/OperatorManagementView.vue
+++ b/admin-web/src/modules/operator/OperatorManagementView.vue
@@ -81,8 +81,8 @@ async function loadData() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/operators', { page: page.value, pageSize: pageSize.value }, { Authorization: `Bearer ${token}` })
- tableData.value = res.list || []
- total.value = res.total || 0
+ tableData.value = res.data?.list || []
+ total.value = res.data?.total || 0
} catch (e) { console.error(e) }
finally { loading.value = false }
}
@@ -91,7 +91,7 @@ async function loadOrgs() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/operators/organizations', {}, { Authorization: `Bearer ${token}` })
- orgList.value = res || []
+ orgList.value = res.data || []
orgList.value.forEach(o => { orgMap.value[o.id] = o.name })
} catch {}
}
diff --git a/admin-web/src/modules/organization/OrganizationView.vue b/admin-web/src/modules/organization/OrganizationView.vue
index 4b53512..07e8b02 100644
--- a/admin-web/src/modules/organization/OrganizationView.vue
+++ b/admin-web/src/modules/organization/OrganizationView.vue
@@ -80,7 +80,7 @@ async function loadData() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/organizations', {}, { Authorization: `Bearer ${token}` })
- tableData.value = res || []
+ tableData.value = res.data || []
} catch (e) { console.error(e) }
finally { loading.value = false }
}
diff --git a/admin-web/src/modules/risk/ReportCenterView.vue b/admin-web/src/modules/risk/ReportCenterView.vue
index be8085d..ef188f8 100644
--- a/admin-web/src/modules/risk/ReportCenterView.vue
+++ b/admin-web/src/modules/risk/ReportCenterView.vue
@@ -125,7 +125,7 @@ async function loadData() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get(`/operator/risk/reports/${statusFilter.value}`, {}, { Authorization: `Bearer ${token}` })
- tableData.value = res || []
+ tableData.value = res.data || []
loadStats()
} catch (e) { console.error(e) }
finally { loading.value = false }
@@ -135,11 +135,12 @@ async function loadStats() {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get('/operator/risk/reports/stats', {}, { Authorization: `Bearer ${token}` })
+ const data = res.data || {}
stats.value = [
- { key: 'pending', label: '待处理', value: res.pendingCount || 0, color: '#E96B6B' },
- { key: 'processing', label: '处理中', value: res.processingCount || 0, color: '#E59A32' },
- { key: 'resolved', label: '已处理', value: res.resolvedCount || 0, color: '#67C23A' },
- { key: 'dismissed', label: '已驳回', value: res.dismissedCount || 0, color: '#909399' }
+ { key: 'pending', label: '待处理', value: data.pendingCount || 0, color: '#E96B6B' },
+ { key: 'processing', label: '处理中', value: data.processingCount || 0, color: '#E59A32' },
+ { key: 'resolved', label: '已处理', value: data.resolvedCount || 0, color: '#67C23A' },
+ { key: 'dismissed', label: '已驳回', value: data.dismissedCount || 0, color: '#909399' }
]
} catch {}
}
@@ -152,7 +153,7 @@ async function openDetail(reportId: string) {
try {
const token = localStorage.getItem('accessToken')
const res: any = await get(`/operator/risk/reports/${reportId}`, {}, { Authorization: `Bearer ${token}` })
- detail.value = res
+ detail.value = res.data
} catch (e) { console.error(e) }
}
diff --git a/admin-web/src/modules/service-appointment/ServiceAppointmentView.vue b/admin-web/src/modules/service-appointment/ServiceAppointmentView.vue
index afc932d..1857051 100644
--- a/admin-web/src/modules/service-appointment/ServiceAppointmentView.vue
+++ b/admin-web/src/modules/service-appointment/ServiceAppointmentView.vue
@@ -3,11 +3,11 @@
@@ -57,8 +57,9 @@ function formatDateTime(d: string) { return d ? new Date(d).toLocaleString('zh-C
async function loadAppointments() {
loading.value = true
try {
- const res: any = await get('/operator/service-appointments', { status: filterStatus.value === 'all' ? undefined : filterStatus.value })
- appointments.value = res.data || []
+ const res: any = await get('/service-appointments/operator/list')
+ const rows = res.data || []
+ appointments.value = filterStatus.value === 'all' ? rows : rows.filter((row: any) => row.status === filterStatus.value)
} finally { loading.value = false }
}
diff --git a/admin-web/src/modules/service-station/ServiceStationView.vue b/admin-web/src/modules/service-station/ServiceStationView.vue
index 8b173f1..5ce7b38 100644
--- a/admin-web/src/modules/service-station/ServiceStationView.vue
+++ b/admin-web/src/modules/service-station/ServiceStationView.vue
@@ -4,11 +4,11 @@
-
+
-
-
- {{ row.opening_hours_text || '-' }}
+
+
+ {{ row.openingHoursText || '-' }}
{{ row.status }}
@@ -24,10 +24,10 @@
-
+
-
-
+
+
@@ -63,7 +63,7 @@ async function loadStations() {
}
function openEdit(row: any) { Object.assign(form.value, row); dialogVisible.value = true }
-function openCreate() { Object.assign(form.value, { name: '', short_name: '', address: '', contact_phone_display: '', opening_hours_text: '', status: 'active' }); dialogVisible.value = true }
+function openCreate() { Object.assign(form.value, { id: undefined, name: '', shortName: '', address: '', contactPhoneDisplay: '', openingHoursText: '', status: 'active' }); dialogVisible.value = true }
async function saveStation() {
try {
diff --git a/admin-web/src/modules/user/UserManagementView.vue b/admin-web/src/modules/user/UserManagementView.vue
index 6abcfbc..cf170df 100644
--- a/admin-web/src/modules/user/UserManagementView.vue
+++ b/admin-web/src/modules/user/UserManagementView.vue
@@ -90,8 +90,8 @@ async function loadData() {
page: page.value,
pageSize: pageSize.value
}, { Authorization: `Bearer ${token}` })
- tableData.value = res.list || []
- total.value = res.total || 0
+ tableData.value = res.data?.list || []
+ total.value = res.data?.total || 0
} catch (e) { console.error(e) }
finally { loading.value = false }
}
diff --git a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityInterestController.java b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityInterestController.java
index 48b538b..4b05293 100644
--- a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityInterestController.java
+++ b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityInterestController.java
@@ -3,6 +3,10 @@ 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.activity.mapper.ActivityMapper;
+import com.danyang.match.platform.activity.entity.Activity;
+import com.danyang.match.platform.activity.entity.ActivityInterest;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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;
@@ -26,6 +30,25 @@ public class ActivityInterestController {
private final ActivityInterestService interestService;
private final RelationshipService relationshipService;
+ private final ActivityMapper activityMapper;
+
+ @GetMapping("/operator/activity-interest-summary")
+ @PreAuthorize("hasAuthority('activity.view')")
+ public ApiResponse>> getActivityInterestSummary() {
+ return ApiResponse.ok(activityMapper.selectList(new LambdaQueryWrapper()
+ .isNull(Activity::getDeletedAt).orderByDesc(Activity::getCreatedAt)).stream().map(activity -> {
+ Map item = new HashMap<>(interestService.getActivityInterestStats(activity.getId()));
+ item.put("activityTitle", activity.getTitle());
+ item.put("feedbackCount", 0);
+ return item;
+ }).toList());
+ }
+
+ @GetMapping("/operator/{id}/mutuals")
+ @PreAuthorize("hasAuthority('activity.view')")
+ public ApiResponse> getMutualsForOperator(@PathVariable String id) {
+ return ApiResponse.ok(interestService.getMutualInterestsForOperator(id));
+ }
/**
* 提交活动反馈 + 表达意向
diff --git a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityOperatorController.java b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityOperatorController.java
index 0c5d6cc..a2adbe6 100644
--- a/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityOperatorController.java
+++ b/backend/src/main/java/com/danyang/match/platform/activity/controller/ActivityOperatorController.java
@@ -96,7 +96,7 @@ public class ActivityOperatorController {
@AuthenticationPrincipal OperatorPrincipal principal) {
List activities = activityMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper()
- .eq(Activity::getDeletedAt, 0)
+ .isNull(Activity::getDeletedAt)
.orderByDesc(Activity::getCreatedAt)
);
return ApiResponse.ok(activities.stream().map(a -> {
diff --git a/backend/src/main/java/com/danyang/match/platform/activity/service/ActivityInterestService.java b/backend/src/main/java/com/danyang/match/platform/activity/service/ActivityInterestService.java
index 6491f84..58bbc61 100644
--- a/backend/src/main/java/com/danyang/match/platform/activity/service/ActivityInterestService.java
+++ b/backend/src/main/java/com/danyang/match/platform/activity/service/ActivityInterestService.java
@@ -207,6 +207,10 @@ public class ActivityInterestService {
return stats;
}
+ public List getMutualInterestsForOperator(String activityId) {
+ return interestMapper.findMutualsByActivity(activityId);
+ }
+
// ==================== 私有辅助方法 ====================
private List getApprovedParticipants(String activityId) {
diff --git a/backend/src/main/java/com/danyang/match/platform/banner/controller/HomeBannerOperatorController.java b/backend/src/main/java/com/danyang/match/platform/banner/controller/HomeBannerOperatorController.java
new file mode 100644
index 0000000..1bf529e
--- /dev/null
+++ b/backend/src/main/java/com/danyang/match/platform/banner/controller/HomeBannerOperatorController.java
@@ -0,0 +1,49 @@
+package com.danyang.match.platform.banner.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.danyang.match.platform.banner.entity.HomeBanner;
+import com.danyang.match.platform.banner.mapper.HomeBannerMapper;
+import com.danyang.match.platform.shared.dto.ApiResponse;
+import com.danyang.match.platform.shared.enums.ApiResponseCode;
+import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
+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;
+
+@RestController
+@RequestMapping("/operator/banners")
+@PreAuthorize("hasAuthority('banner.view')")
+@RequiredArgsConstructor
+public class HomeBannerOperatorController {
+ private final HomeBannerMapper bannerMapper;
+
+ @GetMapping
+ public ApiResponse> list() {
+ return ApiResponse.ok(bannerMapper.selectList(new LambdaQueryWrapper()
+ .isNull(HomeBanner::getDeletedAt).orderByAsc(HomeBanner::getSortOrder)));
+ }
+
+ @PostMapping
+ public ApiResponse create(@AuthenticationPrincipal OperatorPrincipal principal,
+ @RequestBody HomeBanner banner) {
+ banner.setId(null);
+ banner.setCreatedBy(principal.id());
+ banner.setUpdatedBy(principal.id());
+ bannerMapper.insert(banner);
+ return ApiResponse.ok(banner);
+ }
+
+ @PutMapping("/{id}")
+ public ApiResponse update(@AuthenticationPrincipal OperatorPrincipal principal,
+ @PathVariable String id,
+ @RequestBody HomeBanner banner) {
+ if (bannerMapper.selectById(id) == null) return ApiResponse.error(ApiResponseCode.NOT_FOUND);
+ banner.setId(id);
+ banner.setUpdatedBy(principal.id());
+ bannerMapper.updateById(banner);
+ return ApiResponse.ok(bannerMapper.selectById(id));
+ }
+}
diff --git a/backend/src/main/java/com/danyang/match/platform/discover/service/DiscoverService.java b/backend/src/main/java/com/danyang/match/platform/discover/service/DiscoverService.java
index 11adcdb..7967063 100644
--- a/backend/src/main/java/com/danyang/match/platform/discover/service/DiscoverService.java
+++ b/backend/src/main/java/com/danyang/match/platform/discover/service/DiscoverService.java
@@ -35,7 +35,7 @@ public class DiscoverService {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.ne(UserProfile::getGender, myGender)
.eq(UserProfile::getProfileStatus, Constants.PROFILE_STATUS_APPROVED)
- .eq(UserProfile::getDeletedAt, 0);
+ .isNull(UserProfile::getDeletedAt);
// 应用临时筛选条件
if (tempFilter.containsKey("minAge")) {
diff --git a/backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java b/backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java
index 395363e..af9d803 100644
--- a/backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java
+++ b/backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java
@@ -266,7 +266,8 @@ public class HomeApplicationService {
}
private boolean isProfileApproved(User user) {
- String status = user.getProfileStatus();
+ String status = userProfileMapper.findByUserId(user.getId())
+ .map(UserProfile::getProfileStatus).orElse(null);
return Constants.PROFILE_STATUS_APPROVED.equals(status)
|| Constants.PROFILE_STATUS_COMPLETED.equals(status);
}
diff --git a/backend/src/main/java/com/danyang/match/platform/match/service/RecommendationEngineService.java b/backend/src/main/java/com/danyang/match/platform/match/service/RecommendationEngineService.java
index ba09d00..e81e2cc 100644
--- a/backend/src/main/java/com/danyang/match/platform/match/service/RecommendationEngineService.java
+++ b/backend/src/main/java/com/danyang/match/platform/match/service/RecommendationEngineService.java
@@ -159,7 +159,7 @@ public class RecommendationEngineService {
wrapper.isNotNull(UserProfile::getGender)
.ne(UserProfile::getGender, getMyGender(userId))
.eq(UserProfile::getProfileStatus, Constants.PROFILE_STATUS_APPROVED)
- .eq(UserProfile::getDeletedAt, 0);
+ .isNull(UserProfile::getDeletedAt);
// 排除当前用户已Block的人
List blockedIds = relationMapper.findBlockedByUser(userId)
diff --git a/backend/src/main/java/com/danyang/match/platform/messaging/service/MessagingService.java b/backend/src/main/java/com/danyang/match/platform/messaging/service/MessagingService.java
index 3c47241..2e6d333 100644
--- a/backend/src/main/java/com/danyang/match/platform/messaging/service/MessagingService.java
+++ b/backend/src/main/java/com/danyang/match/platform/messaging/service/MessagingService.java
@@ -92,7 +92,7 @@ public class MessagingService {
new LambdaQueryWrapper()
.eq(Message::getSenderId, userId)
.eq(Message::getClientMessageId, clientMessageId)
- .eq(Message::getDeletedAt, null));
+ .isNull(Message::getDeletedAt));
if (existing != null) {
return toMessageDTO(existing, userId);
}
diff --git a/backend/src/main/java/com/danyang/match/platform/photo/service/PhotoService.java b/backend/src/main/java/com/danyang/match/platform/photo/service/PhotoService.java
index 2ea7cf8..b2595a4 100644
--- a/backend/src/main/java/com/danyang/match/platform/photo/service/PhotoService.java
+++ b/backend/src/main/java/com/danyang/match/platform/photo/service/PhotoService.java
@@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
+import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
@@ -94,7 +95,7 @@ public class PhotoService {
photoMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper()
.eq(UserPhoto::getUserId, userId)
.eq(UserPhoto::getPhotoType, "avatar")
- .set(UserPhoto::getDeletedAt, 1));
+ .set(UserPhoto::getDeletedAt, Instant.now()));
}
UserPhoto photo = new UserPhoto();
@@ -213,7 +214,7 @@ public class PhotoService {
public void deletePhoto(String photoId) {
photoMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper()
.eq(UserPhoto::getId, photoId)
- .set(UserPhoto::getDeletedAt, 1));
+ .set(UserPhoto::getDeletedAt, Instant.now()));
log.info("照片已软删除: photoId={}", photoId);
}
diff --git a/backend/src/main/java/com/danyang/match/platform/profile/entity/UserProfile.java b/backend/src/main/java/com/danyang/match/platform/profile/entity/UserProfile.java
index bda2ba0..16d3947 100644
--- a/backend/src/main/java/com/danyang/match/platform/profile/entity/UserProfile.java
+++ b/backend/src/main/java/com/danyang/match/platform/profile/entity/UserProfile.java
@@ -43,5 +43,5 @@ public class UserProfile {
private Instant updatedAt;
@TableField("deleted_at")
- private Integer deletedAt;
+ private Instant deletedAt;
}
diff --git a/backend/src/main/java/com/danyang/match/platform/relationship/service/RelationshipStatusService.java b/backend/src/main/java/com/danyang/match/platform/relationship/service/RelationshipStatusService.java
index 5dbf361..422cfda 100644
--- a/backend/src/main/java/com/danyang/match/platform/relationship/service/RelationshipStatusService.java
+++ b/backend/src/main/java/com/danyang/match/platform/relationship/service/RelationshipStatusService.java
@@ -110,7 +110,7 @@ public class RelationshipStatusService {
RelationshipSuccessRequest existing = successRequestMapper.selectOne(
new LambdaQueryWrapper()
.eq(RelationshipSuccessRequest::getMatchId, match.getId())
- .eq(RelationshipSuccessRequest::getDeletedAt, 0)
+ .isNull(RelationshipSuccessRequest::getDeletedAt)
.orderByDesc(RelationshipSuccessRequest::getCreatedAt)
.last("LIMIT 1"));
@@ -177,7 +177,7 @@ public class RelationshipStatusService {
RelationshipSuccessRequest request = successRequestMapper.selectOne(
new LambdaQueryWrapper()
.eq(RelationshipSuccessRequest::getMatchId, match.getId())
- .eq(RelationshipSuccessRequest::getDeletedAt, 0)
+ .isNull(RelationshipSuccessRequest::getDeletedAt)
.orderByDesc(RelationshipSuccessRequest::getCreatedAt)
.last("LIMIT 1"));
@@ -206,7 +206,7 @@ public class RelationshipStatusService {
.eq(RelationshipSuccessRequest::getMatchId, match.getId())
.eq(RelationshipSuccessRequest::getInitiatedBy, otherUserId)
.eq(RelationshipSuccessRequest::getStatus, Constants.SUCCESS_REQUEST_STATUS_CONFIRMED)
- .eq(RelationshipSuccessRequest::getDeletedAt, 0)
+ .isNull(RelationshipSuccessRequest::getDeletedAt)
.last("LIMIT 1"));
// 5. 标记当前用户已确认
@@ -281,7 +281,7 @@ public class RelationshipStatusService {
RelationshipSuccessRequest request = successRequestMapper.selectOne(
new LambdaQueryWrapper()
.eq(RelationshipSuccessRequest::getMatchId, match.getId())
- .eq(RelationshipSuccessRequest::getDeletedAt, 0)
+ .isNull(RelationshipSuccessRequest::getDeletedAt)
.orderByDesc(RelationshipSuccessRequest::getCreatedAt)
.last("LIMIT 1"));
diff --git a/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceAppointmentController.java b/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceAppointmentController.java
index 04863e0..929967d 100644
--- a/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceAppointmentController.java
+++ b/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceAppointmentController.java
@@ -90,4 +90,14 @@ public class ServiceAppointmentController {
return ApiResponse.ok(serviceAppointmentService.completeAppointment(id, principal.id()));
}
+ @PreAuthorize("hasAuthority('service_appointment.confirm')")
+ @PostMapping("/operator/{id}/cancel")
+ public ApiResponse cancelByOperator(
+ @AuthenticationPrincipal OperatorPrincipal principal,
+ @PathVariable String id,
+ @RequestBody Map body) {
+ return ApiResponse.ok(serviceAppointmentService.cancelAppointmentByOperator(
+ id, principal.id(), body.getOrDefault("reason", "运营取消")));
+ }
+
}
diff --git a/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceLocationOperatorController.java b/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceLocationOperatorController.java
new file mode 100644
index 0000000..b9c837a
--- /dev/null
+++ b/backend/src/main/java/com/danyang/match/platform/service_appointment/controller/ServiceLocationOperatorController.java
@@ -0,0 +1,41 @@
+package com.danyang.match.platform.service_appointment.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.danyang.match.platform.service_appointment.entity.ServiceLocation;
+import com.danyang.match.platform.service_appointment.mapper.ServiceLocationMapper;
+import com.danyang.match.platform.shared.dto.ApiResponse;
+import com.danyang.match.platform.shared.enums.ApiResponseCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/operator/service-locations")
+@PreAuthorize("hasAuthority('service_location.view')")
+@RequiredArgsConstructor
+public class ServiceLocationOperatorController {
+ private final ServiceLocationMapper locationMapper;
+
+ @GetMapping
+ public ApiResponse> list() {
+ return ApiResponse.ok(locationMapper.selectList(new LambdaQueryWrapper()
+ .isNull(ServiceLocation::getDeletedAt).orderByAsc(ServiceLocation::getName)));
+ }
+
+ @PostMapping
+ public ApiResponse create(@RequestBody ServiceLocation location) {
+ location.setId(null);
+ locationMapper.insert(location);
+ return ApiResponse.ok(location);
+ }
+
+ @PutMapping("/{id}")
+ public ApiResponse update(@PathVariable String id, @RequestBody ServiceLocation location) {
+ if (locationMapper.selectById(id) == null) return ApiResponse.error(ApiResponseCode.NOT_FOUND);
+ location.setId(id);
+ locationMapper.updateById(location);
+ return ApiResponse.ok(locationMapper.selectById(id));
+ }
+}
diff --git a/backend/src/main/java/com/danyang/match/platform/service_appointment/service/ServiceAppointmentService.java b/backend/src/main/java/com/danyang/match/platform/service_appointment/service/ServiceAppointmentService.java
index 6f7f9db..1bbca37 100644
--- a/backend/src/main/java/com/danyang/match/platform/service_appointment/service/ServiceAppointmentService.java
+++ b/backend/src/main/java/com/danyang/match/platform/service_appointment/service/ServiceAppointmentService.java
@@ -164,6 +164,21 @@ public class ServiceAppointmentService {
return toDTO(appt);
}
+ @Transactional
+ public ServiceAppointmentDTO cancelAppointmentByOperator(String appointmentId, String operatorId, String reason) {
+ ServiceAppointment appt = appointmentMapper.selectById(appointmentId);
+ if (appt == null || Constants.APPOINTMENT_STATUS_COMPLETED.equals(appt.getStatus())
+ || Constants.APPOINTMENT_STATUS_CANCELLED.equals(appt.getStatus())) {
+ throw new RuntimeException("预约不存在或状态不允许取消");
+ }
+ assertOperatorScope(appointmentId, operatorId);
+ appt.setStatus(Constants.APPOINTMENT_STATUS_CANCELLED);
+ appt.setCancelReason(reason);
+ appt.setUpdatedAt(Instant.now());
+ appointmentMapper.updateById(appt);
+ return toDTO(appt);
+ }
+
private void assertOperatorScope(String appointmentId, String operatorId) {
if (operatorId == null || !appointmentMapper.canAccessByOperator(appointmentId, operatorId)) {
dataScopeGuard.deny(operatorId, "service_appointment", appointmentId,
diff --git a/backend/src/main/java/com/danyang/match/platform/shared/base/AutoFillHandler.java b/backend/src/main/java/com/danyang/match/platform/shared/base/AutoFillHandler.java
index ccd0776..04ec3f3 100644
--- a/backend/src/main/java/com/danyang/match/platform/shared/base/AutoFillHandler.java
+++ b/backend/src/main/java/com/danyang/match/platform/shared/base/AutoFillHandler.java
@@ -9,7 +9,7 @@ import java.util.UUID;
/**
* MyBatis-Plus 自动填充处理器
- * 自动填充:createdAt、updatedAt、deletedAt、id(UUID)
+ * 自动填充:createdAt、updatedAt、id(UUID)
*/
@Component
public class AutoFillHandler implements MetaObjectHandler {
@@ -23,11 +23,10 @@ public class AutoFillHandler implements MetaObjectHandler {
}
this.strictInsertFill(metaObject, "createdAt", Instant.class, Instant.now());
this.strictInsertFill(metaObject, "updatedAt", Instant.class, Instant.now());
- this.strictInsertFill(metaObject, "deletedAt", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", Instant.class, Instant.now());
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/java/com/danyang/match/platform/shared/base/BaseEntity.java b/backend/src/main/java/com/danyang/match/platform/shared/base/BaseEntity.java
index 1335607..113071f 100644
--- a/backend/src/main/java/com/danyang/match/platform/shared/base/BaseEntity.java
+++ b/backend/src/main/java/com/danyang/match/platform/shared/base/BaseEntity.java
@@ -20,7 +20,6 @@ public abstract class BaseEntity {
@TableField(fill = FieldFill.INSERT_UPDATE)
private Instant updatedAt;
- // 软删除标记(NOT NULL, 0=未删除, 1=已删除)
- // 注意:不使用 @TableLogic,由业务代码手动处理
- private Integer deletedAt;
+ // 软删除时间;null 表示未删除。
+ private Instant deletedAt;
}
diff --git a/backend/src/main/java/com/danyang/match/platform/user/entity/User.java b/backend/src/main/java/com/danyang/match/platform/user/entity/User.java
index a6c1638..6005bb6 100644
--- a/backend/src/main/java/com/danyang/match/platform/user/entity/User.java
+++ b/backend/src/main/java/com/danyang/match/platform/user/entity/User.java
@@ -19,8 +19,6 @@ public class User extends BaseEntity {
@TableField("phone")
private String phone;
- private String profileStatus;
-
@TableField("account_status")
private String accountStatus;
diff --git a/backend/src/main/java/com/danyang/match/platform/user/service/UserService.java b/backend/src/main/java/com/danyang/match/platform/user/service/UserService.java
index c9ff4cf..6de0d1e 100644
--- a/backend/src/main/java/com/danyang/match/platform/user/service/UserService.java
+++ b/backend/src/main/java/com/danyang/match/platform/user/service/UserService.java
@@ -59,15 +59,28 @@ public class UserService {
query.eq(User::getAccountStatus, accountStatus);
}
if (profileStatus != null && !profileStatus.isBlank()) {
- query.eq(User::getProfileStatus, profileStatus);
+ List userIds = userProfileMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(UserProfile::getProfileStatus, profileStatus)
+ .select(UserProfile::getUserId))
+ .stream().map(UserProfile::getUserId).toList();
+ query.in(User::getId, userIds.isEmpty()
+ ? List.of("00000000-0000-0000-0000-000000000000") : userIds);
}
LambdaQueryWrapper countQuery = new LambdaQueryWrapper();
if (keyword != null && !keyword.isBlank()) countQuery.and(q -> q.like(User::getId, keyword).or().like(User::getWechatOpenid, keyword));
if (accountStatus != null && !accountStatus.isBlank()) countQuery.eq(User::getAccountStatus, accountStatus);
- if (profileStatus != null && !profileStatus.isBlank()) countQuery.eq(User::getProfileStatus, profileStatus);
+ if (profileStatus != null && !profileStatus.isBlank()) {
+ List userIds = userProfileMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(UserProfile::getProfileStatus, profileStatus)
+ .select(UserProfile::getUserId))
+ .stream().map(UserProfile::getUserId).toList();
+ countQuery.in(User::getId, userIds.isEmpty()
+ ? List.of("00000000-0000-0000-0000-000000000000") : userIds);
+ }
long totalLong = userMapper.selectCount(countQuery); int total = totalLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) totalLong;
- long offsetLong = (page != null ? page : 0L) * (pageSize != null ? pageSize : 20L); int offset = offsetLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) offsetLong;
int limit = pageSize != null ? pageSize : 20;
List users = userMapper.selectPage(
new Page((page != null ? page : 0), limit), query
diff --git a/backend/src/main/resources/db/migration/V032__align_admin_soft_delete_columns.sql b/backend/src/main/resources/db/migration/V032__align_admin_soft_delete_columns.sql
new file mode 100644
index 0000000..3aac852
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V032__align_admin_soft_delete_columns.sql
@@ -0,0 +1,5 @@
+ALTER TABLE organizations ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
+ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
+ALTER TABLE content_categories ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
+ALTER TABLE home_banners ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
+ALTER TABLE service_locations ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
diff --git a/ui-tests/ui-contracts.mjs b/ui-tests/ui-contracts.mjs
index 870771e..933d8b1 100644
--- a/ui-tests/ui-contracts.mjs
+++ b/ui-tests/ui-contracts.mjs
@@ -75,4 +75,16 @@ const adminLayout = read('admin-web/src/layouts/MainLayout.vue')
assert.match(adminLayout, /善缘丹阳/, '后台品牌名称必须为善缘丹阳')
assert.doesNotMatch(adminLayout, /#304156/i, '后台不得继续使用旧式深蓝侧栏')
+const adminApiContracts = [
+ ['admin-web/src/modules/banner/BannerManagementView.vue', '/operator/banners'],
+ ['admin-web/src/modules/service-station/ServiceStationView.vue', '/operator/service-locations'],
+ ['admin-web/src/modules/service-appointment/ServiceAppointmentView.vue', '/service-appointments/operator/list'],
+ ['admin-web/src/modules/activity/ActivityInterestView.vue', '/activities/operator/activity-interest-summary'],
+]
+for (const [file, endpoint] of adminApiContracts) {
+ assert.ok(read(file).includes(endpoint), `${file} 未使用实际后台接口 ${endpoint}`)
+}
+assert.doesNotMatch(read('admin-web/src/modules/risk/ReportCenterView.vue'), /tableData\.value = res \|\|/, '举报列表不能把响应信封当数组')
+assert.doesNotMatch(read('admin-web/src/modules/appeal/AppealCenterView.vue'), /tableData\.value = res \|\|/, '申诉列表不能把响应信封当数组')
+
console.log('UI contracts passed')