会员,未读消息

This commit is contained in:
18631081161 2026-02-07 15:41:18 +08:00
parent 23a620d69d
commit de2b59ca90
20 changed files with 312 additions and 64 deletions

View File

@ -112,6 +112,7 @@ export interface MemberIconsConfig {
unlimitedMemberIcon?: string
sincereMemberIcon?: string
familyMemberIcon?: string
timeLimitedMemberIcon?: string
}
/**

View File

@ -65,3 +65,17 @@ export function updateMemberTier(id: number, data: UpdateMemberTierRequest) {
export function deleteMemberTier(id: number) {
return request.delete(`/admin/memberTiers/${id}`)
}
/**
*
*/
export function getDefaultTierLevel() {
return request.get<number | null>('/admin/memberTiers/defaultLevel')
}
/**
*
*/
export function setDefaultTierLevel(level: number) {
return request.put('/admin/memberTiers/defaultLevel', { level })
}

View File

@ -68,6 +68,7 @@ export interface UserQueryParams {
registerEndTime?: string
gender?: number
city?: string
isTestUser?: boolean
}
/**
@ -94,6 +95,7 @@ export interface UserListItem {
contactCount: number
createTime: string
lastLoginTime: string
isTestUser: boolean
}
/**

View File

@ -30,6 +30,9 @@
<el-tag :type="tier.status === 1 ? 'success' : 'info'" size="small">
{{ tier.status === 1 ? '启用' : '禁用' }}
</el-tag>
<el-tag v-if="defaultLevel === tier.level" type="warning" size="small" style="margin-left: 4px;">
默认选中
</el-tag>
</div>
<!-- 等级信息 -->
@ -70,6 +73,15 @@
<!-- 操作按钮 -->
<div class="tier-actions">
<el-button
v-if="defaultLevel !== tier.level"
type="warning"
size="small"
plain
@click="handleSetDefault(tier)"
>
设为默认
</el-button>
<el-button type="primary" size="small" @click="handleEdit(tier)">
<el-icon><Edit /></el-icon>
编辑
@ -220,6 +232,8 @@ import {
createMemberTier,
updateMemberTier,
deleteMemberTier,
getDefaultTierLevel,
setDefaultTierLevel,
type MemberTier
} from '@/api/memberTier'
import { useUserStore } from '@/stores/user'
@ -235,6 +249,7 @@ const isEdit = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()
const editingId = ref<number | null>(null)
const defaultLevel = ref<number | null>(null)
const formData = reactive({
level: 1,
@ -270,8 +285,12 @@ const getFullUrl = (url: string) => {
const loadList = async () => {
loading.value = true
try {
const res: any = await getMemberTierList()
const [res, defaultRes]: any[] = await Promise.all([
getMemberTierList(),
getDefaultTierLevel()
])
tierList.value = res || []
defaultLevel.value = defaultRes ?? null
} catch (error) {
console.error('加载列表失败:', error)
} finally {
@ -332,6 +351,16 @@ const handleDelete = async (row: MemberTier) => {
}
}
const handleSetDefault = async (tier: MemberTier) => {
try {
await setDefaultTierLevel(tier.level)
defaultLevel.value = tier.level
ElMessage.success(`已设置「${tier.name}」为默认选中`)
} catch (error) {
console.error('设置默认失败:', error)
}
}
const handleUploadSuccess = (response: any) => {
if (response.code === 0 && response.data) {
formData.benefitsImage = response.data.url
@ -575,11 +604,10 @@ onMounted(() => {
/* 排序标识 */
.tier-sort {
position: absolute;
bottom: 8px;
right: 12px;
text-align: right;
font-size: 11px;
color: #c0c4cc;
margin-top: 10px;
}
/* 弹窗样式 */

View File

