171 lines
6.1 KiB
Plaintext
171 lines
6.1 KiB
Plaintext
import { supabase } from '@/components/supadb/aksupainstance.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
export type ProductReviewItem = {
|
||
id: string
|
||
product_id: string
|
||
product_name: string
|
||
product_image: string | null
|
||
user_id: string
|
||
username: string | null
|
||
rating: number
|
||
content: string | null
|
||
merchant_reply: string | null
|
||
status: number
|
||
created_at: string
|
||
total_count: number
|
||
}
|
||
|
||
export type ProductReviewQuery = {
|
||
searchProduct?: string | null
|
||
searchUser?: string | null
|
||
status?: number | null
|
||
startTime?: string | null
|
||
endTime?: string | null
|
||
page?: number
|
||
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 page = query?.page ?? 1
|
||
const pageSize = query?.pageSize ?? 20
|
||
const offset = (page - 1) * pageSize
|
||
|
||
// 构建 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> {
|
||
const { error } = await supa.from('ml_product_reviews').update({ status: 1, updated_at: new Date().toISOString() }).eq('id', id).execute()
|
||
if (error != null) {
|
||
console.error('审核通过失败:', error)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export async function rejectProductReview(id: string): Promise<boolean> {
|
||
const { error } = await supa.from('ml_product_reviews').update({ status: 3, updated_at: new Date().toISOString() }).eq('id', id).execute()
|
||
if (error != null) {
|
||
console.error('审核驳回/隐藏失败:', error)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export async function replyProductReview(id: string, reply: string): Promise<boolean> {
|
||
const { error } = await supa
|
||
.from('ml_product_reviews')
|
||
.update({ merchant_reply: reply, merchant_replied_at: new Date().toISOString(), updated_at: new Date().toISOString() })
|
||
.eq('id', id)
|
||
.execute()
|
||
|
||
if (error != null) {
|
||
console.error('回复失败:', error)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export async function deleteProductReview(id: string): Promise<boolean> {
|
||
const { error } = await supa.from('ml_product_reviews').update({ status: 2, updated_at: new Date().toISOString() }).eq('id', id).execute()
|
||
if (error != null) {
|
||
console.error('删除失败:', error)
|
||
return false
|
||
}
|
||
return true
|
||
}
|