接入商品评论数据

This commit is contained in:
2026-03-20 17:30:30 +08:00
parent 620ae742df
commit 13811ae87d
14 changed files with 535 additions and 1753 deletions

View File

@@ -1,4 +1,4 @@
import { rpcOrValue } from '@/services/analytics/rpc.uts'
import { supabase } from '@/components/supadb/aksupainstance.uts'
import supa from '@/components/supadb/aksupainstance.uts'
export type ProductReviewItem = {
@@ -26,21 +26,106 @@ export type ProductReviewQuery = {
pageSize?: number
}
// 直接查询 ml_product_reviewsJOIN ml_products 和 ak_users 获取商品名/图片/用户名
// 使用 supabase.select + Content-Range 实现服务端分页,无需 RPC
export async function fetchAdminProductReviews(query?: ProductReviewQuery): Promise<{ total: number; items: Array<ProductReviewItem> }> {
const payload = {
p_search_product: query?.searchProduct ?? null,
p_search_user: query?.searchUser ?? null,
p_status: query?.status ?? null,
p_start_time: query?.startTime ?? null,
p_end_time: query?.endTime ?? null,
p_page: query?.page ?? 1,
p_page_size: query?.pageSize ?? 20
} as any
const page = query?.page ?? 1
const pageSize = query?.pageSize ?? 20
const offset = (page - 1) * pageSize
const res = await rpcOrValue('rpc_admin_get_product_reviews', payload as any)
const arr = Array.isArray(res) ? (res as Array<any>) : ([] as Array<any>)
const total = arr.length > 0 ? parseInt(String(arr[0]?.total_count ?? '0')) : 0
return { total, items: arr as Array<ProductReviewItem> }
// 构建 PostgREST filter 字符串
const filters: string[] = []
if (query?.status != null) filters.push(`status=eq.${query.status}`)
if (query?.startTime != null && query.startTime !== '') filters.push(`created_at=gte.${query.startTime}`)
if (query?.endTime != null && query.endTime !== '') filters.push(`created_at=lte.${query.endTime}`)
// offset 注入PostgREST 识别为 SQL OFFSET避免发送 Range 头)
if (offset > 0) filters.push(`offset=${offset}`)
const filterStr = filters.length > 0 ? filters.join('&') : null
// 查询评价表,并内联关联商品和用户信息
const res = await supabase.select(
'ml_product_reviews',
filterStr,
{
columns: 'id, product_id, user_id, rating, content, merchant_reply, status, created_at, product:ml_products!ml_product_reviews_product_id_fkey(name, main_image_url), reviewer:ak_users!ml_product_reviews_user_id_fkey(username)',
limit: pageSize,
order: 'created_at.desc',
count: 'exact'
}
)
if (res.status < 200 || res.status >= 300 || res.data == null) {
console.error('fetchAdminProductReviews 查询失败, status:', res.status)
return { total: 0, items: [] as Array<ProductReviewItem> }
}
// 从 Content-Range 解析总行数
let totalCount = 0
const hdrs = res.headers
if (hdrs != null) {
let cr: string | null = null
if (typeof (hdrs as any).get === 'function') {
cr = (hdrs as any).get('content-range') as string | null
}
if (cr == null) cr = (hdrs as UTSJSONObject)['content-range'] as string | null
if (cr != null) {
const m = /\/(\d+)$/.exec(cr)
if (m != null) totalCount = parseInt(m[1] ?? '0')
}
}
if (totalCount === 0) {
totalCount = offset + (Array.isArray(res.data) ? (res.data as any[]).length : 0)
}
// 字段映射:把 JOIN 结果摊平为 ProductReviewItem
const rawRows = res.data as any[]
const items: Array<ProductReviewItem> = []
// 客户端 searchProduct / searchUser 过滤PostgREST embedded filter 兼容性问题时的兜底)
const spLower = (query?.searchProduct ?? '').toLowerCase()
const suLower = (query?.searchUser ?? '').toLowerCase()
for (let i = 0; i < rawRows.length; i++) {
const row = rawRows[i] as UTSJSONObject
const productRaw = row.get('product')
const reviewerRaw = row.get('reviewer')
let productName = ''
let productImage: string | null = null
if (productRaw != null) {
const p = (productRaw instanceof UTSJSONObject ? productRaw : JSON.parse(JSON.stringify(productRaw))) as UTSJSONObject
productName = p.getString('name') ?? ''
productImage = p.getString('main_image_url') ?? null
}
let username: string | null = null
if (reviewerRaw != null) {
const u = (reviewerRaw instanceof UTSJSONObject ? reviewerRaw : JSON.parse(JSON.stringify(reviewerRaw))) as UTSJSONObject
username = u.getString('username') ?? null
}
// 客户端过滤searchProduct / searchUser
if (spLower !== '' && !productName.toLowerCase().includes(spLower)) continue
if (suLower !== '' && (username ?? '').toLowerCase().includes(suLower) === false) continue
items.push({
id: row.getString('id') ?? '',
product_id: row.getString('product_id') ?? '',
product_name: productName,
product_image: productImage,
user_id: row.getString('user_id') ?? '',
username: username,
rating: row.getNumber('rating') ?? 0,
content: row.getString('content') ?? null,
merchant_reply: row.getString('merchant_reply') ?? null,
status: row.getNumber('status') ?? 1,
created_at: row.getString('created_at') ?? '',
total_count: totalCount
} as ProductReviewItem)
}
return { total: totalCount, items }
}
export async function approveProductReview(id: string): Promise<boolean> {