@ -147,6 +147,28 @@
</div>
</el-form-item>
<el-form-item label="限时会员图标">
<div class="icon-upload">
<el-upload
class="icon-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleTimeLimitedMemberIconSuccess"
:before-upload="beforeAvatarUpload"
accept="image/*"
>
<img v-if="configForm.timeLimitedMemberIcon" :src="getFullUrl(configForm.timeLimitedMemberIcon)" class="icon-preview" />
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="avatar-tip">
<p>建议尺寸64x64像素</p>
<p>支持格式PNG透明背景</p>
<p>限时会员用户显示的图标</p>
</div>
</div>
</el-form-item>
<!-- 会员入口图设置 -->
<el-form-item label="会员入口图">
<div class="banner-upload">
@ -276,6 +298,7 @@ const configForm = ref({
unlimitedMemberIcon: '',
sincereMemberIcon: '',
familyMemberIcon: '',
timeLimitedMemberIcon: '',
memberEntryImage: '',
realNamePrice: 88
})
@ -324,6 +347,7 @@ const loadConfig = async () => {
configForm.value.unlimitedMemberIcon = memberIconsRes.unlimitedMemberIcon || ''
configForm.value.sincereMemberIcon = memberIconsRes.sincereMemberIcon || ''
configForm.value.familyMemberIcon = memberIconsRes.familyMemberIcon || ''
configForm.value.timeLimitedMemberIcon = memberIconsRes.timeLimitedMemberIcon || ''
}
if (memberEntryRes) {
configForm.value.memberEntryImage = memberEntryRes.imageUrl || ''
@ -409,6 +433,15 @@ const handleFamilyMemberIconSuccess = (response) => {
}
}
const handleTimeLimitedMemberIconSuccess = (response) => {
if (response.code === 0 && response.data) {
configForm.value.timeLimitedMemberIcon = response.data.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message || '上传失败')
}
}
const handleMemberEntryImageSuccess = (response) => {
if (response.code === 0 && response.data) {
configForm.value.memberEntryImage = response.data.url
@ -453,9 +486,10 @@ const saveBasicConfig = async () => {
const memberIcons = {
unlimitedMemberIcon: configForm.value.unlimitedMemberIcon || undefined,
sincereMemberIcon: configForm.value.sincereMemberIcon || undefined,
familyMemberIcon: configForm.value.familyMemberIcon || undefined
familyMemberIcon: configForm.value.familyMemberIcon || undefined,
timeLimitedMemberIcon: configForm.value.timeLimitedMemberIcon || undefined
}
if (memberIcons.unlimitedMemberIcon || memberIcons.sincereMemberIcon || memberIcons.familyMemberIcon) {
if (memberIcons.unlimitedMemberIcon || memberIcons.sincereMemberIcon || memberIcons.familyMemberIcon || memberIcons.timeLimitedMemberIcon) {
promises.push(setMemberIcons(memberIcons))
}
//

View File

@ -26,7 +26,8 @@ const searchForm = reactive<Partial<UserQueryParams>>({
isMember: undefined,
memberLevel: undefined,
isRealName: undefined,
gender: undefined
gender: undefined,
isTestUser: undefined
})
//
@ -77,6 +78,13 @@ const genderOptions = [
{ label: '女', value: 2 }
]
//
const userTypeOptions = [
{ label: '全部', value: '' },
{ label: '真实用户', value: false },
{ label: '测试用户', value: true }
]
//
const fetchUserList = async () => {
loading.value = true
@ -320,6 +328,21 @@ onMounted(() => {
/>
</el-select>
</el-form-item>
<el-form-item label="用户类型">
<el-select
v-model="searchForm.isTestUser"
placeholder="请选择"
clearable
style="width: 120px"
>
<el-option
v-for="item in userTypeOptions"
:key="String(item.value)"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</SearchForm>
<!-- 数据表格 -->
@ -351,6 +374,16 @@ onMounted(() => {
fixed="left"
align="center"
/>
<el-table-column
label="用户类型"
min-width="80"
align="center"
>
<template #default="{ row }">
<el-tag v-if="row.isTestUser" type="info" size="small">测试</el-tag>
<el-tag v-else type="success" size="small">真实</el-tag>
</template>
</el-table-column>
<el-table-column
label="用户信息"
min-width="180"

View File

@ -6,8 +6,7 @@
import { get, post, del } from './request'
/**
* 获取会员信息
* WHEN a user visits member page, THE XiangYi_MiniApp SHALL display three membership tiers
* 获取会员信息未登录时只返回等级配置
* Requirements: 10.1
*
* @returns {Promise<Object>} 会员信息

View File

@ -214,6 +214,7 @@
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import { useChatStore } from '@/store/chat.js'
import { getRecommend } from '@/api/user.js'
import { checkUnlock, unlock } from '@/api/interact.js'
import { getFullImageUrl } from '@/utils/image.js'
@ -1017,6 +1018,7 @@ export default {
onShow() {
const configStore = useConfigStore()
const userStore = useUserStore()
const chatStore = useChatStore()
//
if (userStore.isLoggedIn) {
@ -1027,6 +1029,9 @@ export default {
if (now - lastRefresh > REFRESH_INTERVAL) {
userStore.refreshFromServer()
}
// badge
chatStore.fetchAllUnreadCounts()
}
//

View File

@ -125,65 +125,48 @@ export default {
}
/**
* 会员等级配置
* 会员等级配置从后端加载
*/
const memberTiers = ref([
{
level: 1,
name: '永久会员',
price: 1299,
originalPrice: 1899,
discount: '8折优惠',
badge: ''
},
{
level: 2,
name: '诚意会员',
price: 1999,
originalPrice: 2899,
discount: '7折优惠',
badge: ''
},
{
level: 3,
name: '诚意会员',
price: 2999,
originalPrice: 4299,
discount: '7折优惠',
badge: '家庭版'
}
])
const memberTiers = ref([])
//
const selectedTierInfo = computed(() => {
return memberTiers.value.find(t => t.level === selectedTier.value)
})
//
// tiers
const updateTiers = (tiers, configDefaultLevel) => {
memberTiers.value = tiers.map(tier => ({
level: tier.level,
name: tier.name,
badge: tier.badge || '',
price: tier.price,
originalPrice: tier.originalPrice,
discount: tier.discount || '',
benefitsImage: tier.benefitsImage ? getFullImageUrl(tier.benefitsImage) : ''
}))
// > >
if (defaultLevel.value && memberTiers.value.some(t => t.level === defaultLevel.value)) {
selectedTier.value = defaultLevel.value
} else if (configDefaultLevel && memberTiers.value.some(t => t.level === configDefaultLevel)) {
selectedTier.value = configDefaultLevel
} else if (memberTiers.value.length > 0) {
selectedTier.value = memberTiers.value[0].level
}
}
//
const loadMemberConfig = async () => {
try {
const res = await getMemberInfo()
if (res && (res.success || res.code === 0) && res.data) {
//
userStore.setMemberStatus(res.data.isMember, res.data.memberLevel)
//
//
if (userStore.isLoggedIn) {
userStore.setMemberStatus(res.data.isMember, res.data.memberLevel)
}
//
if (res.data.tiers && res.data.tiers.length > 0) {
memberTiers.value = res.data.tiers.map(tier => ({
level: tier.level,
name: tier.name,
badge: tier.badge || '',
price: tier.price,
originalPrice: tier.originalPrice,
discount: tier.discount || '',
benefitsImage: tier.benefitsImage ? getFullImageUrl(tier.benefitsImage) : ''
}))
// 使
if (defaultLevel.value && memberTiers.value.some(t => t.level === defaultLevel.value)) {
selectedTier.value = defaultLevel.value
} else if (memberTiers.value.length > 0) {
selectedTier.value = memberTiers.value[0].level
}
updateTiers(res.data.tiers, res.data.defaultTierLevel)
}
}
} catch (error) {
@ -235,6 +218,12 @@ export default {
const handlePurchase = async () => {
if (!selectedTier.value || purchasing.value) return
//
if (!userStore.isLoggedIn) {
uni.navigateTo({ url: '/pages/login/index' })
return
}
purchasing.value = true
console.log('开始购买会员...')

View File

@ -200,6 +200,7 @@
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import { useChatStore } from '@/store/chat.js'
import { getInteractCounts, markInteractAsRead } from '@/api/interact.js'
import { bindFamilyByXiangQinNo, getFamilyMembers, unbindFamilyMember, getMemberInfo } from '@/api/member.js'
import { getFullImageUrl } from '@/utils/image.js'
@ -541,6 +542,7 @@ export default {
},
async onShow() {
const userStore = useUserStore()
const chatStore = useChatStore()
userStore.restoreFromStorage()
// 便
if (this.avatarLoadError !== undefined) {
@ -552,6 +554,8 @@ export default {
if (this.loadInteractCounts) {
this.loadInteractCounts()
}
// badge
chatStore.fetchAllUnreadCounts()
}
}
}

View File

@ -46,7 +46,8 @@ export const useConfigStore = defineStore('config', {
memberIcons: {
unlimitedMemberIcon: '', // 不限时会员图标
sincereMemberIcon: '', // 诚意会员图标
familyMemberIcon: '' // 家庭版会员图标
familyMemberIcon: '', // 家庭版会员图标
timeLimitedMemberIcon: '' // 限时会员图标
},
// 弹窗配置
@ -99,6 +100,8 @@ export const useConfigStore = defineStore('config', {
return state.memberIcons.sincereMemberIcon || ''
case 3:
return state.memberIcons.familyMemberIcon || ''
case 4:
return state.memberIcons.timeLimitedMemberIcon || ''
default:
return ''
}
@ -135,7 +138,8 @@ export const useConfigStore = defineStore('config', {
this.memberIcons = {
unlimitedMemberIcon: config.memberIcons.unlimitedMemberIcon || '',
sincereMemberIcon: config.memberIcons.sincereMemberIcon || '',
familyMemberIcon: config.memberIcons.familyMemberIcon || ''
familyMemberIcon: config.memberIcons.familyMemberIcon || '',
timeLimitedMemberIcon: config.memberIcons.timeLimitedMemberIcon || ''
}
}

View File

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using XiangYi.Application.DTOs.Responses;
using XiangYi.Application.Interfaces;
using XiangYi.Application.Services;
using XiangYi.Core.Entities.Biz;
using XiangYi.Core.Interfaces;
@ -15,13 +17,16 @@ namespace XiangYi.AdminApi.Controllers;
public class AdminMemberTierController : ControllerBase
{
private readonly IRepository<MemberTierConfig> _tierRepository;
private readonly ISystemConfigService _systemConfigService;
private readonly ILogger<AdminMemberTierController> _logger;
public AdminMemberTierController(
IRepository<MemberTierConfig> tierRepository,
ISystemConfigService systemConfigService,
ILogger<AdminMemberTierController> logger)
{
_tierRepository = tierRepository;
_systemConfigService = systemConfigService;
_logger = logger;
}
@ -189,6 +194,31 @@ public class AdminMemberTierController : ControllerBase
return ApiResponse.Success("删除成功");
}
/// <summary>
/// 获取默认选中会员等级
/// </summary>
[HttpGet("defaultLevel")]
public async Task<ApiResponse<int?>> GetDefaultLevel()
{
var value = await _systemConfigService.GetConfigValueAsync(SystemConfigService.DefaultMemberTierLevelKey);
int? level = int.TryParse(value, out var l) ? l : null;
return ApiResponse<int?>.Success(level);
}
/// <summary>
/// 设置默认选中会员等级
/// </summary>
[HttpPut("defaultLevel")]
public async Task<ApiResponse> SetDefaultLevel([FromBody] SetDefaultLevelRequest request)
{
await _systemConfigService.SetConfigValueAsync(
SystemConfigService.DefaultMemberTierLevelKey,
request.Level.ToString(),
"默认选中会员等级");
_logger.LogInformation("设置默认选中会员等级: Level={Level}", request.Level);
return ApiResponse.Success("设置成功");
}
}
/// <summary>
@ -244,3 +274,11 @@ public class UpdateMemberTierRequest
public int Status { get; set; }
public int DurationMonths { get; set; } = 0;
}
/// <summary>
/// 设置默认选中等级请求
/// </summary>
public class SetDefaultLevelRequest
{
public int Level { get; set; }
}

View File

@ -17,22 +17,32 @@ namespace XiangYi.AppApi.Controllers;
public class MemberController : ControllerBase
{
private readonly IMemberService _memberService;
private readonly ISystemConfigService _systemConfigService;
private readonly ILogger<MemberController> _logger;
public MemberController(IMemberService memberService, ILogger<MemberController> logger)
public MemberController(IMemberService memberService, ISystemConfigService systemConfigService, ILogger<MemberController> logger)
{
_memberService = memberService;
_systemConfigService = systemConfigService;
_logger = logger;
}
/// <summary>
/// 获取会员信息
/// 获取会员信息(未登录时只返回等级配置)
/// </summary>
/// <returns>会员信息</returns>
[HttpGet("info")]
[AllowAnonymous]
public async Task<ApiResponse<MemberInfoResponse>> GetMemberInfo()
{
var userId = GetCurrentUserId();
if (userId <= 0)
{
// 未登录:只返回会员等级配置和默认选中等级
var tiers = await _memberService.GetMemberTiersAsync();
var defaultTierLevel = await GetDefaultTierLevelAsync();
return ApiResponse<MemberInfoResponse>.Success(new MemberInfoResponse { Tiers = tiers, DefaultTierLevel = defaultTierLevel });
}
var result = await _memberService.GetMemberInfoAsync(userId);
return ApiResponse<MemberInfoResponse>.Success(result);
}
@ -45,7 +55,7 @@ public class MemberController : ControllerBase
[HttpPost("purchase")]
public async Task<ApiResponse<MemberPurchaseResponse>> Purchase([FromBody] MemberPurchaseRequest request)
{
if (request.MemberLevel < 1 || request.MemberLevel > 3)
if (request.MemberLevel < 1 || request.MemberLevel > 4)
{
return ApiResponse<MemberPurchaseResponse>.Error(ErrorCodes.InvalidParameter, "会员等级无效");
}
@ -141,4 +151,14 @@ public class MemberController : ControllerBase
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return long.TryParse(userIdClaim, out var userId) ? userId : 0;
}
/// <summary>
/// 获取默认选中会员等级
/// </summary>
private async Task<int?> GetDefaultTierLevelAsync()
{
var value = await _systemConfigService.GetConfigValueAsync(
XiangYi.Application.Services.SystemConfigService.DefaultMemberTierLevelKey);
return int.TryParse(value, out var level) ? level : null;
}
}

View File

@ -69,6 +69,11 @@ public class AdminUserQueryRequest
/// 城市
/// </summary>
public string? City { get; set; }
/// <summary>
/// 是否测试用户
/// </summary>
public bool? IsTestUser { get; set; }
}
/// <summary>

View File

@ -104,6 +104,11 @@ public class AdminUserListDto
/// 最后登录时间
/// </summary>
public DateTime? LastLoginTime { get; set; }
/// <summary>
/// 是否测试用户
/// </summary>
public bool IsTestUser { get; set; }
}

View File

@ -64,6 +64,11 @@ public class MemberInfoResponse
/// 会员等级配置列表
/// </summary>
public List<MemberTierResponse> Tiers { get; set; } = new();
/// <summary>
/// 默认选中的会员等级(管理后台配置)
/// </summary>
public int? DefaultTierLevel { get; set; }
}
/// <summary>

View File

@ -54,6 +54,12 @@ public interface IMemberService
/// <returns>是否成功</returns>
Task<bool> UnbindFamilyMemberAsync(long userId, long bindUserId);
/// <summary>
/// 获取会员等级配置列表(无需登录)
/// </summary>
/// <returns>会员等级配置列表</returns>
Task<List<MemberTierResponse>> GetMemberTiersAsync();
/// <summary>
/// 计算家庭版绑定后的数量(用于属性测试)
/// </summary>

View File

@ -110,6 +110,19 @@ public class AdminUserService : IAdminUserService
// Exclude soft deleted users
query = query.Where(u => !u.IsDeleted);
// Filter by test user
if (request.IsTestUser.HasValue)
{
if (request.IsTestUser.Value)
{
query = query.Where(u => u.OpenId.StartsWith("test_openid_"));
}
else
{
query = query.Where(u => !u.OpenId.StartsWith("test_openid_"));
}
}
// Get total count
var total = query.Count();
@ -620,7 +633,8 @@ public class AdminUserService : IAdminUserService
AuditStatusText = profile != null ? GetAuditStatusText(profile.AuditStatus) : null,
ContactCount = user.ContactCount,
CreateTime = user.CreateTime,
LastLoginTime = user.LastLoginTime
LastLoginTime = user.LastLoginTime,
IsTestUser = user.OpenId.StartsWith("test_openid_")
};
}

View File

@ -102,6 +102,10 @@ public class MemberService : IMemberService
BenefitsImage = t.BenefitsImage
}).ToList();
// 获取默认选中等级配置
var defaultTierLevelStr = await _systemConfigService.GetConfigValueAsync(SystemConfigService.DefaultMemberTierLevelKey);
int? defaultTierLevel = int.TryParse(defaultTierLevelStr, out var dtl) ? dtl : null;
// 判断是否可以绑定家庭成员
// 条件:是家庭版会员 且 不是被别人绑定的用户
var canBindFamily = false;
@ -124,7 +128,8 @@ public class MemberService : IMemberService
CanBindFamily = canBindFamily,
FamilyBindCount = familyBindCount,
MaxFamilyBindCount = IMemberService.MaxFamilyBindCount,
Tiers = tiers
Tiers = tiers,
DefaultTierLevel = defaultTierLevel
};
// 获取会员记录详情
@ -448,6 +453,22 @@ public class MemberService : IMemberService
return true;
}
/// <inheritdoc />
public async Task<List<MemberTierResponse>> GetMemberTiersAsync()
{
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Status == 1);
return tierConfigs.OrderBy(t => t.Sort).Select(t => new MemberTierResponse
{
Level = t.Level,
Name = t.Name,
Badge = t.Badge,
Price = t.Price,
OriginalPrice = t.OriginalPrice,
Discount = t.Discount,
BenefitsImage = t.BenefitsImage
}).ToList();
}
#region
/// <summary>

View File

@ -63,6 +63,16 @@ public class SystemConfigService : ISystemConfigService
/// </summary>
public const string FamilyMemberIconKey = "family_member_icon";
/// <summary>
/// 限时会员图标配置键
/// </summary>
public const string TimeLimitedMemberIconKey = "time_limited_member_icon";
/// <summary>
/// 默认选中会员等级配置键
/// </summary>
public const string DefaultMemberTierLevelKey = "default_member_tier_level";
/// <summary>
/// 会员入口图配置键
/// </summary>
@ -231,12 +241,14 @@ public class SystemConfigService : ISystemConfigService
var unlimited = await GetConfigValueAsync(UnlimitedMemberIconKey);
var sincere = await GetConfigValueAsync(SincereMemberIconKey);
var family = await GetConfigValueAsync(FamilyMemberIconKey);
var timeLimited = await GetConfigValueAsync(TimeLimitedMemberIconKey);
return new MemberIconsDto
{
UnlimitedMemberIcon = unlimited,
SincereMemberIcon = sincere,
FamilyMemberIcon = family
FamilyMemberIcon = family,
TimeLimitedMemberIcon = timeLimited
};
}
@ -257,6 +269,10 @@ public class SystemConfigService : ISystemConfigService
{
await SetConfigValueAsync(FamilyMemberIconKey, icons.FamilyMemberIcon, "家庭版会员图标URL");
}
if (!string.IsNullOrEmpty(icons.TimeLimitedMemberIcon))
{
await SetConfigValueAsync(TimeLimitedMemberIconKey, icons.TimeLimitedMemberIcon, "限时会员图标URL");
}
return true;
}
catch (Exception ex)
@ -315,4 +331,9 @@ public class MemberIconsDto
/// 家庭版会员图标URL
/// </summary>
public string? FamilyMemberIcon { get; set; }
/// <summary>
/// 限时会员图标URL
/// </summary>
public string? TimeLimitedMemberIcon { get; set; }
}