Files
medical-mall/services/admin/productReviewService.uts
2026-03-20 17:30:30 +08:00

171 lines
6.1 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_reviewsJOIN 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
}