feat:修改数据看板sql的数据问题
This commit is contained in:
parent
d859650136
commit
4f3bb7e3ab
@ -132,16 +132,22 @@ type TopExhibitionRow struct {
|
|||||||
|
|
||||||
func (r *DashboardRepository) GetExhibitionIncomeSummary(ctx context.Context, userID, starID int64) (*ExhibitionSummary, error) {
|
func (r *DashboardRepository) GetExhibitionIncomeSummary(ctx context.Context, userID, starID int64) (*ExhibitionSummary, error) {
|
||||||
// 简化实现:直接统计 events(生产环境用 mv_daily_exhibition_revenue)
|
// 简化实现:直接统计 events(生产环境用 mv_daily_exhibition_revenue)
|
||||||
|
// LEFT JOIN assets 取 name/cover_url;event 里 asset_id 可能为 null/非数字,需要 WHERE 防御
|
||||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
(properties->>'asset_id')::BIGINT AS asset_id,
|
(e.properties->>'asset_id')::BIGINT AS asset_id,
|
||||||
SUM((properties->>'amount')::BIGINT) AS earnings,
|
COALESCE(MIN(a.name), '') AS asset_name,
|
||||||
SUM((properties->>'duration_ms')::BIGINT) AS duration_ms
|
COALESCE(MIN(a.cover_url), '') AS asset_thumb,
|
||||||
FROM %s.events
|
SUM((e.properties->>'amount')::BIGINT) AS earnings,
|
||||||
WHERE user_id=$1 AND star_id=$2
|
SUM((e.properties->>'duration_ms')::BIGINT) AS duration_ms
|
||||||
AND event_type='exhibition.revenue'
|
FROM %s.events e
|
||||||
AND received_at >= NOW() - INTERVAL '7 days'
|
LEFT JOIN public.assets a ON a.id = (e.properties->>'asset_id')::BIGINT
|
||||||
GROUP BY asset_id
|
WHERE e.user_id=$1 AND e.star_id=$2
|
||||||
|
AND e.event_type='exhibition.revenue'
|
||||||
|
AND e.received_at >= NOW() - INTERVAL '7 days'
|
||||||
|
AND (e.properties->>'asset_id') IS NOT NULL
|
||||||
|
AND (e.properties->>'asset_id') ~ '^[0-9]+$'
|
||||||
|
GROUP BY (e.properties->>'asset_id')::BIGINT
|
||||||
ORDER BY earnings DESC
|
ORDER BY earnings DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
`, r.schema), userID, starID)
|
`, r.schema), userID, starID)
|
||||||
@ -154,7 +160,7 @@ func (r *DashboardRepository) GetExhibitionIncomeSummary(ctx context.Context, us
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var t TopExhibitionRow
|
var t TopExhibitionRow
|
||||||
var durationMs int64
|
var durationMs int64
|
||||||
if err := rows.Scan(&t.AssetID, &t.Earnings7d, &durationMs); err != nil {
|
if err := rows.Scan(&t.AssetID, &t.AssetName, &t.AssetThumb, &t.Earnings7d, &durationMs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.Duration7d = formatDuration(durationMs)
|
t.Duration7d = formatDuration(durationMs)
|
||||||
@ -166,10 +172,29 @@ func (r *DashboardRepository) GetExhibitionIncomeSummary(ctx context.Context, us
|
|||||||
top5 = append(top5, t)
|
top5 = append(top5, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简化:exhibiting_count / starbook_count 留 0
|
// 展出中:当前 active 的 exhibitions(未删除 + 未过期)
|
||||||
|
var exhibitingCount int32
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*)::INT FROM public.exhibitions
|
||||||
|
WHERE occupier_uid=$1 AND occupier_star_id=$2
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND expire_at > EXTRACT(EPOCH FROM NOW()) * 1000
|
||||||
|
`, userID, starID).Scan(&exhibitingCount); err != nil {
|
||||||
|
exhibitingCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 星册数:该用户在此 star 下启用的 booth_slots 总数
|
||||||
|
var starbookCount int32
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*)::INT FROM public.booth_slots
|
||||||
|
WHERE user_id=$1 AND star_id=$2 AND is_enabled = TRUE
|
||||||
|
`, userID, starID).Scan(&starbookCount); err != nil {
|
||||||
|
starbookCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
return &ExhibitionSummary{
|
return &ExhibitionSummary{
|
||||||
ExhibitingCount: 0,
|
ExhibitingCount: exhibitingCount,
|
||||||
StarbookCount: 0,
|
StarbookCount: starbookCount,
|
||||||
TotalDuration: formatDuration(totalDurationMs),
|
TotalDuration: formatDuration(totalDurationMs),
|
||||||
TotalEarnings: totalEarnings,
|
TotalEarnings: totalEarnings,
|
||||||
Top5: top5,
|
Top5: top5,
|
||||||
|
|||||||
@ -65,26 +65,85 @@ export function extractOssObjectPath(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 URL 是否为带 Expires 的 OSS 预签名 URL,且已过期
|
||||||
|
* @param {String} url
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isExpiredPresignedUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return false
|
||||||
|
const m = url.match(/[?&]Expires=(\d+)/)
|
||||||
|
if (!m) return false
|
||||||
|
// OSS Expires 单位是秒,Date.now() 单位是毫秒
|
||||||
|
return Number(m[1]) * 1000 < Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从完整 OSS URL 提取对象路径(去掉 ?Expires=...&OSSAccessKeyId=...&Signature=... 部分)
|
||||||
|
* 例如:https://bucket.oss-cn-shanghai.aliyuncs.com/laser-card/x.png?Expires=... → laser-card/x.png
|
||||||
|
* @param {String} url
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function extractOssObjectPathFromUrl(url) {
|
||||||
|
try {
|
||||||
|
// 去掉 query string
|
||||||
|
const pathOnly = url.split('?')[0]
|
||||||
|
// 提取 aliyuncs.com/ 之后的路径
|
||||||
|
const marker = 'aliyuncs.com/'
|
||||||
|
const idx = pathOnly.indexOf(marker)
|
||||||
|
if (idx !== -1) {
|
||||||
|
return pathOnly.substring(idx + marker.length)
|
||||||
|
}
|
||||||
|
// 兜底:去掉协议 + 域名
|
||||||
|
const protoEnd = pathOnly.indexOf('://')
|
||||||
|
const pathStart = pathOnly.indexOf('/', protoEnd + 3)
|
||||||
|
if (pathStart !== -1) {
|
||||||
|
return pathOnly.substring(pathStart + 1)
|
||||||
|
}
|
||||||
|
return pathOnly
|
||||||
|
} catch (e) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取藏品封面的真实URL - 使用 OSS 预签名 URL
|
* 获取藏品封面的真实URL - 使用 OSS 预签名 URL
|
||||||
* @param {String} coverUrl - 后端返回的cover_url(相对路径或完整URL)
|
* 处理 3 种 coverUrl 形态:
|
||||||
|
* 1) /static/... 本地静态资源 → 原样返回
|
||||||
|
* 2) 相对路径 asset/1/87/xxx.jpg → 调 getOssPresignedUrlApi 重签
|
||||||
|
* 3) 完整 OSS 预签名 URL(可能已过期) → 检测 Expires,过期则提取对象路径重签
|
||||||
|
* @param {String} coverUrl - 后端返回的cover_url
|
||||||
* @returns {Promise<String>} 真实可访问的URL
|
* @returns {Promise<String>} 真实可访问的URL
|
||||||
*/
|
*/
|
||||||
export async function getAssetCoverRealUrl(coverUrl) {
|
export async function getAssetCoverRealUrl(coverUrl) {
|
||||||
// 默认图片路径
|
// 默认图片路径
|
||||||
const DEFAULT_IMAGE = '/static/nft/collection.png';
|
const DEFAULT_IMAGE = '/static/nft/collection.png';
|
||||||
|
|
||||||
// 如果是本地静态资源或为空,直接返回
|
// 1. 本地静态资源或空 → 直接返回
|
||||||
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
if (!coverUrl || coverUrl.startsWith('/static/')) {
|
||||||
return coverUrl || DEFAULT_IMAGE;
|
return coverUrl || DEFAULT_IMAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是完整的绝对URL(包含 ://),直接返回
|
// 2. 完整 URL(带 ://)
|
||||||
if (coverUrl.includes('://')) {
|
if (coverUrl.includes('://')) {
|
||||||
return coverUrl;
|
// 检查是否带 Expires 且已过期 → 提取对象路径重签
|
||||||
|
if (isExpiredPresignedUrl(coverUrl)) {
|
||||||
|
const objectPath = extractOssObjectPathFromUrl(coverUrl)
|
||||||
|
try {
|
||||||
|
const res = await getOssPresignedUrlApi(objectPath, 3600, 'asset')
|
||||||
|
if (res && res.data && res.data.url) {
|
||||||
|
return res.data.url
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[assetImageHelper] re-sign expired url failed:', e)
|
||||||
|
}
|
||||||
|
return objectPath // 兜底:返回相对路径
|
||||||
|
}
|
||||||
|
// 未过期:原样返回
|
||||||
|
return coverUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相对路径:调用预签名接口获取可访问的URL
|
// 3. 相对路径:调预签名接口
|
||||||
try {
|
try {
|
||||||
const res = await getOssPresignedUrlApi(coverUrl, 3600, 'asset');
|
const res = await getOssPresignedUrlApi(coverUrl, 3600, 'asset');
|
||||||
if (res && res.data && res.data.url) {
|
if (res && res.data && res.data.url) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user