feat: 修改双击点赞的逻辑和处理收益记录的bug

This commit is contained in:
zerosaturation 2026-05-21 10:45:47 +08:00
parent afda66cdb9
commit bb797ebf87
9 changed files with 177 additions and 80 deletions

View File

@ -0,0 +1,19 @@
-- Migration: Reset incorrectly processed exhibitions
-- Description: Reset is_processed=true for exhibitions that have no revenue record
-- (due to bug where OnExhibitionCompleted failure still marked as processed)
-- Date: 2026-05-20
-- Reset exhibitions that are marked as processed but have no revenue record
-- These exhibitions need to be reprocessed to generate revenue records
-- Note: expire_at is stored in milliseconds, so we multiply by 1000
UPDATE exhibitions e
SET is_processed = false
WHERE e.is_processed = true
AND e.deleted_at IS NULL
AND e.expire_at <= EXTRACT(EPOCH FROM NOW()) * 1000 -- expired exhibitions (milliseconds)
AND NOT EXISTS (
SELECT 1 FROM exhibition_revenue_records r
WHERE r.exhibition_id = e.id
);
-- Note: Run this manually via psql or apply via migrate tool

View File

@ -0,0 +1,29 @@
-- Check how many exhibitions were incorrectly marked as processed
-- These are exhibitions that:
-- 1. is_processed = true
-- 2. not deleted (deleted_at IS NULL)
-- 3. expired (expire_at <= now in milliseconds)
-- 4. have no revenue record
SELECT
COUNT(*) as incorrectly_processed_count
FROM exhibitions e
WHERE e.is_processed = true
AND e.deleted_at IS NULL
AND e.expire_at <= EXTRACT(EPOCH FROM NOW()) * 1000
AND NOT EXISTS (
SELECT 1 FROM exhibition_revenue_records r
WHERE r.exhibition_id = e.id
);
-- List the affected exhibitions (optional, for debugging)
-- SELECT e.id, e.asset_id, e.slot_id, e.expire_at, e.is_processed
-- FROM exhibitions e
-- WHERE e.is_processed = true
-- AND e.deleted_at IS NULL
-- AND e.expire_at <= EXTRACT(EPOCH FROM NOW()) * 1000
-- AND NOT EXISTS (
-- SELECT 1 FROM exhibition_revenue_records r
-- WHERE r.exhibition_id = e.id
-- )
-- LIMIT 100;

View File

