逻辑优化

This commit is contained in:
18631081161 2026-02-06 17:58:15 +08:00
parent 2f18801615
commit 23a620d69d
26 changed files with 673 additions and 52 deletions

View File

@ -11,6 +11,7 @@ export interface MemberTier {
benefitsImage?: string
sort: number
status: number
durationMonths: number
createTime: string
updateTime: string
}
@ -25,6 +26,7 @@ export interface CreateMemberTierRequest {
benefitsImage?: string
sort: number
status: number
durationMonths: number
}
export interface UpdateMemberTierRequest extends CreateMemberTierRequest {}

View File

@ -41,6 +41,9 @@
<span class="original-price">¥{{ tier.originalPrice }}</span>
</div>
<div class="tier-discount" v-if="tier.discount">{{ tier.discount }}</div>
<div class="tier-duration">
{{ tier.durationMonths === 0 ? '永久' : tier.durationMonths + '个月' }}
</div>
</div>
<!-- 权益图预览 -->
@ -103,6 +106,7 @@
<el-option :value="1" label="等级1 - 永久会员" />
<el-option :value="2" label="等级2 - 诚意会员" />
<el-option :value="3" label="等级3 - 家庭版会员" />
<el-option :value="4" label="等级4 - 限时会员" />
</el-select>
</el-form-item>
</el-col>
@ -139,6 +143,15 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="生效时长" prop="durationMonths">
<el-input-number v-model="formData.durationMonths" :min="0" :max="999" :controls="false" style="width: 100%" placeholder="0表示永久" />
<div style="font-size: 12px; color: #909399; margin-top: 4px;">填0表示永久其他数字为月数</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="权益图片" prop="benefitsImage">
<div class="benefits-upload-wrapper">
<el-upload
@ -232,7 +245,8 @@ const formData = reactive({
discount: '',
benefitsImage: '',
sort: 0,
status: 1
status: 1,
durationMonths: 0
})
const formRules: FormRules = {
@ -275,6 +289,7 @@ const resetForm = () => {
formData.benefitsImage = ''
formData.sort = 0
formData.status = 1
formData.durationMonths = 0
editingId.value = null
}
@ -296,6 +311,7 @@ const handleEdit = (row: MemberTier) => {
formData.benefitsImage = row.benefitsImage || ''
formData.sort = row.sort
formData.status = row.status
formData.durationMonths = row.durationMonths || 0
dialogVisible.value = true
}
@ -356,7 +372,8 @@ const handleSubmit = async () => {
discount: formData.discount || undefined,
benefitsImage: formData.benefitsImage || undefined,
sort: formData.sort,
status: formData.status
status: formData.status,
durationMonths: formData.durationMonths
}
if (isEdit.value && editingId.value) {
@ -506,6 +523,16 @@ onMounted(() => {
display: inline-block;
}
.tier-duration {
font-size: 12px;
color: #409eff;
background: #ecf5ff;
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
margin-top: 8px;
}
/* 权益图预览 */
.tier-image {
height: 120px;

View File

@ -45,7 +45,8 @@ const memberLevelOptions = [
{ label: '全部', value: '' },
{ label: '不限时', value: 1 },
{ label: '诚意', value: 2 },
{ label: '家庭版', value: 3 }
{ label: '家庭版', value: 3 },
{ label: '限时', value: 4 }
]
//
@ -121,7 +122,8 @@ const getMemberLevelType = (level: number): 'success' | 'warning' | 'danger' | '
const types: Record<number, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
1: 'success',
2: 'warning',
3: 'danger'
3: 'danger',
4: 'primary'
}
return types[level] || 'info'
}

View File

@ -111,7 +111,8 @@ const memberLevelOptions = [
{ value: 0, label: '非会员' },
{ value: 1, label: '不限时会员' },
{ value: 2, label: '诚意会员' },
{ value: 3, label: '家庭版会员' }
{ value: 3, label: '家庭版会员' },
{ value: 4, label: '限时会员' }
]
//

View File

@ -59,7 +59,8 @@ const memberLevelOptions = [
{ label: '非会员', value: 0 },
{ label: '不限时', value: 1 },
{ label: '诚意', value: 2 },
{ label: '家庭版', value: 3 }
{ label: '家庭版', value: 3 },
{ label: '限时', value: 4 }
]
//

View File

@ -28,6 +28,9 @@
return
}
// badge
chatStore.fetchAllUnreadCounts()
try {
// SignalR
await signalR.connect()
@ -110,7 +113,7 @@
return
}
//
// badge
if (message.sessionId) {
chatStore.incrementUnreadCount(message.sessionId)
}

View File

@ -41,6 +41,17 @@ export async function bindFamily(bindUserId) {
return response
}
/**
* 通过相亲编号绑定家庭成员
*
* @param {string} xiangQinNo - 相亲编号
* @returns {Promise<Object>} 绑定结果
*/
export async function bindFamilyByXiangQinNo(xiangQinNo) {
const response = await post('/member/bindFamilyByXiangQinNo', { xiangQinNo })
return response
}
/**
* 获取家庭成员列表
*
@ -66,6 +77,7 @@ export default {
getMemberInfo,
purchase,
bindFamily,
bindFamilyByXiangQinNo,
getFamilyMembers,
unbindFamilyMember
}

View File

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

View File

@ -67,6 +67,9 @@
<view class="system-info">
<text class="system-title">系统消息</text>
</view>
<view class="system-badge" v-if="systemUnreadCount > 0">
<text class="badge-text">{{ systemUnreadCount > 99 ? '99+' : systemUnreadCount }}</text>
</view>
<view class="system-arrow">
<text class="arrow-icon"></text>
</view>
@ -161,6 +164,9 @@
getInteractCounts,
markInteractAsRead
} from '@/api/interact.js'
import {
getUnreadCount as getSystemUnreadCount
} from '@/api/message.js'
import {
formatTimestamp
} from '@/utils/format.js'
@ -181,6 +187,7 @@
//
const sessions = ref([])
const systemUnreadCount = ref(0)
const interactCounts = ref({
viewedMe: 0,
favoritedMe: 0,
@ -247,7 +254,10 @@
if (!userStore.isLoggedIn) return
try {
const res = await getSessions()
const [res, sysRes] = await Promise.all([
getSessions(),
getSystemUnreadCount()
])
console.log('[Message] 加载会话列表:', res)
if (res?.success || res?.code === 0) {
sessions.value = res.data || []
@ -258,6 +268,11 @@
})))
chatStore.setSessions(sessions.value)
}
//
if (sysRes?.code === 0) {
systemUnreadCount.value = sysRes.data?.unreadCount || 0
chatStore.setSystemUnreadCount(systemUnreadCount.value)
}
} catch (error) {
console.error('加载会话列表失败:', error)
uni.showToast({
@ -645,6 +660,24 @@
}
}
.system-badge {
min-width: 36rpx;
height: 36rpx;
background: #FF3B30;
border-radius: 18rpx;
padding: 0 10rpx;
margin-right: 12rpx;
display: flex;
align-items: center;
justify-content: center;
.badge-text {
font-size: 20rpx;
color: #fff;
line-height: 1;
}
}
.system-arrow {
.arrow-icon {
font-size: 32rpx;

View File

@ -70,7 +70,8 @@
<script>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { getSystemMessages } from '@/api/message.js'
import { useChatStore } from '@/store/chat.js'
import { getSystemMessages, markAllSystemMessagesRead } from '@/api/message.js'
import { formatTimestamp } from '@/utils/format.js'
import Loading from '@/components/Loading/index.vue'
import Empty from '@/components/Empty/index.vue'
@ -83,6 +84,7 @@ export default {
},
setup() {
const userStore = useUserStore()
const chatStore = useChatStore()
//
const statusBarHeight = ref(20)
@ -167,6 +169,13 @@ export default {
getSystemInfo()
userStore.restoreFromStorage()
await loadMessages()
//
try {
await markAllSystemMessagesRead()
chatStore.setSystemUnreadCount(0)
} catch (e) {
console.error('标记系统消息已读失败:', e)
}
} catch (error) {
console.error('初始化页面失败:', error)
} finally {

View File

@ -8,6 +8,49 @@
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 绑定家庭成员弹窗 -->
<view class="family-popup-mask" v-if="showFamilyPopup" @click="closeFamilyPopup">
<view class="family-popup" @click.stop>
<view class="popup-title">绑定家庭成员</view>
<view class="popup-input-wrapper">
<input
v-model="bindXiangQinNo"
class="popup-input"
type="text"
placeholder="请输入相亲编号"
maxlength="20"
/>
</view>
<button class="popup-btn" :class="{ 'has-content': bindXiangQinNo.trim() }" @click="handleBindFamily" :disabled="bindLoading">
{{ bindLoading ? '绑定中...' : '确定' }}
</button>
<view class="popup-divider"></view>
<view class="popup-members-title">已绑定的用户{{ familyMembers.length }}/3</view>
<view class="popup-members-list" v-if="familyMembers.length > 0">
<view class="member-item" v-for="member in familyMembers" :key="member.bindId">
<image class="member-avatar" :src="member.avatar || defaultAvatar" mode="aspectFill" />
<view class="member-info">
<text class="member-nickname">{{ member.nickname || '用户' }}</text>
<text class="member-no">{{ member.xiangQinNo }}</text>
</view>
<text class="member-unbind" @click="handleUnbindFamily(member)">解绑</text>
</view>
</view>
<view class="popup-empty" v-else>
<text>暂未绑定</text>
</view>
<view class="popup-close" @click="closeFamilyPopup">
<text></text>
</view>
</view>
</view>
<!-- 未登录状态 -->
<view class="login-card" v-if="!isLoggedIn && !pageLoading" :style="{ marginTop: (statusBarHeight + 35) + 'px' }">
<text class="login-tip">登陆后帮您更精确准备</text>
@ -120,6 +163,12 @@
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="handleFamilyBind" v-if="isLoggedIn && canBindFamily">
<image src="/static/ic_unlock.png" mode="aspectFit" class="menu-icon" />
<text class="menu-title">绑定家庭成员</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="handleUserAgreement">
<image src="/static/ic_agreement1.png" mode="aspectFit" class="menu-icon" />
<text class="menu-title">用户协议</text>
@ -152,6 +201,7 @@ import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import { getInteractCounts, markInteractAsRead } from '@/api/interact.js'
import { bindFamilyByXiangQinNo, getFamilyMembers, unbindFamilyMember, getMemberInfo } from '@/api/member.js'
import { getFullImageUrl } from '@/utils/image.js'
import Loading from '@/components/Loading/index.vue'
@ -178,6 +228,13 @@ export default {
unlockedMe: 0
})
//
const showFamilyPopup = ref(false)
const bindXiangQinNo = ref('')
const bindLoading = ref(false)
const familyMembers = ref([])
const canBindFamily = ref(false) //
//
const getSystemInfo = () => {
uni.getSystemInfo({
@ -233,6 +290,41 @@ export default {
}
}
/**
* 加载家庭成员列表
*/
const loadFamilyMembers = async () => {
if (!userStore.isLoggedIn || userStore.memberLevel !== 3) return
try {
const res = await getFamilyMembers()
if (res?.code === 0 && res.data) {
familyMembers.value = res.data.map(item => ({
...item,
avatar: getFullImageUrl(item.avatar)
}))
}
} catch (error) {
console.error('加载家庭成员失败:', error)
}
}
/**
* 加载会员信息检查是否可以绑定家庭成员
*/
const loadMemberInfo = async () => {
if (!userStore.isLoggedIn) return
try {
const res = await getMemberInfo()
if (res?.code === 0 && res.data) {
canBindFamily.value = res.data.canBindFamily || false
}
} catch (error) {
console.error('加载会员信息失败:', error)
}
}
//
const initPage = async () => {
pageLoading.value = true
@ -240,6 +332,8 @@ export default {
userStore.restoreFromStorage()
if (userStore.isLoggedIn) {
await loadInteractCounts()
await loadMemberInfo()
await loadFamilyMembers()
}
} catch (error) {
console.error('初始化页面失败:', error)
@ -278,6 +372,72 @@ export default {
uni.navigateTo({ url: '/pages/realname/index' })
}
// -
const handleFamilyBind = () => {
showFamilyPopup.value = true
loadFamilyMembers()
}
//
const closeFamilyPopup = () => {
showFamilyPopup.value = false
bindXiangQinNo.value = ''
}
//
const handleBindFamily = async () => {
if (!bindXiangQinNo.value.trim()) {
uni.showToast({ title: '请输入相亲编号', icon: 'none' })
return
}
if (familyMembers.value.length >= 3) {
uni.showToast({ title: '最多绑定3位家庭成员', icon: 'none' })
return
}
bindLoading.value = true
try {
const res = await bindFamilyByXiangQinNo(bindXiangQinNo.value.trim())
if (res?.code === 0) {
uni.showToast({ title: '绑定成功', icon: 'success' })
bindXiangQinNo.value = ''
await loadFamilyMembers()
} else {
uni.showToast({ title: res?.message || '绑定失败', icon: 'none' })
}
} catch (error) {
console.error('绑定家庭成员失败:', error)
uni.showToast({ title: '绑定失败', icon: 'none' })
} finally {
bindLoading.value = false
}
}
//
const handleUnbindFamily = (member) => {
uni.showModal({
title: '提示',
content: `确定要解绑 ${member.nickname || '该用户'} 吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await unbindFamilyMember(member.userId)
if (result?.code === 0) {
uni.showToast({ title: '解绑成功', icon: 'success' })
await loadFamilyMembers()
} else {
uni.showToast({ title: result?.message || '解绑失败', icon: 'none' })
}
} catch (error) {
console.error('解绑失败:', error)
uni.showToast({ title: '解绑失败', icon: 'none' })
}
}
}
})
}
//
const handleUserAgreement = () => {
uni.navigateTo({ url: '/pages/agreement/index?type=user' })
@ -355,6 +515,17 @@ export default {
memberEntryImageUrl,
avatarLoadError,
onAvatarError,
//
showFamilyPopup,
bindXiangQinNo,
bindLoading,
familyMembers,
canBindFamily,
handleFamilyBind,
closeFamilyPopup,
handleBindFamily,
handleUnbindFamily,
//
handleLogin,
handlePersonalProfile,
handleEditProfile,
@ -791,4 +962,157 @@ export default {
color: #999;
}
}
//
.family-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.family-popup {
width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
position: relative;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 40rpx;
}
.popup-input-wrapper {
margin-bottom: 32rpx;
.popup-input {
width: 100%;
height: 88rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
text-align: center;
}
}
.popup-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #A4ADB3;
border-radius: 12rpx;
font-size: 32rpx;
color: #fff;
border: none;
margin-bottom: 32rpx;
&::after {
border: none;
}
&.has-content {
background: #1F97E7;
}
&[disabled] {
background: #ccc;
}
}
.popup-divider {
height: 1rpx;
background: #eee;
margin-bottom: 32rpx;
}
.popup-members-title {
font-size: 28rpx;
color: #333;
text-align: center;
margin-bottom: 24rpx;
}
.popup-members-list {
.member-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.member-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.member-info {
flex: 1;
.member-nickname {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 4rpx;
}
.member-no {
font-size: 24rpx;
color: #999;
}
}
.member-unbind {
font-size: 26rpx;
color: #ff6b6b;
padding: 8rpx 16rpx;
}
}
}
.popup-empty {
text-align: center;
padding: 40rpx 0;
text {
font-size: 28rpx;
color: #999;
}
}
.popup-close {
position: absolute;
bottom: -100rpx;
left: 50%;
transform: translateX(-50%);
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 64rpx;
color: #fff;
line-height: 1;
}
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -4,6 +4,8 @@
*/
import { defineStore } from 'pinia'
import { getUnreadCount as getChatUnreadCount } from '../api/chat.js'
import { getUnreadCount as getSystemUnreadCount } from '../api/message.js'
/**
* 消息类型枚举
@ -68,7 +70,10 @@ export const useChatStore = defineStore('chat', {
currentSessionId: null,
// 总未读消息数
totalUnreadCount: 0
totalUnreadCount: 0,
// 系统消息未读数
systemUnreadCount: 0
}),
getters: {
@ -86,6 +91,11 @@ export const useChatStore = defineStore('chat', {
*/
hasUnreadMessages: (state) => state.totalUnreadCount > 0,
/**
* 消息tab总未读数聊天 + 系统消息
*/
messageBadgeCount: (state) => state.totalUnreadCount + state.systemUnreadCount,
/**
* 获取指定会话的未读数
*/
@ -123,6 +133,51 @@ export const useChatStore = defineStore('chat', {
(total, session) => total + (session.unreadCount || 0),
0
)
this.updateTabBarBadge()
},
/**
* 设置系统消息未读数
*/
setSystemUnreadCount(count) {
this.systemUnreadCount = count
this.updateTabBarBadge()
},
/**
* 更新底部导航栏消息tab的badge
*/
updateTabBarBadge() {
const total = this.messageBadgeCount
if (total > 0) {
uni.setTabBarBadge({
index: 1,
text: total > 99 ? '99+' : String(total)
})
} else {
uni.removeTabBarBadge({ index: 1 })
}
},
/**
* 从服务器获取所有未读数并更新badge
*/
async fetchAllUnreadCounts() {
try {
const [chatRes, sysRes] = await Promise.all([
getChatUnreadCount(),
getSystemUnreadCount()
])
if (chatRes?.code === 0) {
this.totalUnreadCount = chatRes.data || 0
}
if (sysRes?.code === 0) {
this.systemUnreadCount = sysRes.data?.unreadCount || 0
}
this.updateTabBarBadge()
} catch (e) {
console.error('获取未读数失败:', e)
}
},
/**
@ -286,6 +341,8 @@ export const useChatStore = defineStore('chat', {
this.messagesBySession = {}
this.currentSessionId = null
this.totalUnreadCount = 0
this.systemUnreadCount = 0
uni.removeTabBarBadge({ index: 1 })
}
}
})

View File

@ -44,6 +44,7 @@ public class AdminMemberTierController : ControllerBase
BenefitsImage = t.BenefitsImage,
Sort = t.Sort,
Status = t.Status,
DurationMonths = t.DurationMonths,
CreateTime = t.CreateTime,
UpdateTime = t.UpdateTime
}).ToList();
@ -75,6 +76,7 @@ public class AdminMemberTierController : ControllerBase
BenefitsImage = tier.BenefitsImage,
Sort = tier.Sort,
Status = tier.Status,
DurationMonths = tier.DurationMonths,
CreateTime = tier.CreateTime,
UpdateTime = tier.UpdateTime
});
@ -104,6 +106,7 @@ public class AdminMemberTierController : ControllerBase
BenefitsImage = request.BenefitsImage,
Sort = request.Sort,
Status = request.Status,
DurationMonths = request.DurationMonths,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
@ -123,6 +126,7 @@ public class AdminMemberTierController : ControllerBase
BenefitsImage = tier.BenefitsImage,
Sort = tier.Sort,
Status = tier.Status,
DurationMonths = tier.DurationMonths,
CreateTime = tier.CreateTime,
UpdateTime = tier.UpdateTime
});
@ -159,6 +163,7 @@ public class AdminMemberTierController : ControllerBase
tier.BenefitsImage = request.BenefitsImage;
tier.Sort = request.Sort;
tier.Status = request.Status;
tier.DurationMonths = request.DurationMonths;
tier.UpdateTime = DateTime.Now;
await _tierRepository.UpdateAsync(tier);
@ -201,6 +206,7 @@ public class MemberTierDto
public string? BenefitsImage { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public int DurationMonths { get; set; }
public DateTime? CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
}
@ -219,6 +225,7 @@ public class CreateMemberTierRequest
public string? BenefitsImage { get; set; }
public int Sort { get; set; }
public int Status { get; set; } = 1;
public int DurationMonths { get; set; } = 0;
}
/// <summary>
@ -235,4 +242,5 @@ public class UpdateMemberTierRequest
public string? BenefitsImage { get; set; }
public int Sort { get; set; }
public int Status { get; set; }
public int DurationMonths { get; set; } = 0;
}

View File

@ -79,6 +79,24 @@ public class MemberController : ControllerBase
return ApiResponse<FamilyBindResponse>.Success(result);
}
/// <summary>
/// 通过相亲编号绑定家庭成员
/// </summary>
/// <param name="request">绑定请求(包含相亲编号)</param>
/// <returns>绑定结果</returns>
[HttpPost("bindFamilyByXiangQinNo")]
public async Task<ApiResponse<FamilyBindResponse>> BindFamilyByXiangQinNo([FromBody] FamilyBindByXiangQinNoRequest request)
{
if (string.IsNullOrWhiteSpace(request.XiangQinNo))
{
return ApiResponse<FamilyBindResponse>.Error(ErrorCodes.InvalidParameter, "请输入相亲编号");
}
var userId = GetCurrentUserId();
var result = await _memberService.BindFamilyMemberByXiangQinNoAsync(userId, request);
return ApiResponse<FamilyBindResponse>.Success(result);
}
/// <summary>
/// 获取家庭成员列表
/// </summary>

View File

@ -21,3 +21,14 @@ public class FamilyBindRequest
/// </summary>
public long BindUserId { get; set; }
}
/// <summary>
/// 通过相亲编号绑定家庭成员请求
/// </summary>
public class FamilyBindByXiangQinNoRequest
{
/// <summary>
/// 相亲编号
/// </summary>
public string XiangQinNo { get; set; } = string.Empty;
}

View File

@ -31,6 +31,14 @@ public interface IMemberService
/// <returns>绑定结果</returns>
Task<FamilyBindResponse> BindFamilyMemberAsync(long userId, FamilyBindRequest request);
/// <summary>
/// 通过相亲编号绑定家庭成员
/// </summary>
/// <param name="userId">主账号用户ID</param>
/// <param name="request">绑定请求(包含相亲编号)</param>
/// <returns>绑定结果</returns>
Task<FamilyBindResponse> BindFamilyMemberByXiangQinNoAsync(long userId, FamilyBindByXiangQinNoRequest request);
/// <summary>
/// 获取家庭成员列表
/// </summary>
@ -75,5 +83,5 @@ public interface IMemberService
/// <summary>
/// 家庭版最大绑定数量(不含主账号)
/// </summary>
const int MaxFamilyBindCount = 2;
const int MaxFamilyBindCount = 3;
}

View File

@ -27,7 +27,8 @@ public class AdminMemberService : IAdminMemberService
{
{ 1, "不限时会员" },
{ 2, "诚意会员" },
{ 3, "家庭版会员" }
{ 3, "家庭版会员" },
{ 4, "限时会员" }
};
/// <summary>
@ -122,14 +123,16 @@ public class AdminMemberService : IAdminMemberService
var familyBinds = await _memberFamilyRepository.GetListAsync(f => memberIds.Contains(f.MemberId));
var familyBindCountDict = familyBinds.GroupBy(f => f.MemberId).ToDictionary(g => g.Key, g => g.Count());
// Map to DTOs
var dtos = items.Select(m =>
{
userDict.TryGetValue(m.UserId, out var user);
orderDict.TryGetValue(m.OrderId, out var order);
familyBindCountDict.TryGetValue(m.Id, out var familyBindCount);
return MapToMemberListDto(m, user, order, familyBindCount);
}).ToList();
// Map to DTOs - 过滤掉没有关联用户的会员记录
var dtos = items
.Where(m => userDict.ContainsKey(m.UserId)) // 只保留有关联用户的会员
.Select(m =>
{
userDict.TryGetValue(m.UserId, out var user);
orderDict.TryGetValue(m.OrderId, out var order);
familyBindCountDict.TryGetValue(m.Id, out var familyBindCount);
return MapToMemberListDto(m, user, order, familyBindCount);
}).ToList();
return new PagedResult<AdminMemberListDto>
{

View File

@ -315,13 +315,24 @@ public class AdminUserService : IAdminUserService
var femaleNames = new[] { "林", "何", "郭", "马", "罗", "梁", "宋", "郑", "谢", "韩", "唐", "冯", "于", "董", "萧" };
var cities = new[] { "北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "南京", "苏州", "西安" };
var provinces = new[] { "北京", "上海", "广东", "广东", "浙江", "四川", "湖北", "江苏", "江苏", "陕西" };
var districts = new[] { "朝阳区", "浦东新区", "天河区", "南山区", "西湖区", "锦江区", "武昌区", "鼓楼区", "姑苏区", "雁塔区" };
var occupations = new[] { "程序员", "设计师", "产品经理", "教师", "医生", "律师", "会计", "销售", "工程师", "公务员" };
var introductions = new[] {
"性格开朗,喜欢运动和旅行",
"工作稳定,希望找到志同道合的另一半",
"热爱生活,喜欢美食和电影",
"踏实上进,有责任心",
"温柔体贴,顾家爱家"
"性格开朗,喜欢运动和旅行,希望找到一个志同道合的伴侣",
"工作稳定,希望找到志同道合的另一半,一起经营美好生活",
"热爱生活,喜欢美食和电影,周末喜欢去探店",
"踏实上进,有责任心,对未来有清晰的规划",
"温柔体贴,顾家爱家,喜欢做饭和养花"
};
var parentStatuses = new[] { "父母健在", "父母健在,身体健康", "父亲已故,母亲健在", "父母均已退休" };
var parentRetireStatuses = new[] { "均已退休", "父亲退休,母亲在职", "均在职", "父亲在职,母亲退休" };
var parentHousingStatuses = new[] { "自有住房", "自有住房(已还清贷款)", "租房", "与子女同住" };
var pensionStatuses = new[] { "城镇职工养老保险", "城乡居民养老保险", "无养老保险" };
var medicalStatuses = new[] { "城镇职工医保", "城乡居民医保", "商业医疗保险" };
// 默认头像列表
var defaultAvatars = new[] {
"/uploads/avatars/default_male.png",
"/uploads/avatars/default_female.png"
};
for (int i = 0; i < count; i++)
@ -336,12 +347,29 @@ public class AdminUserService : IAdminUserService
// 生成相亲编号
var xiangQinNo = $"{random.Next(100000, 999999)}";
// 确定关系1父亲 2母亲 3本人
var relationship = random.Next(1, 4);
// 根据关系生成昵称
var nickname = relationship switch
{
1 => $"{surname}家长(父亲)",
2 => $"{surname}家长(母亲)",
_ => userGender == 1 ? $"{surname}先生" : $"{surname}女士"
};
// 生成手机号
var phonePrefixes = new[] { "138", "139", "150", "151", "152", "158", "159", "186", "187", "188" };
var phone = $"{phonePrefixes[random.Next(phonePrefixes.Length)]}{random.Next(10000000, 99999999)}";
// 生成用户
var cityIndex = random.Next(cities.Length);
var user = new User
{
OpenId = $"test_openid_{Guid.NewGuid():N}",
Nickname = $"{surname}先生" + (userGender == 1 ? "" : "女士").Replace("先生女士", "女士"),
Nickname = nickname,
Avatar = defaultAvatars[userGender == 1 ? 0 : 1],
Phone = phone,
XiangQinNo = xiangQinNo,
Gender = userGender,
City = cities[cityIndex],
@ -356,19 +384,20 @@ public class AdminUserService : IAdminUserService
LastLoginTime = DateTime.Now.AddHours(-random.Next(72))
};
// 如果是会员,设置会员等级
// 如果是会员,设置会员等级包含新增的限时会员等级4
if (user.IsMember)
{
user.MemberLevel = random.Next(1, 4);
if (user.MemberLevel != 1) // 非不限时会员
user.MemberLevel = random.Next(1, 5); // 1-4
if (user.MemberLevel == 1) // 不限时会员
{
user.MemberExpireTime = null;
}
else
{
user.MemberExpireTime = DateTime.Now.AddMonths(random.Next(1, 12));
}
}
// 修正昵称
user.Nickname = userGender == 1 ? $"{surname}先生" : $"{surname}女士";
await _userRepository.AddAsync(user);
// 生成用户资料
@ -376,13 +405,14 @@ public class AdminUserService : IAdminUserService
var profile = new UserProfile
{
UserId = user.Id,
Relationship = random.Next(1, 4), // 1父亲 2母亲 3本人
Relationship = relationship,
Surname = surname,
ChildGender = userGender,
BirthYear = birthYear,
Education = random.Next(3, 7), // 3大专 4本科 5研究生 6博士
WorkProvince = provinces[cityIndex],
WorkCity = cities[cityIndex],
WorkDistrict = districts[cityIndex],
Occupation = occupations[random.Next(occupations.Length)],
MonthlyIncome = random.Next(2, 5), // 2:5k-1w 3:1w-2w 4:2w-5w
Height = userGender == 1 ? random.Next(168, 186) : random.Next(158, 172),
@ -396,6 +426,14 @@ public class AdminUserService : IAdminUserService
HomeProvince = provinces[random.Next(provinces.Length)],
HomeCity = cities[random.Next(cities.Length)],
WeChatNo = $"wx_test_{random.Next(100000, 999999)}",
IsLivingAlone = random.Next(2) == 1,
ParentStatus = parentStatuses[random.Next(parentStatuses.Length)],
ParentRetireStatus = parentRetireStatuses[random.Next(parentRetireStatuses.Length)],
ParentProvince = provinces[random.Next(provinces.Length)],
ParentCity = cities[random.Next(cities.Length)],
ParentHousingStatus = parentHousingStatuses[random.Next(parentHousingStatuses.Length)],
ParentPensionStatus = pensionStatuses[random.Next(pensionStatuses.Length)],
ParentMedicalStatus = medicalStatuses[random.Next(medicalStatuses.Length)],
AuditStatus = 1, // 已通过审核
AuditTime = DateTime.Now,
CreateTime = user.CreateTime,
@ -404,6 +442,27 @@ public class AdminUserService : IAdminUserService
await _profileRepository.AddAsync(profile);
// 生成择偶要求
var reqCityIndex = random.Next(cities.Length);
var requirement = new UserRequirement
{
UserId = user.Id,
AgeMin = random.Next(22, 28),
AgeMax = random.Next(30, 38),
HeightMin = userGender == 1 ? random.Next(155, 165) : random.Next(170, 178),
HeightMax = userGender == 1 ? random.Next(168, 175) : random.Next(180, 190),
Education = "[3,4,5]", // 大专、本科、研究生
City1Province = provinces[reqCityIndex],
City1City = cities[reqCityIndex],
MonthlyIncomeMin = random.Next(2, 4),
MonthlyIncomeMax = random.Next(4, 6),
MarriageStatus = "[1]", // 未婚
CreateTime = user.CreateTime,
UpdateTime = DateTime.Now
};
await _requirementRepository.AddAsync(requirement);
createdIds.Add(user.Id);
_logger.LogInformation("创建测试用户成功: UserId={UserId}, Nickname={Nickname}",
@ -475,7 +534,7 @@ public class AdminUserService : IAdminUserService
}
// 验证会员等级
if (memberLevel < 0 || memberLevel > 3)
if (memberLevel < 0 || memberLevel > 4)
{
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的会员等级");
}
@ -750,6 +809,7 @@ public class AdminUserService : IAdminUserService
1 => "不限时会员",
2 => "诚意会员",
3 => "家庭版会员",
4 => "限时会员",
_ => "未知"
};
}

View File

@ -32,7 +32,8 @@ public class MemberService : IMemberService
{
{ 1, 1299m }, // 不限时会员
{ 2, 1999m }, // 诚意会员
{ 3, 2999m } // 家庭版会员
{ 3, 2999m }, // 家庭版会员
{ 4, 999m } // 限时会员
};
/// <summary>
@ -43,7 +44,8 @@ public class MemberService : IMemberService
{ 0, "非会员" },
{ 1, "不限时会员" },
{ 2, "诚意会员" },
{ 3, "家庭版会员" }
{ 3, "家庭版会员" },
{ 4, "限时会员" }
};
/// <summary>
@ -54,7 +56,8 @@ public class MemberService : IMemberService
{ 0, 10 }, // 非会员
{ 1, 24 }, // 不限时会员
{ 2, 29 }, // 诚意会员24-29人
{ 3, 24 } // 家庭版会员
{ 3, 24 }, // 家庭版会员
{ 4, 24 } // 限时会员(与不限时会员相同)
};
public MemberService(
@ -99,6 +102,17 @@ public class MemberService : IMemberService
BenefitsImage = t.BenefitsImage
}).ToList();
// 判断是否可以绑定家庭成员
// 条件:是家庭版会员 且 不是被别人绑定的用户
var canBindFamily = false;
var familyBindCount = 0;
if (user.MemberLevel == 3)
{
var isBoundByOthers = await _memberFamilyRepository.ExistsAsync(f => f.BindUserId == userId);
canBindFamily = !isBoundByOthers;
}
var response = new MemberInfoResponse
{
IsMember = user.IsMember,
@ -107,7 +121,8 @@ public class MemberService : IMemberService
ExpireTime = user.MemberExpireTime,
ContactCount = user.ContactCount,
DailyRecommendCount = GetDailyRecommendCount(user.MemberLevel),
CanBindFamily = user.MemberLevel == 3, // 家庭版可绑定
CanBindFamily = canBindFamily,
FamilyBindCount = familyBindCount,
MaxFamilyBindCount = IMemberService.MaxFamilyBindCount,
Tiers = tiers
};
@ -125,12 +140,10 @@ public class MemberService : IMemberService
response.StartTime = member.StartTime;
response.ExpireTime = member.ExpireTime;
// 家庭版获取绑定数量
if (user.MemberLevel == 3)
// 主账号获取已绑定数量
if (canBindFamily)
{
var bindCount = await _memberFamilyRepository.CountAsync(f =>
f.MemberId == member.Id);
response.FamilyBindCount = (int)bindCount;
response.FamilyBindCount = (int)await _memberFamilyRepository.CountAsync(f => f.MemberId == member.Id);
}
}
}
@ -320,6 +333,27 @@ public class MemberService : IMemberService
};
}
/// <inheritdoc />
public async Task<FamilyBindResponse> BindFamilyMemberByXiangQinNoAsync(long userId, FamilyBindByXiangQinNoRequest request)
{
if (string.IsNullOrWhiteSpace(request.XiangQinNo))
{
throw new BusinessException(ErrorCodes.InvalidParameter, "请输入相亲编号");
}
// 通过相亲编号查找用户
var bindUsers = await _userRepository.GetListAsync(u => u.XiangQinNo == request.XiangQinNo.Trim());
var bindUser = bindUsers.FirstOrDefault();
if (bindUser == null)
{
throw new BusinessException(ErrorCodes.UserNotFound, "未找到该相亲编号对应的用户");
}
// 调用原有的绑定方法
return await BindFamilyMemberAsync(userId, new FamilyBindRequest { BindUserId = bindUser.Id });
}
/// <inheritdoc />
public async Task<List<FamilyMemberDto>> GetFamilyMembersAsync(long userId)
{

View File

@ -396,6 +396,11 @@ public class OrderService : IOrderService
return;
}
// 获取会员等级配置,读取生效时长
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Level == memberLevel && t.Status == 1);
var tierConfig = tierConfigs.FirstOrDefault();
var durationMonths = tierConfig?.DurationMonths ?? 0;
// 创建会员记录
var member = new Member
{
@ -403,7 +408,7 @@ public class OrderService : IOrderService
MemberLevel = memberLevel,
OrderId = order.Id,
StartTime = DateTime.Now,
ExpireTime = memberLevel == 1 ? null : DateTime.Now.AddYears(1), // 不限时会员无过期时间
ExpireTime = durationMonths == 0 ? null : DateTime.Now.AddMonths(durationMonths),
Status = 1,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now

View File

@ -176,12 +176,10 @@ public class PaymentService : IPaymentService
var existingMembers = await _memberRepository.GetListAsync(m => m.UserId == order.UserId && m.Status == 1);
var existingMember = existingMembers.FirstOrDefault();
DateTime? expireTime = memberLevel switch
DateTime? expireTime = tierConfig?.DurationMonths switch
{
1 => null, // 永久会员
2 => DateTime.Now.AddMonths(1), // 诚意会员1个月
3 => DateTime.Now.AddYears(1), // 家庭版1年
_ => DateTime.Now.AddMonths(1)
null or 0 => null, // 永久会员
var months => DateTime.Now.AddMonths(months.Value)
};
if (existingMember != null)

View File

@ -58,4 +58,9 @@ public class MemberTierConfig : BaseEntity
/// 状态1启用 2禁用
/// </summary>
public int Status { get; set; } = 1;
/// <summary>
/// 会员生效时长0表示永久
/// </summary>
public int DurationMonths { get; set; } = 0;
}

View File

@ -157,9 +157,9 @@ public class FamilyBindLimitPropertyTests
/// 家庭版绑定 - 最大绑定数量应该是2
/// </summary>
[Fact]
public void FamilyBind_MaxBindCount_ShouldBeTwo()
public void FamilyBind_MaxBindCount_ShouldBeThree()
{
Assert.Equal(2, IMemberService.MaxFamilyBindCount);
Assert.Equal(3, IMemberService.MaxFamilyBindCount);
}
/// <summary>