逻辑优化

This commit is contained in:
18631081161 2026-01-27 20:09:30 +08:00
parent 4827573849
commit cb4c207ca8
13 changed files with 1503 additions and 960 deletions

View File

@ -148,6 +148,27 @@ export async function checkUnlock(targetUserId) {
return response
}
/**
* 获取互动新增数量统计
*
* @returns {Promise<Object>} 各类互动的新增数量 { viewedMeCount, favoritedMeCount, unlockedMeCount }
*/
export async function getInteractCounts() {
const response = await get('/interact/counts')
return response
}
/**
* 标记互动为已读
*
* @param {string} type - 互动类型viewedMe, favoritedMe, unlockedMe
* @returns {Promise<Object>} 操作结果
*/
export async function markInteractAsRead(type) {
const response = await post(`/interact/markRead/${type}`)
return response
}
export default {
recordView,
favorite,
@ -159,5 +180,7 @@ export default {
getFavoritedMe,
getMyFavorite,
getUnlockedMe,
getMyUnlocked
getMyUnlocked,
getInteractCounts,
markInteractAsRead
}

View File

@ -186,7 +186,8 @@ export default {
}
},
handleButtonClick() {
if (this.linkUrl) {
//
if (this.type !== 'serviceAccount' && this.linkUrl) {
this.navigateToLink()
}
this.$emit('confirm')

File diff suppressed because it is too large Load Diff

View File

@ -158,9 +158,8 @@
deleteSession
} from '@/api/chat.js'
import {
getViewedMe,
getFavoritedMe,
getUnlockedMe
getInteractCounts,
markInteractAsRead
} from '@/api/interact.js'
import {
formatTimestamp
@ -173,7 +172,6 @@
//
const pageLoading = ref(true)
const listLoading = ref(false)
const isRefreshing = ref(false)
//
@ -222,52 +220,20 @@
}).exec()
}
//
/**
* 加载互动统计从后端获取新增数量
*/
const loadInteractCounts = async () => {
if (!userStore.isLoggedIn) return
try {
//
const lastViewedTime = uni.getStorageSync('lastViewedMeTime') || 0
const lastFavoritedTime = uni.getStorageSync('lastFavoritedMeTime') || 0
const lastUnlockedTime = uni.getStorageSync('lastUnlockedMeTime') || 0
const [viewedRes, favoritedRes, unlockedRes] = await Promise.all([
getViewedMe(1, 100).catch(() => null),
getFavoritedMe(1, 100).catch(() => null),
getUnlockedMe(1, 100).catch(() => null)
])
//
let viewedCount = 0
let favoritedCount = 0
let unlockedCount = 0
if (viewedRes?.data?.items) {
viewedCount = viewedRes.data.items.filter(item => {
const itemTime = new Date(item.viewTime || item.createTime).getTime()
return itemTime > lastViewedTime
}).length
}
if (favoritedRes?.data?.items) {
favoritedCount = favoritedRes.data.items.filter(item => {
const itemTime = new Date(item.favoriteTime || item.createTime).getTime()
return itemTime > lastFavoritedTime
}).length
}
if (unlockedRes?.data?.items) {
unlockedCount = unlockedRes.data.items.filter(item => {
const itemTime = new Date(item.unlockTime || item.createTime).getTime()
return itemTime > lastUnlockedTime
}).length
}
interactCounts.value = {
viewedMe: viewedCount,
favoritedMe: favoritedCount,
unlockedMe: unlockedCount
const res = await getInteractCounts()
if (res?.code === 0 && res.data) {
interactCounts.value = {
viewedMe: res.data.viewedMeCount || 0,
favoritedMe: res.data.favoritedMeCount || 0,
unlockedMe: res.data.unlockedMeCount || 0
}
}
} catch (error) {
console.error('加载互动统计失败:', error)
@ -278,7 +244,6 @@
const loadSessions = async () => {
if (!userStore.isLoggedIn) return
listLoading.value = true
try {
const res = await getSessions()
if (res?.success || res?.code === 0) {
@ -291,8 +256,6 @@
title: '加载失败',
icon: 'none'
})
} finally {
listLoading.value = false
}
}
@ -328,23 +291,28 @@
const formatTime = (timestamp) => formatTimestamp(timestamp)
//
const navigateTo = (url) => {
//
const now = Date.now()
const navigateTo = async (url) => {
// URL
let interactType = ''
if (url.includes('viewedMe')) {
uni.setStorageSync('lastViewedMeTime', now)
interactType = 'viewedMe'
interactCounts.value.viewedMe = 0
} else if (url.includes('favoritedMe')) {
uni.setStorageSync('lastFavoritedMeTime', now)
interactType = 'favoritedMe'
interactCounts.value.favoritedMe = 0
} else if (url.includes('unlockedMe')) {
uni.setStorageSync('lastUnlockedMeTime', now)
interactType = 'unlockedMe'
interactCounts.value.unlockedMe = 0
}
uni.navigateTo({
url
})
//
if (interactType) {
markInteractAsRead(interactType).catch(err => {
console.error('标记已读失败:', err)
})
}
uni.navigateTo({ url })
}
//

View File

@ -147,9 +147,7 @@
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import { getMyProfile } from '@/api/profile.js'
import { login } from '@/api/auth.js'
import { getViewedMe, getFavoritedMe, getUnlockedMe } from '@/api/interact.js'
import { getInteractCounts, markInteractAsRead } from '@/api/interact.js'
import { getFullImageUrl } from '@/utils/image.js'
import Loading from '@/components/Loading/index.vue'
@ -211,52 +209,20 @@ export default {
return configStore.memberEntryImage ? getFullImageUrl(configStore.memberEntryImage) : ''
})
//
/**
* 加载互动统计从后端获取新增数量
*/
const loadInteractCounts = async () => {
if (!userStore.isLoggedIn) return
try {
//
const lastViewedTime = uni.getStorageSync('lastViewedMeTime') || 0
const lastFavoritedTime = uni.getStorageSync('lastFavoritedMeTime') || 0
const lastUnlockedTime = uni.getStorageSync('lastUnlockedMeTime') || 0
const [viewedRes, favoritedRes, unlockedRes] = await Promise.all([
getViewedMe(1, 100).catch(() => null),
getFavoritedMe(1, 100).catch(() => null),
getUnlockedMe(1, 100).catch(() => null)
])
//
let viewedCount = 0
let favoritedCount = 0
let unlockedCount = 0
if (viewedRes?.data?.items) {
viewedCount = viewedRes.data.items.filter(item => {
const itemTime = new Date(item.viewTime || item.createTime).getTime()
return itemTime > lastViewedTime
}).length
}
if (favoritedRes?.data?.items) {
favoritedCount = favoritedRes.data.items.filter(item => {
const itemTime = new Date(item.favoriteTime || item.createTime).getTime()
return itemTime > lastFavoritedTime
}).length
}
if (unlockedRes?.data?.items) {
unlockedCount = unlockedRes.data.items.filter(item => {
const itemTime = new Date(item.unlockTime || item.createTime).getTime()
return itemTime > lastUnlockedTime
}).length
}
interactCounts.value = {
viewedMe: viewedCount,
favoritedMe: favoritedCount,
unlockedMe: unlockedCount
const res = await getInteractCounts()
if (res?.code === 0 && res.data) {
interactCounts.value = {
viewedMe: res.data.viewedMeCount || 0,
favoritedMe: res.data.favoritedMeCount || 0,
unlockedMe: res.data.unlockedMeCount || 0
}
}
} catch (error) {
console.error('加载互动统计失败:', error)
@ -288,15 +254,6 @@ export default {
uni.navigateTo({ url: '/pages/profile/personal' })
}
//
const handlePreviewProfile = () => {
if (!userStore.isProfileCompleted) {
handleEditProfile()
return
}
uni.navigateTo({ url: '/pages/profile/preview' })
}
//
const handleEditProfile = () => {
uni.navigateTo({ url: '/pages/profile/edit' })
@ -347,20 +304,27 @@ export default {
}
//
const navigateTo = (url) => {
//
const now = Date.now()
const navigateTo = async (url) => {
// URL
let interactType = ''
if (url.includes('viewedMe')) {
uni.setStorageSync('lastViewedMeTime', now)
interactType = 'viewedMe'
interactCounts.value.viewedMe = 0
} else if (url.includes('favoritedMe')) {
uni.setStorageSync('lastFavoritedMeTime', now)
interactType = 'favoritedMe'
interactCounts.value.favoritedMe = 0
} else if (url.includes('unlockedMe')) {
uni.setStorageSync('lastUnlockedMeTime', now)
interactType = 'unlockedMe'
interactCounts.value.unlockedMe = 0
}
//
if (interactType) {
markInteractAsRead(interactType).catch(err => {
console.error('标记已读失败:', err)
})
}
uni.navigateTo({ url })
}
@ -388,7 +352,6 @@ export default {
onAvatarError,
handleLogin,
handlePersonalProfile,
handlePreviewProfile,
handleEditProfile,
handleMember,
handleButler,

View File

@ -396,7 +396,8 @@
console.log('调起微信支付...')
await requestPayment(paymentParams)
// (Requirements 12.2)
// (Requirements 12.2)
isPaid.value = true
currentStep.value = 2
uni.showToast({
@ -685,9 +686,25 @@
handleDone
}
},
onShow() {
async onShow() {
const userStore = useUserStore()
userStore.restoreFromStorage()
//
const { getToken } = require('@/utils/storage.js')
if (getToken()) {
const { get } = require('@/api/request.js')
try {
const res = await get('/realname/status')
if (res && (res.success || res.code === 0) && res.data) {
if (res.data.isRealName) {
userStore.setRealNameStatus(true)
}
}
} catch (error) {
console.error('刷新实名状态失败:', error)
}
}
}
}
</script>

View File

@ -37,7 +37,9 @@ export const useUserStore = defineStore('user', {
isMember: false,
memberLevel: 0,
isRealName: false,
genderPreference: getGenderPreference() || 0
genderPreference: getGenderPreference() || 0,
/** 上次从服务器刷新用户信息的时间戳(用于节流) */
lastRefreshTime: 0
}),
getters: {
@ -215,6 +217,8 @@ export const useUserStore = defineStore('user', {
memberLevel: data.memberLevel,
isRealName: data.isRealName
})
// 更新刷新时间戳
this.lastRefreshTime = Date.now()
}
} catch (error) {
console.error('刷新用户信息失败:', error)

200
miniapp/utils/navigate.js Normal file
View File

@ -0,0 +1,200 @@
/**
* 页面导航工具函数
* 统一处理小程序内的各种跳转逻辑
*/
/**
* TabBar 页面路径列表
* 这些页面需要使用 switchTab 而不是 navigateTo
*/
export const TABBAR_PAGES = [
'/pages/index/index',
'/pages/message/index',
'/pages/mine/index'
]
/**
* 链接类型枚举
*/
export const LINK_TYPE = {
/** 内部页面 */
INTERNAL: 1,
/** 外部链接H5 */
EXTERNAL: 2,
/** 其他小程序 */
MINI_PROGRAM: 3
}
/**
* 判断是否为 TabBar 页面
* @param {string} url - 页面路径
* @returns {boolean}
*/
export function isTabBarPage(url) {
return TABBAR_PAGES.includes(url)
}
/**
* 统一页面跳转函数
* 根据链接类型自动选择合适的跳转方式
*
* @param {string} url - 跳转地址
* @param {number} linkType - 链接类型1=内部页面, 2=外部链接, 3=小程序
* @param {Object} options - 额外配置
* @param {Function} options.onFail - 跳转失败回调
* @param {Function} options.onSuccess - 跳转成功回调
*/
export function navigateToPage(url, linkType = LINK_TYPE.INTERNAL, options = {}) {
if (!url) {
console.warn('[navigate] url 为空,跳过跳转')
return
}
const { onFail, onSuccess } = options
switch (linkType) {
case LINK_TYPE.EXTERNAL:
// 外部链接:复制到剪贴板(小程序限制无法直接打开外部链接)
handleExternalLink(url)
break
case LINK_TYPE.MINI_PROGRAM:
// 跳转其他小程序
handleMiniProgramLink(url, { onFail, onSuccess })
break
case LINK_TYPE.INTERNAL:
default:
// 内部页面跳转
handleInternalLink(url, { onFail, onSuccess })
break
}
}
/**
* 处理内部页面跳转
* 自动判断是否为 TabBar 页面
*
* @param {string} url - 页面路径
* @param {Object} callbacks - 回调函数
*/
function handleInternalLink(url, { onFail, onSuccess } = {}) {
const navigateMethod = isTabBarPage(url) ? uni.switchTab : uni.navigateTo
navigateMethod({
url,
success: () => {
onSuccess && onSuccess()
},
fail: (err) => {
console.error('[navigate] 页面跳转失败:', url, err)
if (onFail) {
onFail(err)
} else {
uni.showToast({
title: '页面跳转失败',
icon: 'none'
})
}
}
})
}
/**
* 处理外部链接
* 由于小程序限制外部链接只能复制到剪贴板
*
* @param {string} url - 外部链接地址
*/
function handleExternalLink(url) {
uni.setClipboardData({
data: url,
success: () => {
uni.showToast({
title: '链接已复制',
icon: 'success'
})
},
fail: () => {
uni.showToast({
title: '复制失败',
icon: 'none'
})
}
})
}
/**
* 处理小程序跳转
* URL 格式appId:pathpath 可选
*
* @param {string} url - 格式为 "appId:path" 的字符串
* @param {Object} callbacks - 回调函数
*/
function handleMiniProgramLink(url, { onFail, onSuccess } = {}) {
const [appId, path] = url.split(':')
if (!appId) {
console.error('[navigate] 小程序 appId 为空')
uni.showToast({
title: '跳转配置错误',
icon: 'none'
})
return
}
uni.navigateToMiniProgram({
appId,
path: path || '',
success: () => {
onSuccess && onSuccess()
},
fail: (err) => {
console.error('[navigate] 跳转小程序失败:', appId, err)
if (onFail) {
onFail(err)
} else {
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
}
})
}
/**
* 跳转到 WebView 页面
* 用于在小程序内打开 H5 页面
*
* @param {string} url - H5 页面地址
*/
export function navigateToWebView(url) {
if (!url) return
uni.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(url)}`
})
}
/**
* 返回上一页
* @param {number} delta - 返回的页面数默认 1
*/
export function navigateBack(delta = 1) {
uni.navigateBack({ delta })
}
/**
* 重定向到指定页面关闭当前页
* @param {string} url - 页面路径
*/
export function redirectTo(url) {
if (!url) return
if (isTabBarPage(url)) {
uni.switchTab({ url })
} else {
uni.redirectTo({ url })
}
}

View File

@ -250,6 +250,36 @@ public class InteractController : ControllerBase
});
}
/// <summary>
/// 获取互动新增数量统计
/// </summary>
/// <returns>各类互动的新增数量</returns>
[HttpGet("counts")]
public async Task<ApiResponse<InteractCountsResponse>> GetInteractCounts()
{
var userId = GetCurrentUserId();
var result = await _interactService.GetInteractCountsAsync(userId);
return ApiResponse<InteractCountsResponse>.Success(result);
}
/// <summary>
/// 标记互动为已读
/// </summary>
/// <param name="type">互动类型viewedMe, favoritedMe, unlockedMe</param>
/// <returns>操作结果</returns>
[HttpPost("markRead/{type}")]
public async Task<ApiResponse<bool>> MarkInteractAsRead(string type)
{
if (string.IsNullOrWhiteSpace(type))
{
return ApiResponse<bool>.Error(ErrorCodes.InvalidParameter, "互动类型不能为空");
}
var userId = GetCurrentUserId();
await _interactService.MarkInteractAsReadAsync(userId, type);
return ApiResponse<bool>.Success(true);
}
/// <summary>
/// 获取当前用户ID
/// </summary>

View File

@ -328,3 +328,24 @@ public class UnlockRecordResponse
/// </summary>
public DateTime CreateTime { get; set; }
}
/// <summary>
/// 互动新增数量统计响应
/// </summary>
public class InteractCountsResponse
{
/// <summary>
/// "看过我"新增数量
/// </summary>
public int ViewedMeCount { get; set; }
/// <summary>
/// "收藏我"新增数量
/// </summary>
public int FavoritedMeCount { get; set; }
/// <summary>
/// "解锁我"新增数量
/// </summary>
public int UnlockedMeCount { get; set; }
}

View File

@ -86,4 +86,18 @@ public interface IInteractService
/// <param name="userId">用户ID</param>
/// <returns>剩余解锁次数</returns>
Task<int> GetRemainingUnlockQuotaAsync(long userId);
/// <summary>
/// 获取互动新增数量统计
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>各类互动的新增数量</returns>
Task<InteractCountsResponse> GetInteractCountsAsync(long userId);
/// <summary>
/// 标记互动为已读
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="type">互动类型viewedMe, favoritedMe, unlockedMe</param>
Task MarkInteractAsReadAsync(long userId, string type);
}

View File

@ -776,4 +776,72 @@ public class InteractService : IInteractService
}
#endregion
#region
/// <inheritdoc />
public async Task<InteractCountsResponse> GetInteractCountsAsync(long userId)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
{
return new InteractCountsResponse();
}
// 获取用户最后查看各类互动的时间,如果为空则使用用户创建时间
var lastViewedMeTime = user.LastViewedMeReadTime ?? user.CreateTime;
var lastFavoritedMeTime = user.LastFavoritedMeReadTime ?? user.CreateTime;
var lastUnlockedMeTime = user.LastUnlockedMeReadTime ?? user.CreateTime;
// 统计各类互动的新增数量
var viewedMeCount = await _viewRepository.CountAsync(v =>
v.TargetUserId == userId && v.LastViewTime > lastViewedMeTime);
var favoritedMeCount = await _favoriteRepository.CountAsync(f =>
f.TargetUserId == userId && f.CreateTime > lastFavoritedMeTime);
var unlockedMeCount = await _unlockRepository.CountAsync(u =>
u.TargetUserId == userId && u.CreateTime > lastUnlockedMeTime);
return new InteractCountsResponse
{
ViewedMeCount = (int)viewedMeCount,
FavoritedMeCount = (int)favoritedMeCount,
UnlockedMeCount = (int)unlockedMeCount
};
}
/// <inheritdoc />
public async Task MarkInteractAsReadAsync(long userId, string type)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
{
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
}
var now = DateTime.Now;
switch (type.ToLower())
{
case "viewedme":
user.LastViewedMeReadTime = now;
break;
case "favoritedme":
user.LastFavoritedMeReadTime = now;
break;
case "unlockedme":
user.LastUnlockedMeReadTime = now;
break;
default:
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的互动类型");
}
user.UpdateTime = now;
await _userRepository.UpdateAsync(user);
_logger.LogInformation("标记互动已读: UserId={UserId}, Type={Type}", userId, type);
}
#endregion
}

View File

@ -106,6 +106,21 @@ public class User : SoftDeleteEntity
/// </summary>
public DateTime? LastLoginTime { get; set; }
/// <summary>
/// 最后查看"看过我"的时间
/// </summary>
public DateTime? LastViewedMeReadTime { get; set; }
/// <summary>
/// 最后查看"收藏我"的时间
/// </summary>
public DateTime? LastFavoritedMeReadTime { get; set; }
/// <summary>
/// 最后查看"解锁我"的时间
/// </summary>
public DateTime? LastUnlockedMeReadTime { get; set; }
#region
/// <summary>