接入商品评论数据
This commit is contained in:
@@ -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_reviews,JOIN 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> {
|
||||
|
||||
Reference in New Issue
Block a user