feat: 修改双击点赞的逻辑和处理收益记录的bug
This commit is contained in:
parent
afda66cdb9
commit
bb797ebf87
@ -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
|
||||
@ -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;
|
||||
@ -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()").
|
||||
|
||||
@ -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 // 不标记为已处理,让下次重试
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
@ -266,7 +265,7 @@ const starActivities = computed(() => {
|
||||
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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// 如果第二个参数是函数,说明调用者传的是 callback,exhibitionId 未提供
|
||||
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);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user