diff --git a/admin/src/api/user.ts b/admin/src/api/user.ts index a9d5bae..6f69e58 100644 --- a/admin/src/api/user.ts +++ b/admin/src/api/user.ts @@ -96,3 +96,12 @@ export function updateMemberLevel(id: number, memberLevel: number, memberExpireT export function cancelRealName(id: number): Promise { return request.put(`/admin/users/${id}/cancel-realname`) } + +/** + * 刷新用户推荐列表(清空今日推荐并重新生成) + * @param id 用户ID + * @returns 重新生成的推荐数量 + */ +export function refreshRecommend(id: number): Promise { + return request.post(`/admin/users/${id}/refresh-recommend`) +} diff --git a/admin/src/views/user/detail.vue b/admin/src/views/user/detail.vue index 797bad7..22cbdfd 100644 --- a/admin/src/views/user/detail.vue +++ b/admin/src/views/user/detail.vue @@ -9,7 +9,6 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { ArrowLeft } from '@element-plus/icons-vue' import StatusTag from '@/components/StatusTag/index.vue' import { getUserDetail, updateUserStatus, updateContactCount, updateMemberLevel, cancelRealName } from '@/api/user' -import { getMemberTierList } from '@/api/memberTier' import { getFullImageUrl } from '@/utils/image' import type { UserDetail } from '@/types/user.d' @@ -107,26 +106,14 @@ const handleEditContactCount = async () => { } } -// 会员等级选项 - 从后台配置动态获取 -const memberLevelOptions = ref([ - { value: 0, label: '非会员' } -]) - -// 加载会员等级配置 -const loadMemberTierOptions = async () => { - try { - const res = await getMemberTierList() - const tiers = res.data || res || [] - if (Array.isArray(tiers) && tiers.length > 0) { - memberLevelOptions.value = [ - { value: 0, label: '非会员' }, - ...tiers.map((t: any) => ({ value: t.level, label: t.name })) - ] - } - } catch (error) { - console.error('加载会员等级配置失败:', error) - } -} +// 会员等级选项 - 固定等级名称 +const memberLevelOptions = [ + { value: 0, label: '非会员' }, + { value: 1, label: '等级1 - 不限时会员' }, + { value: 2, label: '等级2 - 诚意会员' }, + { value: 3, label: '等级3 - 家庭版会员' }, + { value: 4, label: '等级4 - 限时会员' } +] // 修改会员等级对话框 const memberLevelDialogVisible = ref(false) @@ -270,7 +257,6 @@ const formatIncome = (min: number | undefined, max: number | undefined) => { onMounted(() => { fetchUserDetail() - loadMemberTierOptions() }) diff --git a/admin/src/views/user/list.vue b/admin/src/views/user/list.vue index 4586661..907ca98 100644 --- a/admin/src/views/user/list.vue +++ b/admin/src/views/user/list.vue @@ -6,11 +6,11 @@ import { ref, reactive, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { View, Edit, Plus, Delete } from '@element-plus/icons-vue' +import { View, Edit, Plus, Delete, Refresh } from '@element-plus/icons-vue' import SearchForm from '@/components/SearchForm/index.vue' import Pagination from '@/components/Pagination/index.vue' import StatusTag from '@/components/StatusTag/index.vue' -import { getUserList, updateUserStatus, createTestUsers, deleteUser } from '@/api/user' +import { getUserList, updateUserStatus, createTestUsers, deleteUser, refreshRecommend } from '@/api/user' import { getMemberTierList } from '@/api/memberTier' import { getFullImageUrl } from '@/utils/image' import type { UserListItem, UserQueryParams } from '@/types/user.d' @@ -245,6 +245,28 @@ const handleCreateTestUsers = async () => { } } +// 刷新用户推荐 +const handleRefreshRecommend = async (row: UserListItem) => { + try { + await ElMessageBox.confirm( + `确定要刷新用户「${row.nickname || row.xiangQinNo}」的今日推荐列表吗?将清空现有推荐并按当前会员等级重新生成。`, + '刷新推荐', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ) + + const count = await refreshRecommend(row.userId) + ElMessage.success(`已重新生成${count}条推荐`) + } catch (error) { + if (error !== 'cancel') { + console.error('刷新推荐失败:', error) + } + } +} + onMounted(() => { fetchUserList() loadMemberTierOptions() @@ -516,7 +538,7 @@ onMounted(() => { @@ -530,6 +552,14 @@ onMounted(() => { > 详情 + + 刷新推荐 + _logger; - public AdminUserController(IAdminUserService adminUserService, ILogger logger) + public AdminUserController(IAdminUserService adminUserService, IRecommendService recommendService, ILogger logger) { _adminUserService = adminUserService; + _recommendService = recommendService; _logger = logger; } @@ -159,6 +161,27 @@ public class AdminUserController : ControllerBase var result = await _adminUserService.CancelRealNameAsync(id, adminId); return result ? ApiResponse.Success("取消实名成功") : ApiResponse.Error(40001, "取消实名失败"); } + + /// + /// 刷新用户今日推荐(清空并重新生成) + /// + /// 用户ID + /// 重新生成的推荐数量 + [HttpPost("{id}/refresh-recommend")] + [OperationLog("用户管理", "修改", Description = "刷新用户推荐列表")] + public async Task> RefreshRecommend(long id) + { + try + { + var count = await _recommendService.RefreshDailyRecommendAsync(id); + return ApiResponse.Success(count, $"已重新生成{count}条推荐"); + } + catch (Exception ex) + { + _logger.LogError(ex, "刷新用户 {UserId} 推荐失败", id); + return ApiResponse.Error(40001, "刷新推荐失败:" + ex.Message); + } + } } /// diff --git a/server/src/XiangYi.Application/Interfaces/IRecommendService.cs b/server/src/XiangYi.Application/Interfaces/IRecommendService.cs index d06c2ea..9b72b64 100644 --- a/server/src/XiangYi.Application/Interfaces/IRecommendService.cs +++ b/server/src/XiangYi.Application/Interfaces/IRecommendService.cs @@ -60,4 +60,11 @@ public interface IRecommendService /// 用户ID /// 推荐用户ID Task MarkAsViewedAsync(long userId, long recommendUserId); + + /// + /// 清空用户今日推荐并重新生成 + /// + /// 用户ID + /// 重新生成的推荐数量 + Task RefreshDailyRecommendAsync(long userId); } diff --git a/server/src/XiangYi.Application/Services/PaymentService.cs b/server/src/XiangYi.Application/Services/PaymentService.cs index ec4b6bc..37cff5d 100644 --- a/server/src/XiangYi.Application/Services/PaymentService.cs +++ b/server/src/XiangYi.Application/Services/PaymentService.cs @@ -20,6 +20,7 @@ public class PaymentService : IPaymentService private readonly IRepository _tierConfigRepository; private readonly IRepository _userRepository; private readonly IWeChatPayService _weChatPayService; + private readonly IRecommendService _recommendService; private readonly ILogger _logger; public PaymentService( @@ -28,6 +29,7 @@ public class PaymentService : IPaymentService IRepository tierConfigRepository, IRepository userRepository, IWeChatPayService weChatPayService, + IRecommendService recommendService, ILogger logger) { _orderRepository = orderRepository; @@ -35,6 +37,7 @@ public class PaymentService : IPaymentService _tierConfigRepository = tierConfigRepository; _userRepository = userRepository; _weChatPayService = weChatPayService; + _recommendService = recommendService; _logger = logger; } @@ -244,6 +247,17 @@ public class PaymentService : IPaymentService _logger.LogInformation("会员开通成功: UserId={UserId}, Level={Level}, ExpireTime={ExpireTime}", order.UserId, memberLevel, expireTime); + + // 会员等级变更后,重新生成今日推荐(按新等级的推荐数量) + try + { + await _recommendService.RefreshDailyRecommendAsync(order.UserId); + _logger.LogInformation("会员开通后已刷新推荐: UserId={UserId}", order.UserId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "会员开通后刷新推荐失败: UserId={UserId}(不影响会员开通)", order.UserId); + } } /// diff --git a/server/src/XiangYi.Application/Services/RecommendService.cs b/server/src/XiangYi.Application/Services/RecommendService.cs index 038cf6b..528a14d 100644 --- a/server/src/XiangYi.Application/Services/RecommendService.cs +++ b/server/src/XiangYi.Application/Services/RecommendService.cs @@ -281,9 +281,15 @@ public class RecommendService : IRecommendService // 多获取一些候选用户,弥补查询时可能被过滤掉的(用户被禁用、资料未审核等) var candidateCount = (int)Math.Ceiling(recommendCount * 1.5); - // 获取候选用户 + // 获取候选用户(多取一些,弥补过滤损耗) var candidates = await GetCandidateUsersAsync(userId, userProfile, userRequirement, candidateCount); + // 按实际推荐数量截断 + if (candidates.Count > recommendCount) + { + candidates = candidates.Take(recommendCount).ToList(); + } + // 删除今日已有的推荐 var today = DateTime.Today; await _recommendRepository.DeleteAsync(r => r.UserId == userId && r.RecommendDate == today); @@ -522,6 +528,17 @@ public class RecommendService : IRecommendService } } + /// + public async Task RefreshDailyRecommendAsync(long userId) + { + // 删除今日推荐记录 + var today = DateTime.Today; + await _recommendRepository.DeleteAsync(r => r.UserId == userId && r.RecommendDate == today); + + // 重新生成 + return await GenerateDailyRecommendForUserAsync(userId); + } + /// /// 获取候选用户列表 ///