聊天,会员

This commit is contained in:
18631081161 2026-02-07 21:50:03 +08:00
parent de2b59ca90
commit 0e84a4fe84
14 changed files with 308 additions and 247 deletions

View File

@ -16,7 +16,8 @@ import type { PopupItem, UpdatePopupRequest } from '@/types/popup.d'
const popupTypes = [
{ type: 1, name: '每日首次弹窗', description: '用户每天首次打开小程序时展示' },
{ type: 2, name: '服务号关注弹窗', description: '引导用户关注公众号' },
{ type: 3, name: '会员广告弹窗', description: '会员推广广告条,显示在首页底部', showDisplayMode: true }
{ type: 3, name: '会员广告弹窗', description: '会员推广广告条,显示在首页底部', showDisplayMode: true },
{ type: 4, name: '订阅消息提醒条', description: '引导用户开启消息通知,显示在首页底部' }
]
//

View File

@ -9,6 +9,7 @@ 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'
@ -106,14 +107,26 @@ const handleEditContactCount = async () => {
}
}
//
const memberLevelOptions = [
{ value: 0, label: '非会员' },
{ value: 1, label: '不限时会员' },
{ value: 2, label: '诚意会员' },
{ value: 3, label: '家庭版会员' },
{ value: 4, label: '限时会员' }
]
// -
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 memberLevelDialogVisible = ref(false)
@ -257,6 +270,7 @@ const formatIncome = (min: number | undefined, max: number | undefined) => {
onMounted(() => {
fetchUserDetail()
loadMemberTierOptions()
})
</script>

View File

@ -11,6 +11,7 @@ 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 { getMemberTierList } from '@/api/memberTier'
import { getFullImageUrl } from '@/utils/image'
import type { UserListItem, UserQueryParams } from '@/types/user.d'
@ -54,15 +55,28 @@ const memberOptions = [
{ label: '否', value: false }
]
//
const memberLevelOptions = [
// -
const memberLevelOptions = ref([
{ label: '全部', value: '' },
{ label: '非会员', value: 0 },
{ label: '不限时', value: 1 },
{ label: '诚意', value: 2 },
{ label: '家庭版', value: 3 },
{ label: '限时', value: 4 }
]
{ label: '非会员', value: 0 }
])
//
const loadMemberTierOptions = async () => {
try {
const res = await getMemberTierList()
const tiers = res.data || res || []
if (Array.isArray(tiers) && tiers.length > 0) {
memberLevelOptions.value = [
{ label: '全部', value: '' },
{ label: '非会员', value: 0 },
...tiers.map((t: any) => ({ label: t.name, value: t.level }))
]
}
} catch (error) {
console.error('加载会员等级配置失败:', error)
}
}
//
const realNameOptions = [
@ -213,8 +227,8 @@ const handleOpenTestUserDialog = () => {
//
const handleCreateTestUsers = async () => {
if (testUserForm.count < 1 || testUserForm.count > 50) {
ElMessage.warning('创建数量必须在1-50之间')
if (testUserForm.count < 1 || testUserForm.count > 100) {
ElMessage.warning('创建数量必须在1-100之间')
return
}
@ -233,6 +247,7 @@ const handleCreateTestUsers = async () => {
onMounted(() => {
fetchUserList()
loadMemberTierOptions()
})
</script>
@ -559,9 +574,10 @@ onMounted(() => {
<el-input-number
v-model="testUserForm.count"
:min="1"
:max="50"
:max="100"
style="width: 100%"
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px;">单次最多创建100个</div>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="testUserForm.gender">

View File

@ -4,107 +4,137 @@
<view
class="voice-btn"
:class="{ recording: isRecording }"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
@touchstart.prevent="handleTouchStart"
@touchmove.prevent="handleTouchMove"
@touchend.prevent="handleTouchEnd"
@touchcancel.prevent="handleTouchCancel"
>
<text>{{ isRecording ? '松开发送' : '按住说话' }}</text>
<text>{{ btnText }}</text>
</view>
<!-- 录音提示弹窗 -->
<view class="voice-modal" v-if="isRecording">
<view class="voice-modal-content">
<view class="voice-modal" v-if="showModal">
<view class="voice-modal-content" :class="{ cancel: isCancelling }">
<view class="voice-icon">
<view class="voice-wave" :class="{ active: isRecording }"></view>
<text class="icon">🎤</text>
<view class="voice-wave" :class="{ active: isRecording && !isCancelling }"></view>
<text class="icon">{{ isCancelling ? '↩' : '🎤' }}</text>
</view>
<text class="voice-tip">{{ recordingTip }}</text>
<text class="voice-time">{{ recordingTime }}s</text>
<text class="voice-time" v-if="!isCancelling">{{ recordingTime }}s</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
const emit = defineEmits(['send'])
const isRecording = ref(false)
const showModal = ref(false)
const isCancelling = ref(false)
const recordingTime = ref(0)
const recordingTip = ref('正在录音...')
const recorderManager = ref(null)
const recordingTimer = ref(null)
const tempFilePath = ref('')
const startY = ref(0)
//
let cancelled = false
const btnText = computed(() => {
if (isRecording.value && isCancelling.value) return '松开取消'
if (isRecording.value) return '松开发送'
return '按住说话'
})
const recordingTip = computed(() => {
if (isCancelling.value) return '松开手指,取消发送'
return '正在录音...'
})
//
const initRecorder = () => {
if (recorderManager.value) return
recorderManager.value = uni.getRecorderManager()
//
recorderManager.value.onStart(() => {
console.log('[VoiceRecorder] 录音开始')
isRecording.value = true
showModal.value = true
recordingTime.value = 0
recordingTip.value = '正在录音...'
cancelled = false
//
recordingTimer.value = setInterval(() => {
recordingTime.value++
// 60
if (recordingTime.value >= 60) {
handleTouchEnd()
stopRecording()
}
}, 1000)
})
//
recorderManager.value.onStop((res) => {
console.log('[VoiceRecorder] 录音结束:', res)
clearInterval(recordingTimer.value)
console.log('[VoiceRecorder] 录音结束:', res, '已取消:', cancelled)
clearTimer()
isRecording.value = false
showModal.value = false
isCancelling.value = false
tempFilePath.value = res.tempFilePath
const duration = Math.ceil(res.duration / 1000)
// 1
if (duration < 1) {
uni.showToast({
title: '录音时间太短',
icon: 'none'
})
//
if (cancelled) {
cancelled = false
return
}
const duration = Math.ceil((res.duration || 0) / 1000)
if (duration < 1) {
uni.showToast({ title: '录音时间太短', icon: 'none' })
return
}
//
emit('send', {
tempFilePath: tempFilePath.value,
tempFilePath: res.tempFilePath,
duration: duration
})
})
//
recorderManager.value.onError((err) => {
console.error('[VoiceRecorder] 录音错误:', err)
clearInterval(recordingTimer.value)
clearTimer()
isRecording.value = false
showModal.value = false
isCancelling.value = false
cancelled = false
uni.showToast({
title: '录音失败',
icon: 'none'
})
uni.showToast({ title: '录音失败,请检查麦克风权限', icon: 'none' })
})
}
//
const handleTouchStart = () => {
if (!recorderManager.value) {
initRecorder()
const clearTimer = () => {
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
recordingTimer.value = null
}
}
const stopRecording = () => {
if (recorderManager.value) {
recorderManager.value.stop()
}
}
//
const handleTouchStart = (e) => {
startY.value = e.touches[0].clientY
isCancelling.value = false
cancelled = false
initRecorder()
//
recorderManager.value.start({
duration: 60000, // 60
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
@ -112,121 +142,117 @@ const handleTouchStart = () => {
})
}
//
// -
const handleTouchMove = (e) => {
if (!isRecording.value) return
const moveY = e.touches[0].clientY
const diff = startY.value - moveY
// 80px
isCancelling.value = diff > 80
}
// -
const handleTouchEnd = () => {
if (!isRecording.value) return
//
recorderManager.value.stop()
isRecording.value = false
if (isCancelling.value) {
//
cancelled = true
stopRecording()
} else {
//
stopRecording()
}
}
//
//
const handleTouchCancel = () => {
if (!isRecording.value) return
clearInterval(recordingTimer.value)
isRecording.value = false
recordingTip.value = '录音已取消'
//
recorderManager.value.stop()
cancelled = true
stopRecording()
}
onUnmounted(() => {
if (recordingTimer.value) {
clearInterval(recordingTimer.value)
}
clearTimer()
})
</script>
<style lang="scss" scoped>
.voice-recorder {
.voice-btn {
width: 100%;
height: 80rpx;
background-color: #fff;
border: 1rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #333;
user-select: none;
&.recording {
background-color: #ff6b6b;
border-color: #ff6b6b;
color: #fff;
}
&:active {
opacity: 0.8;
}
}
flex: 1;
display: flex;
align-items: center;
}
.voice-btn {
flex: 1;
height: 72rpx;
line-height: 72rpx;
text-align: center;
background: #f5f5f5;
border-radius: 36rpx;
font-size: 28rpx;
color: #333;
.voice-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
.voice-modal-content {
width: 300rpx;
height: 300rpx;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.voice-icon {
position: relative;
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
.voice-wave {
position: absolute;
width: 100%;
height: 100%;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
&.active {
animation: wave 1.5s ease-out infinite;
}
}
.icon {
font-size: 80rpx;
z-index: 1;
}
}
.voice-tip {
font-size: 28rpx;
color: #fff;
margin-bottom: 12rpx;
}
.voice-time {
font-size: 48rpx;
color: #fff;
font-weight: 600;
}
}
&.recording {
background: #e0e0e0;
color: #999;
}
}
.voice-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
}
.voice-modal-content {
width: 300rpx;
height: 300rpx;
background: rgba(0, 0, 0, 0.75);
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&.cancel {
background: rgba(200, 50, 50, 0.85);
}
}
.voice-icon {
position: relative;
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 60rpx;
z-index: 1;
}
}
.voice-wave {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 4rpx solid rgba(255, 255, 255, 0.3);
&.active {
animation: wave 1.2s ease-in-out infinite;
}
}
@ -240,4 +266,16 @@ onUnmounted(() => {
opacity: 0;
}
}
.voice-tip {
color: #fff;
font-size: 26rpx;
margin-top: 20rpx;
}
.voice-time {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
margin-top: 10rpx;
}
</style>

View File

@ -23,7 +23,7 @@ const ENV = {
}
// 当前环境 - 开发时使用 development打包时改为 production
const CURRENT_ENV = 'development'
const CURRENT_ENV = 'production'
// 导出配置
export const config = {

View File

@ -39,7 +39,7 @@
scroll-y
scroll-with-animation
:scroll-into-view="scrollToId"
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
:style="{ paddingTop: (statusBarHeight + 44) + 'px', paddingBottom: (140 + keyboardHeight) + 'px' }"
@scrolltoupper="loadMoreMessages"
>
<!-- 用户信息卡片 -->
@ -270,7 +270,7 @@
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-action-bar">
<view class="bottom-action-bar" :style="{ bottom: keyboardHeight + 'px' }">
<!-- 三个操作按钮 - 键盘弹起时隐藏 -->
<view class="action-buttons" v-show="!isInputFocused && inputMode === 'text'">
<button class="action-btn" @click="handleExchangeWeChat">
@ -297,8 +297,10 @@
type="text"
placeholder="请输入..."
placeholder-style="color: #999;"
:adjust-position="true"
:adjust-position="false"
:hold-keyboard="true"
confirm-type="send"
confirm-hold
@focus="handleInputFocus"
@blur="handleInputBlur"
@confirm="handleSendMessage"
@ -317,15 +319,15 @@
@send="handleSendVoice"
/>
<!-- 发送按钮 -->
<button
<!-- 发送按钮 - 使用view避免抢夺input焦点 -->
<view
v-if="inputMode === 'text'"
class="send-btn"
:class="{ active: inputText.trim() }"
@click="handleSendMessage"
>
发送
</button>
</view>
</view>
</view>
@ -388,6 +390,9 @@ const hasMore = ref(true)
const pageIndex = ref(1)
const isInputFocused = ref(false)
//
const keyboardHeight = ref(0)
// - /
const hasExchangedWeChat = ref(false)
const hasExchangedPhoto = ref(false)
@ -1045,10 +1050,7 @@ const handleInputFocus = () => {
}
const handleInputBlur = () => {
//
setTimeout(() => {
isInputFocused.value = false
}, 100)
isInputFocused.value = false
}
const shouldShowTime = (message, index) => {
@ -1100,6 +1102,14 @@ const handleMore = () => {
onMounted(async () => {
getSystemInfo()
//
uni.onKeyboardHeightChange((res) => {
keyboardHeight.value = res.height || 0
if (res.height > 0) {
nextTick(() => scrollToBottom())
}
})
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage?.options || {}
@ -1373,6 +1383,11 @@ const handleMessagesRead = (data) => {
onUnmounted(() => {
chatStore.clearCurrentSession()
//
// #ifdef MP-WEIXIN
uni.offKeyboardHeightChange()
// #endif
//
stopVoice()
if (innerAudioContext.value) {
@ -1455,7 +1470,6 @@ onUnmounted(() => {
.content-scroll {
flex: 1;
height: 0;
padding-bottom: 280rpx;
background-color: #f8f8f8;
}
@ -1877,6 +1891,8 @@ onUnmounted(() => {
right: 0;
background: #f5f6fa;
padding-bottom: env(safe-area-inset-bottom);
transition: bottom 0.15s ease-out;
z-index: 100;
.action-buttons {
display: flex;
@ -2004,9 +2020,10 @@ onUnmounted(() => {
color: #fff;
border: none;
flex-shrink: 0;
text-align: center;
&::after {
border: none;
&:active {
opacity: 0.8;
}
&.active {

View File

@ -66,12 +66,8 @@
<!-- 小程序订阅消息提醒条 - 固定在底部会员广告关闭后显示 -->
<view class="subscribe-reminder-section" v-if="showSubscribeReminder && !showMemberAd">
<view class="subscribe-reminder-bar" :style="subscribeReminderBgStyle">
<view class="reminder-content">
<text class="reminder-text">开启消息通知不错过任何心动对象</text>
</view>
<view class="reminder-btn" @click="handleOpenSubscribe">打开</view>
<view class="reminder-close" @click.stop="handleCloseSubscribeReminder">
<view class="member-ad-bar" :style="subscribeReminderBgStyle" @click="handleOpenSubscribe">
<view class="ad-close" @click.stop="handleCloseSubscribeReminder">
<text>×</text>
</view>
</view>
@ -1304,53 +1300,5 @@ export default {
right: 0;
bottom: 0;
z-index: 98;
.subscribe-reminder-bar {
position: relative;
display: flex;
align-items: center;
min-height: 100rpx;
padding: 0 24rpx;
.reminder-content {
flex: 1;
display: flex;
align-items: center;
.reminder-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
}
.reminder-btn {
background: #fff;
color: #667eea;
font-size: 24rpx;
padding: 12rpx 32rpx;
border-radius: 30rpx;
margin-right: 50rpx;
font-weight: 500;
}
.reminder-close {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
line-height: 1;
}
}
}
}
</style>

View File

@ -86,9 +86,9 @@ public class AdminUserController : ControllerBase
[OperationLog("用户管理", "新增", Description = "创建测试用户")]
public async Task<ApiResponse<List<long>>> CreateTestUsers([FromBody] CreateTestUsersRequest request)
{
if (request.Count <= 0 || request.Count > 50)
if (request.Count <= 0 || request.Count > 100)
{
return ApiResponse<List<long>>.Error(40001, "创建数量必须在1-50之间");
return ApiResponse<List<long>>.Error(40001, "创建数量必须在1-100之间");
}
var result = await _adminUserService.CreateTestUsersAsync(request.Count, request.Gender);

View File

@ -90,7 +90,7 @@ public class OrderController : ControllerBase
return ApiResponse<OrderCreateResponse>.Error(ErrorCodes.InvalidParameter, "订单类型无效");
}
if (request.OrderType == 1 && (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 3))
if (request.OrderType == 1 && (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 4))
{
return ApiResponse<OrderCreateResponse>.Error(ErrorCodes.InvalidParameter, "会员等级无效");
}

View File

@ -49,7 +49,7 @@ public class PayController : ControllerBase
[HttpPost("createOrder")]
public async Task<ApiResponse<CreateOrderResponse>> CreateOrder([FromBody] CreateMemberOrderRequest request)
{
if (request.MemberLevel < 1 || request.MemberLevel > 3)
if (request.MemberLevel < 1 || request.MemberLevel > 4)
{
return ApiResponse<CreateOrderResponse>.Error(ErrorCodes.InvalidParameter, "无效的会员等级");
}

View File

@ -19,6 +19,7 @@ public class AdminUserService : IAdminUserService
private readonly IRepository<UserPhoto> _photoRepository;
private readonly IRepository<UserRequirement> _requirementRepository;
private readonly IRepository<Order> _orderRepository;
private readonly ISystemConfigService _systemConfigService;
private readonly ILogger<AdminUserService> _logger;
public AdminUserService(
@ -27,6 +28,7 @@ public class AdminUserService : IAdminUserService
IRepository<UserPhoto> photoRepository,
IRepository<UserRequirement> requirementRepository,
IRepository<Order> orderRepository,
ISystemConfigService systemConfigService,
ILogger<AdminUserService> logger)
{
_userRepository = userRepository;
@ -34,6 +36,7 @@ public class AdminUserService : IAdminUserService
_photoRepository = photoRepository;
_requirementRepository = requirementRepository;
_orderRepository = orderRepository;
_systemConfigService = systemConfigService;
_logger = logger;
}
@ -342,11 +345,8 @@ public class AdminUserService : IAdminUserService
var parentHousingStatuses = new[] { "自有住房", "自有住房(已还清贷款)", "租房", "与子女同住" };
var pensionStatuses = new[] { "城镇职工养老保险", "城乡居民养老保险", "无养老保险" };
var medicalStatuses = new[] { "城镇职工医保", "城乡居民医保", "商业医疗保险" };
// 默认头像列表
var defaultAvatars = new[] {
"/uploads/avatars/default_male.png",
"/uploads/avatars/default_female.png"
};
// 从系统配置获取默认头像
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync() ?? "/uploads/avatars/default.png";
for (int i = 0; i < count; i++)
{
@ -381,7 +381,7 @@ public class AdminUserService : IAdminUserService
{
OpenId = $"test_openid_{Guid.NewGuid():N}",
Nickname = nickname,
Avatar = defaultAvatars[userGender == 1 ? 0 : 1],
Avatar = defaultAvatar,
Phone = phone,
XiangQinNo = xiangQinNo,
Gender = userGender,

View File

@ -85,7 +85,7 @@ public class OrderService : IOrderService
if (request.OrderType == (int)OrderType.Membership)
{
// 会员订单 - 从数据库读取价格配置
if (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 3)
if (!request.MemberLevel.HasValue || request.MemberLevel < 1 || request.MemberLevel > 4)
{
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的会员等级");
}

View File

@ -85,6 +85,7 @@ public class PaymentService : IPaymentService
Amount = tierConfig.Price,
PayAmount = tierConfig.Price,
Status = 1, // 待支付
Remark = $"会员等级:{request.MemberLevel}",
ExpireTime = DateTime.Now.AddMinutes(30), // 30分钟过期
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
@ -167,10 +168,31 @@ public class PaymentService : IPaymentService
/// </summary>
private async Task ActivateMembershipAsync(Order order)
{
// 获取会员等级配置
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Name == order.ProductName);
var tierConfig = tierConfigs.FirstOrDefault();
var memberLevel = tierConfig?.Level ?? 1;
// 从订单备注解析会员等级(格式:"会员等级:3"
int? parsedLevel = null;
if (!string.IsNullOrEmpty(order.Remark) && order.Remark.StartsWith("会员等级:"))
{
var levelStr = order.Remark.Replace("会员等级:", "");
if (int.TryParse(levelStr, out var lv))
{
parsedLevel = lv;
}
}
// 优先用解析出的等级查配置fallback 用商品名称匹配
MemberTierConfig? tierConfig = null;
if (parsedLevel.HasValue)
{
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Level == parsedLevel.Value);
tierConfig = tierConfigs.FirstOrDefault();
}
if (tierConfig == null)
{
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Name == order.ProductName);
tierConfig = tierConfigs.FirstOrDefault();
}
var memberLevel = tierConfig?.Level ?? parsedLevel ?? 1;
// 检查是否已有会员记录
var existingMembers = await _memberRepository.GetListAsync(m => m.UserId == order.UserId && m.Status == 1);

View File

@ -278,8 +278,11 @@ public class RecommendService : IRecommendService
// 确定推荐数量
var recommendCount = GetRecommendCountByMemberLevel(user.MemberLevel);
// 多获取一些候选用户,弥补查询时可能被过滤掉的(用户被禁用、资料未审核等)
var candidateCount = (int)Math.Ceiling(recommendCount * 1.5);
// 获取候选用户
var candidates = await GetCandidateUsersAsync(userId, userProfile, userRequirement, recommendCount);
var candidates = await GetCandidateUsersAsync(userId, userProfile, userRequirement, candidateCount);
// 删除今日已有的推荐
var today = DateTime.Today;
@ -349,6 +352,7 @@ public class RecommendService : IRecommendService
1 => UnlimitedMemberRecommendCount, // 不限时会员24人
2 => _random.Next(SincereMemberMinRecommendCount, SincereMemberMaxRecommendCount + 1), // 诚意会员24-29人
3 => FamilyMemberRecommendCount, // 家庭版会员24人
4 => UnlimitedMemberRecommendCount, // 限时会员24人
_ => NormalUserRecommendCount
};
}
@ -364,6 +368,7 @@ public class RecommendService : IRecommendService
1 => (UnlimitedMemberRecommendCount, UnlimitedMemberRecommendCount),
2 => (SincereMemberMinRecommendCount, SincereMemberMaxRecommendCount),
3 => (FamilyMemberRecommendCount, FamilyMemberRecommendCount),
4 => (UnlimitedMemberRecommendCount, UnlimitedMemberRecommendCount),
_ => (NormalUserRecommendCount, NormalUserRecommendCount)
};
}