@ -82,6 +82,7 @@ type GalleryRepository interface {
// InspirationFlowItem 灵感瀑布展品项
type InspirationFlowItem struct {
ExhibitionID int64
AssetID int64
Name string
CoverURL string
@ -95,6 +96,7 @@ type InspirationFlowItem struct {
// ExhibitedAssetInfo 我展出的作品信息
type ExhibitedAssetInfo struct {
AssetID int64
ExhibitionID int64
Name string
CoverURL string
LikeCount int32
@ -429,7 +431,7 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
SELECT exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
(a.tags @> '["craft:lenticular"]') as is_lenticular
FROM exhibitions
@ -475,7 +477,7 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
SELECT exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
(a.tags @> '["craft:lenticular"]') as is_lenticular
FROM exhibitions
@ -542,7 +544,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
var err error
if materialType == "" || materialType == "all" || materialType == "random" {
err = baseQuery.
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
Where("a.status = 1 AND a.is_active = true").
@ -553,7 +555,7 @@ func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType stri
} else {
// baseQuery 已经包含了 assets JOIN不需要重复添加
err = baseQuery.
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Select(`exhibitions.id as exhibition_id, exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname, a.material_type, a.created_at`).
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
Where("a.status = 1 AND a.is_active = true").
Order("RANDOM()").

View File

@ -134,10 +134,11 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
CrystalAmount: revenue,
})
if err != nil {
logger.Logger.Error("调用TaskService记录收益失败",
logger.Logger.Error("调用TaskService记录收益失败,跳过标记已处理以便重试",
zap.Int64("exhibition_id", e.ID),
zap.Error(err))
// 不阻断主流程
failedCount++
continue // 不标记为已处理,让下次重试
}
}

View File

@ -109,9 +109,9 @@
</view>
<!-- 创作者信息模块 -->
<!-- <view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true"> -->
<view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true">
<!-- 点赞用户头像区域 -->
<!-- <view class="liked-users-area">
<view class="liked-users-area">
<view v-for="(user, index) in likedUsers" :key="index" class="liked-user-avatar" :style="{
transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
@ -126,8 +126,8 @@
<image class="creator-preview" src="/static/rank/activity-support-icon/tubiao.png"
mode="aspectFill"></image>
</view>
</view> -->
<!-- </view> -->
</view>
</view>
<!-- 完整链上哈希显示遮罩层 -->
<view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false">

View File

@ -37,15 +37,12 @@
<!-- 星援活动列表 -->
<view v-if="showStarActivityIcon" class="star-activity-list">
<view
v-for="activity in starActivities"
:key="activity.id"
class="daily-task-group"
@click="handleActivityClick(activity)"
>
<view v-for="activity in starActivities" :key="activity.id" class="daily-task-group"
@click="handleActivityClick(activity)">
<!-- 上层活动图标 -->
<view class="task-icon-box">
<image class="task-icon-img" :src="activity.icon || '/static/icon/bus-icon.png'" mode="aspectFit"></image>
<image class="task-icon-img" :src="activity.icon || '/static/icon/bus-icon.png'"
mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
@ -65,7 +62,8 @@
<!-- 2. 右侧容器用于包裹背景和文字 -->
<view class="crystal-info-container">
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill"></image>
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill">
</image>
<!-- 上层文字内容 -->
<view class="crystal-text-layer">
@ -97,6 +95,7 @@
<script setup>
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { useStore } from 'vuex';
import Avatar from './Avatar.vue';
import DailyTasks from '@/pages/tasks/daily-tasks.vue';
@ -238,7 +237,7 @@ function checkAndReportDailyLogin() {
try {
const user = JSON.parse(userStr)
userId = user?.uid || null
} catch (e) {}
} catch (e) { }
}
if (!userId) return
@ -263,10 +262,10 @@ const starActivities = computed(() => {
});
//
onMounted(async() => {
onMounted(async () => {
await loadUserInfo();
await loadBannerActivities();
await loadEarningsSummary();
// await loadEarningsSummary();
uni.$on('avatarUpdated', handleAvatarUpdate);
uni.$on('userInfoUpdated', handleUserInfoUpdate);
uni.$on('balanceUpdated', handleBalanceUpdate);
@ -275,6 +274,11 @@ onMounted(async() => {
checkAndReportDailyLogin();
});
onShow(() => {
//
loadEarningsSummary();
});
//
onUnmounted(() => {
uni.$off('avatarUpdated', handleAvatarUpdate);
@ -602,8 +606,7 @@ defineExpose({
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E7399 100%
);
#B94E7399 100%);
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),

View File

@ -415,7 +415,7 @@ const handleExhibitionCardTap = (item, index) => {
//
clearTimeout(cardTapTimers[item.id]);
delete cardTapTimers[item.id];
doubleTapLike(item.id, async (success, data) => {
doubleTapLike(item.id, item.exhibition_id, async (success, data) => {
if (success) {
//
// exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1;
@ -817,6 +817,7 @@ const loadExhibitedAssets = async () => {
exhibitionWorks.value = res.data.items
.map(item => ({
id: item.asset_id,
exhibition_id: item.exhibition_id,
cover_url: item.cover_url,
like_count: item.like_count,
earnings: item.earnings,

View File

@ -633,6 +633,7 @@ const loadUsers = async () => {
const withData = items.map((item) => {
return {
id: item.asset_id,
exhibition_id: item.exhibition_id,
userId: item.asset_id,
nickname: item.owner_nickname || item.name,
coverUrl: item.cover_url || MOCK_IMAGES[idCounter % MOCK_IMAGES.length],
@ -759,7 +760,7 @@ const handleCardClick = (card) => {
likingMap.value = { ...likingMap.value, [card.id]: false };
}, 600);
doubleTapLike(card.id, (success) => {
doubleTapLike(card.id, card.exhibition_id || 0, (success) => {
if (success) {
// cards likes
const idx = cards.value.findIndex(c => c.id === card.id)

View File

@ -2,86 +2,127 @@
import { likeAssetApi } from './api.js';
// 存储已点赞的作品key: assetId, value: 点赞日期 (YYYY-MM-DD)
const LIKE_STORAGE_KEY = 'liked_assets_daily';
// 存储已点赞的作品
// - 带 exhibitionId 时key = "${assetId}_${exhibitionId}",支持每次展示点赞一次
// - 不带 exhibitionId 时key = "asset_${assetId}",兼容旧行为(每天每个作品只能点赞一次)
const LIKE_STORAGE_KEY = 'liked_assets_exhibition';
/**
* 获取今天日期字符串
* 获取存储键
* @param {string|number} assetId
* @param {string|number} exhibitionId
*/
function getTodayStr() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
function getStorageKey(assetId, exhibitionId) {
if (exhibitionId) {
return `${assetId}_${exhibitionId}`;
}
return `asset_${assetId}`;
}
/**
* 检查作品今天是否已点赞
* 双击点赞处理
* - exhibitionId 每个展品每次展示只能点赞一次
* - exhibitionId 每个藏品每天只能点赞一次兼容旧逻辑
* @param {string|number} assetId - 藏品ID
* @param {string|number} [exhibitionId] - 展品展示ID每次展示唯一可选
* @param {Function} [callback] - 回调函数参数为是否成功
*/
function hasLikedToday(assetId) {
export function doubleTapLike(assetId, exhibitionId, callback) {
if (!assetId) {
console.error('doubleTapLike: assetId 是必需的');
if (callback) callback(false);
return;
}
// 如果第二个参数是函数,说明调用者传的是 callbackexhibitionId 未提供
let actualCallback = callback;
let actualExhibitionId = exhibitionId;
if (typeof exhibitionId === 'function') {
actualCallback = exhibitionId;
actualExhibitionId = null;
}
const key = getStorageKey(assetId, actualExhibitionId);
try {
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
const today = getTodayStr();
return storage[assetId] === today;
if (storage[key]) {
const msg = actualExhibitionId ? '本次展示已点赞' : '今日已点赞';
uni.showToast({ title: msg, icon: 'none' });
if (actualCallback) actualCallback(false);
return;
}
} catch (e) {
console.error('读取点赞记录失败:', e);
}
likeAssetApi(assetId).then(res => {
console.log('点赞成功', res);
// 记录点赞
try {
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
storage[key] = Date.now();
uni.setStorageSync(LIKE_STORAGE_KEY, storage);
} catch (e) {
console.error('存储点赞记录失败:', e);
}
// 触发全局点赞成功事件
uni.$emit('assetLiked', {
asset_id: assetId,
exhibition_id: actualExhibitionId,
data: res.data
});
if (actualCallback) actualCallback(true, res.data);
}).catch(err => {
console.error('点赞失败:', err);
uni.showToast({ title: '点赞失败', icon: 'none' });
if (actualCallback) actualCallback(false);
});
}
/**
* 检查藏品在当前展示中是否已点赞
* @param {string|number} assetId - 藏品ID
* @param {string|number} [exhibitionId] - 展品展示ID
*/
export function hasLikedExhibition(assetId, exhibitionId) {
if (!assetId) return false;
const key = getStorageKey(assetId, exhibitionId);
try {
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
return !!storage[key];
} catch (e) {
return false;
}
}
/**
* 记录作品今天已点赞
* 清除指定展览的点赞记录用于展品下架时
* @param {string|number} exhibitionId - 展品展示ID
*/
function markLikedToday(assetId) {
export function clearLikeByExhibition(exhibitionId) {
if (!exhibitionId) return;
try {
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
storage[assetId] = getTodayStr();
uni.setStorageSync(LIKE_STORAGE_KEY, storage);
} catch (e) {
console.error('存储点赞记录失败:', e);
}
}
/**
* 清除过期的点赞记录昨天及之前
*/
function cleanExpiredLikes() {
try {
const storage = uni.getStorageSync(LIKE_STORAGE_KEY) || {};
const today = getTodayStr();
const keys = Object.keys(storage);
keys.forEach(key => {
if (storage[key] !== today) {
if (key.endsWith(`_${exhibitionId}`)) {
delete storage[key];
}
});
uni.setStorageSync(LIKE_STORAGE_KEY, storage);
} catch (e) {
console.error('清除过期点赞记录失败:', e);
console.error('清除点赞记录失败:', e);
}
}
/**
* 双击点赞处理每天每个作品只能点赞一次
* @param {string|number} assetId - 藏品ID
* @param {Function} callback - 回调函数参数为是否成功
* 清除所有点赞记录
*/
export function doubleTapLike(assetId, callback) {
// 清理过期记录
cleanExpiredLikes();
// 检查今天是否已点赞
if (hasLikedToday(assetId)) {
uni.showToast({ title: '今日已点赞', icon: 'none' });
return;
export function clearAllLikes() {
try {
uni.removeStorageSync(LIKE_STORAGE_KEY);
} catch (e) {
console.error('清除所有点赞记录失败:', e);
}
likeAssetApi(assetId).then(res => {
console.log('点赞成功', res);
markLikedToday(assetId);
// 触发全局点赞成功事件
uni.$emit('assetLiked', { asset_id: assetId, data: res.data });
if (callback) callback(true, res.data);
}).catch(err => {
console.error('点赞失败:', err);
uni.showToast({ title: '今日已点赞', icon: 'none' });
if (callback) callback(false);
});
}