Files
medical-mall/utils/supabaseService.uts

2131 lines
66 KiB
Plaintext
Raw 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 supa from '@/components/supadb/aksupainstance.uts'
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
// 使用单例 Supabase 客户端
// const supa = createClient(SUPA_URL, SUPA_KEY)
// 类型定义
export interface Category {
id: string
name: string
icon: string
description: string
color: string
created_at?: string
}
export interface Product {
id: string
category_id: string
merchant_id: string
name: string
subtitle?: string
description?: string
base_price: number
market_price?: number
cost_price?: number
main_image_url?: string
image_urls?: string // JSON string array
video_urls?: string // JSON string array
sale_count?: number
view_count?: number
total_stock?: number
available_stock?: number
is_hot?: boolean
is_new?: boolean
is_featured?: boolean
status?: number
rating_avg?: number
rating_count?: number
tags?: string // array string in DB
attributes?: string // JSON string
created_at?: string
updated_at?: string
// View fields
brand_name?: string
category_name?: string
shop_name?: string
merchant_name?: string
}
export interface Shop {
id: string
merchant_id: string
shop_name: string
shop_logo?: string
shop_banner?: string
description?: string
contact_name?: string
contact_phone?: string
rating_avg?: number
total_sales?: number
product_count?: number
total_sales_count?: number
created_at?: string
}
export interface CartItem {
id: string
user_id: string
product_id: string
sku_id?: string
merchant_id?: string
quantity: number
selected: boolean
product_name?: string
product_image?: string
product_price?: number
product_specification?: string
shop_id?: string
shop_name?: string
created_at?: string
updated_at?: string
}
export interface UserAddress {
id: string
user_id: string
recipient_name: string
phone: string
province: string
city: string
district: string
detail_address: string
postal_code?: string
is_default: boolean
created_at?: string
updated_at?: string
}
export interface Notification {
id: string
user_id: string
type: string
title: string
content: string
icon_url?: string
link_url?: string
is_read: boolean
extra_data?: string
created_at?: string
}
export interface ChatMessage {
id: string
session_id?: string
sender_id?: string
receiver_id?: string
content: string
msg_type: string
is_read: boolean
is_from_user: boolean
extra_data?: string
created_at?: string
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
hasmore: boolean
}
export interface ProductSku {
id: string
product_id: string
sku_code: string
specifications: string // JSON string
price: number
market_price?: number
cost_price?: number
stock?: number
warning_stock?: number
image_url?: string
weight?: number
status?: number
created_at?: string
}
class SupabaseService {
// 获取当前用户ID
public getCurrentUserId(): string | null {
try {
// 优先从 Supabase 会话获取
const session = supa.getSession()
if (session && session.user) {
return session.user.getString('id')
}
// 后备:尝试从本地存储获取 (兼容旧逻辑)
const userId = uni.getStorageSync('user_id')
return userId ? userId as string : null
} catch (e) {
console.error('获取用户ID失败:', e)
return null
}
}
// 获取所有分类
async getCategories(): Promise<Category[]> {
try {
const response = await supa
.from('ml_categories')
.select('*')
.order('name', { ascending: true })
.execute()
if (response.error) {
console.error('获取分类失败:', response.error)
return []
}
return response.data as Category[]
} catch (error) {
console.error('获取分类异常:', error)
return []
}
}
// 获取指定分类的商品
async getProductsByCategory(
categoryId: string,
page: number = 1,
limit: number = 20
): Promise<PaginatedResponse<Product>> {
try {
const response = await supa
.from('ml_products_detail_view')
.select('*', { count: 'exact' })
.eq('category_id', categoryId)
.eq('status', 1)
.order('sale_count', { ascending: false })
.page(page)
.limit(limit)
.execute()
if (response.error) {
console.error('获取商品失败:', response.error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
return {
data: response.data as Product[],
total: response.total || 0,
page,
limit,
hasmore: response.hasmore || false
}
} catch (error) {
console.error('获取商品异常:', error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
}
// 根据商品ID获取SKU列表
async getProductSkus(productId: string): Promise<ProductSku[]> {
try {
const response = await supa
.from('ml_product_skus')
.select('*')
.eq('product_id', productId)
.eq('status', 1)
.execute()
if (response.error) {
console.error('获取商品SKU失败:', response.error)
return []
}
return response.data as ProductSku[]
} catch (error) {
console.error('获取商品SKU异常:', error)
return []
}
}
// 搜索商品
async searchProducts(
keyword: string,
page: number = 1,
limit: number = 20,
sortBy: string = 'sales',
ascending: boolean = false
): Promise<PaginatedResponse<Product>> {
try {
let query = supa
.from('ml_products_detail_view')
.select('*', { count: 'exact' })
.eq('status', 1)
.or(`name.ilike.%${keyword}%,description.ilike.%${keyword}%,subtitle.ilike.%${keyword}%,brand_name.ilike.%${keyword}%`)
// 根据sortBy和ascending设置排序
if (sortBy === 'price') {
query = query.order('base_price', { ascending })
} else if (sortBy === 'sales' || sortBy === 'sale_count') {
query = query.order('sale_count', { ascending: false }) // 销量总是降序
} else {
// 默认按销量降序
query = query.order('sale_count', { ascending: false })
}
const response = await query
.page(page)
.limit(limit)
.execute()
if (response.error) {
console.error('搜索商品失败:', response.error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
return {
data: response.data as Product[],
total: response.total || 0,
page,
limit,
hasmore: response.hasmore || false
}
} catch (error) {
console.error('搜索商品异常:', error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
}
// 获取单个商品详情
async getProductById(productId: string): Promise<Product | null> {
try {
const response = await supa
.from('ml_products_detail_view')
.select('*')
.eq('id', productId)
.single()
.executeAs<Product>()
if (response.error) {
console.error('获取商品详情失败:', response.error)
return null
}
return response.data as Product
} catch (error) {
console.error('获取商品详情异常:', error)
return null
}
}
// 根据商户ID获取店铺信息
async getShopByMerchantId(merchantId: string): Promise<Shop | null> {
try {
const response = await supa
.from('ml_shops')
.select('*')
.eq('merchant_id', merchantId)
.single()
.executeAs<Shop>()
if (response.error) {
console.error('获取店铺信息失败:', response.error)
return null
}
const data = response.data
if (Array.isArray(data)) {
if (data.length > 0) return data[0] as Shop
return null
}
return data as Shop
} catch (error) {
console.error('获取店铺信息异常:', error)
return null
}
}
// 根据商户ID获取商品列表
async getProductsByMerchantId(merchantId: string, page: number = 1, limit: number = 20): Promise<PaginatedResponse<Product>> {
try {
const response = await supa
.from('ml_products')
.select('*', { count: 'exact' })
.eq('merchant_id', merchantId)
.order('created_at', { ascending: false })
.page(page)
.limit(limit)
.execute()
if (response.error) {
console.error('获取商户商品失败:', response.error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
return {
data: response.data as Product[],
total: response.total || 0,
page,
limit,
hasmore: response.hasmore || false
}
} catch (error) {
console.error('获取商户商品异常:', error)
return {
data: [],
total: 0,
page,
limit,
hasmore: false
}
}
}
// 获取热销商品(按销量排序)
async getHotProducts(limit: number = 10): Promise<Product[]> {
try {
const response = await supa
.from('ml_products_detail_view')
.select('*')
.eq('is_hot', true)
.eq('status', 1)
.order('sale_count', { ascending: false })
.limit(limit)
.execute()
if (response.error) {
console.error('获取热销商品失败:', response.error)
return []
}
return response.data as Product[]
} catch (error) {
console.error('获取热销商品异常:', error)
return []
}
}
// 获取按价格排序的商品(升序:从低到高)
async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise<Product[]> {
try {
const response = await supa
.from('ml_products_detail_view')
.select('*')
.eq('status', 1)
.order('base_price', { ascending })
.limit(limit)
.execute()
if (response.error) {
console.error('获取价格排序商品失败:', response.error)
return []
}
return response.data as Product[]
} catch (error) {
console.error('获取价格排序商品异常:', error)
return []
}
}
// 获取新品(按创建时间排序,最新的在前)
async getProductsByNewest(limit: number = 10): Promise<Product[]> {
try {
const response = await supa
.from('ml_products_detail_view')
.select('*')
.eq('is_new', true)
.eq('status', 1)
.order('published_at', { ascending: false }) // Use published_at for newest
.limit(limit)
.execute()
if (response.error) {
console.error('获取新品失败:', response.error)
return []
}
return response.data as Product[]
} catch (error) {
console.error('获取新品异常:', error)
return []
}
}
// 获取推荐商品is_featured=true
async getRecommendedProducts(limit: number = 10): Promise<Product[]> {
try {
// 查询 is_featured = true 的商品
const response = await supa
.from('ml_products_detail_view')
.select('*')
.eq('is_featured', true)
.eq('status', 1)
.order('sale_count', { ascending: false })
.limit(limit)
.execute()
if (response.error) {
console.error('获取推荐商品失败:', response.error)
return []
}
console.log('推荐商品查询结果条数:', response.data?.length || 0)
return response.data as Product[] || []
} catch (error) {
console.error('获取推荐商品异常:', error)
return []
}
}
// 获取特价商品这里假设没有specific flag, just use logic or tag if exists, defaulting to hot for now or just skip
// Modify to use compatible logic if badge column doesn't exist
async getDiscountProducts(limit: number = 10): Promise<Product[]> {
return [] // 暂无特价字段
}
// 获取当前用户的购物车商品(关联商品和店铺信息)
async getCartItems(): Promise<CartItem[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.warn('用户未登录,无法获取购物车')
return []
}
// 查询购物车表,并关联商品表(使用内联关联)
// 注意:使用 !inner 进行内连接,或者 left join (默认)
// 修改查询语法以符合 PostgREST 规范
// 尝试简化查询,先只查购物车,再查商品,避免复杂的嵌套查询导致 400 错误
const response = await supa
.from('ml_shopping_cart')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.execute()
if (response.error) {
console.error('获取购物车失败:', response.error)
return []
}
const cartData = response.data as any[]
// console.log('Raw Cart Data:', JSON.stringify(cartData))
if (!cartData || cartData.length === 0) {
return []
}
// 收集所有 product_id 和 sku_id
const productIds: string[] = []
const skuIds: string[] = []
for (let i = 0; i < cartData.length; i++) {
let item = cartData[i]
let pid: string = ''
let sid: string = ''
if (item instanceof UTSJSONObject) {
pid = item.getString('product_id') || ''
sid = item.getString('sku_id') || ''
} else {
pid = (item['product_id'] as string) || ''
sid = (item['sku_id'] as string) || ''
}
if (pid !== '' && !productIds.includes(pid)) {
productIds.push(pid)
}
if (sid !== '' && !skuIds.includes(sid)) {
skuIds.push(sid)
}
}
// 批量查询商品详情 (使用视图关联店铺信息,修复字段名 specification -> attributes)
const productMap = new Map<string, any>()
if (productIds.length > 0) {
const productRes = await supa
.from('ml_products_detail_view')
.select('id,name,main_image_url,base_price,attributes,merchant_id,shop_name')
.in('id', productIds)
.execute()
if (!productRes.error && productRes.data != null) {
const products = productRes.data as any[]
for (let i = 0; i < products.length; i++) {
let p = products[i]
let pid: string = ''
if (p instanceof UTSJSONObject) {
pid = p.getString('id') || ''
} else {
pid = (p['id'] as string) || ''
}
if (pid !== '') {
productMap.set(pid, p)
}
}
}
}
// 批量查询 SKU 详情
const skuMap = new Map<string, any>()
if (skuIds.length > 0) {
const skuRes = await supa
.from('ml_product_skus')
.select('id, specifications, price, image_url')
.in('id', skuIds)
.execute()
if (!skuRes.error && skuRes.data != null) {
const skus = skuRes.data as any[]
for (let i = 0; i < skus.length; i++) {
let s = skus[i]
let sid: string = ''
if (s instanceof UTSJSONObject) {
sid = s.getString('id') || ''
} else {
sid = (s['id'] as string) || ''
}
if (sid !== '') {
skuMap.set(sid, s)
}
}
}
}
// 处理返回数据构建CartItem数组
const cartItems: CartItem[] = []
if (cartData && Array.isArray(cartData)) {
for (let i = 0; i < cartData.length; i++) {
let item = cartData[i]
let itemId: string = ''
let userIdVal: string = ''
let productId: string = ''
let skuId: string = ''
let quantity: number = 0
let selected: boolean = false
let createdAt: string = ''
let updatedAt: string = ''
if (item instanceof UTSJSONObject) {
itemId = item.getString('id') || ''
userIdVal = item.getString('user_id') || ''
productId = item.getString('product_id') || ''
skuId = item.getString('sku_id') || ''
quantity = item.getNumber('quantity') || 0
selected = item.getBoolean('selected') || false
createdAt = item.getString('created_at') || ''
updatedAt = item.getString('updated_at') || ''
} else {
itemId = (item['id'] as string) || ''
userIdVal = (item['user_id'] as string) || ''
productId = (item['product_id'] as string) || ''
skuId = (item['sku_id'] as string) || ''
quantity = (item['quantity'] as number) || 0
selected = (item['selected'] as boolean) || false
createdAt = (item['created_at'] as string) || ''
updatedAt = (item['updated_at'] as string) || ''
}
const product = productMap.get(productId)
const sku = (skuId !== '' && skuMap.has(skuId)) ? skuMap.get(skuId) : null
let merchantId: string = ''
let productName: string = ''
let productImage: string = ''
let productPrice: number = 0
let productSpec: string = ''
let shopNameStr: string = '未知店铺'
if (product != null) {
if (product instanceof UTSJSONObject) {
merchantId = product.getString('merchant_id') || ''
productName = product.getString('name') || ''
productImage = product.getString('main_image_url') || ''
productPrice = product.getNumber('base_price') || 0
shopNameStr = product.getString('shop_name') || '未知店铺'
// 只有当没有sku信息时才尝试使用商品的attributes作为降级显示
// 或者如果业务需求是属性和规格都显示,可以修改这里
// 但通常SKU规格更具体。如果sku为空再显示product的attributes
if (sku == null) {
const specRaw = product.get('attributes')
if (specRaw != null) {
if (typeof specRaw === 'string') {
productSpec = specRaw
} else if (specRaw instanceof UTSJSONObject) {
const keys = UTSJSONObject.keys(specRaw)
const parts: string[] = []
for(let k = 0; k < keys.length; k++) {
let val = specRaw.get(keys[k])
if (val != null) {
parts.push(`${keys[k]}: ${val}`)
}
}
productSpec = parts.join('; ')
} else {
try {
let jsonStr = JSON.stringify(specRaw)
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, '; ')
} catch (e) {}
}
}
}
} else {
merchantId = (product['merchant_id'] as string) || ''
productName = (product['name'] as string) || ''
productImage = (product['main_image_url'] as string) || ''
productPrice = (product['base_price'] as number) || 0
shopNameStr = (product['shop_name'] as string) || '未知店铺'
if (sku == null) {
const specRaw = product['attributes']
if (specRaw != null) {
if (typeof specRaw === 'string') {
productSpec = specRaw
} else if (specRaw instanceof UTSJSONObject) {
const keys = UTSJSONObject.keys(specRaw)
const parts: string[] = []
for(let k = 0; k < keys.length; k++) {
let val = specRaw.get(keys[k])
if (val != null) {
parts.push(`${keys[k]}: ${val}`)
}
}
productSpec = parts.join('; ')
} else {
try {
let jsonStr = JSON.stringify(specRaw)
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, '; ')
} catch (e) {}
}
}
}
}
}
// 如果有SKU信息覆盖价格、图片和规格
if (sku != null) {
if (sku instanceof UTSJSONObject) {
const skuPrice = sku.getNumber('price')
if (skuPrice != null && skuPrice > 0) {
productPrice = skuPrice
}
const skuImg = sku.getString('image_url')
if (skuImg != null && skuImg !== '') {
productImage = skuImg
}
const specRaw = sku.get('specifications')
if (specRaw != null) {
// 优先使用SKU的规格
if (typeof specRaw === 'string') {
productSpec = specRaw
} else if (specRaw instanceof UTSJSONObject) {
const keys = UTSJSONObject.keys(specRaw)
const parts: string[] = []
for(let k = 0; k < keys.length; k++) {
let val = specRaw.get(keys[k])
if (val != null) {
parts.push(`${keys[k]}: ${val}`)
}
}
productSpec = parts.join('; ')
} else {
try {
let jsonStr = JSON.stringify(specRaw)
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, '; ')
} catch (e) {}
}
}
} else {
const skuPrice = sku['price'] as number
if (skuPrice > 0) productPrice = skuPrice
const skuImg = sku['image_url'] as string
if (skuImg && skuImg !== '') productImage = skuImg
const specRaw = sku['specifications']
if (specRaw != null) {
// 优先使用SKU的规格
if (typeof specRaw === 'string') {
productSpec = specRaw
} else if (specRaw instanceof UTSJSONObject) {
const keys = UTSJSONObject.keys(specRaw)
const parts: string[] = []
for(let k = 0; k < keys.length; k++) {
let val = specRaw.get(keys[k])
if (val != null) {
parts.push(`${keys[k]}: ${val}`)
}
}
productSpec = parts.join('; ')
} else {
try {
let jsonStr = JSON.stringify(specRaw)
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, '; ')
} catch (e) {}
}
}
}
}
let shopIdStr = merchantId || 'unknown_shop'
cartItems.push({
id: itemId,
user_id: userIdVal,
product_id: productId,
sku_id: skuId,
merchant_id: merchantId,
quantity: quantity,
selected: selected,
product_name: productName,
product_image: productImage,
product_price: productPrice,
product_specification: productSpec,
shop_id: shopIdStr,
shop_name: shopNameStr,
created_at: createdAt,
updated_at: updatedAt
})
}
}
return cartItems
} catch (error) {
console.error('获取购物车异常:', error)
return []
}
}
// 获取用户通知 (系统、活动、订单)
async getUserNotifications(type: string | null = null): Promise<Notification[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
let query = supa
.from('ml_notifications')
.select('*')
.eq('user_id', userId)
if (type) {
query = query.eq('type', type)
}
const response = await query.order('created_at', { ascending: false }).execute()
if (response.error) {
console.error('获取通知失败:', response.error)
return []
}
return response.data as Notification[]
} catch (e) {
console.error('获取通知异常:', e)
return []
}
}
// 获取用户聊天消息
async getUserChatMessages(): Promise<ChatMessage[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
const response = await supa
.from('ml_chat_messages')
.select('*')
.or(`sender_id.eq.${userId},receiver_id.eq.${userId}`)
.order('created_at', { ascending: false })
.limit(50)
.execute()
if (response.error) {
console.error('获取聊天记录失败:', response.error)
return []
}
return response.data as ChatMessage[]
} catch (e) {
console.error('获取聊天记录异常:', e)
return []
}
}
// 添加商品到购物车
async addToCart(productId: string, quantity: number = 1, skuId?: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法添加商品到购物车')
return false
}
// 检查商品是否已在购物车中
// 注意:必须处理 sku_id 为空的情况,使用 is.null 过滤器
let query = supa
.from('ml_shopping_cart')
.select('*')
.eq('user_id', userId)
.eq('product_id', productId)
if (skuId && skuId.length > 0) {
query = query.eq('sku_id', skuId)
} else {
query = query.is('sku_id', null)
}
const existingResponse = await query.single().execute()
let existingItem: any | null = null
if (existingResponse.data != null) {
const rawData = existingResponse.data as any
if (Array.isArray(rawData)) {
if (rawData.length > 0) {
existingItem = rawData[0]
}
} else {
existingItem = rawData
}
}
let response
if (existingItem != null) {
// 商品已存在,更新数量
console.log('Found existing cart item:', JSON.stringify(existingItem))
// 确保 existingItem.id 存在
const itemId = existingItem['id']
const itemQty = existingItem['quantity']
if (itemId != null) {
const currentQty = typeof itemQty === 'number' ? itemQty : parseInt(String(itemQty || 0))
const newQty = currentQty + quantity
response = await supa
.from('ml_shopping_cart')
.update({
quantity: newQty,
updated_at: new Date().toISOString()
})
.eq('id', itemId)
.execute()
} else {
console.error('购物车已有商品但缺少ID无法更新. Data:', JSON.stringify(existingItem))
return false
}
} else {
// 商品不存在,添加新记录
response = await supa
.from('ml_shopping_cart')
.insert({
user_id: userId,
product_id: productId,
sku_id: skuId || null,
quantity: quantity,
selected: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.execute()
}
if (response.error) {
console.error('添加商品到购物车失败:', response.error)
return false
}
return true
} catch (error) {
console.error('添加商品到购物车异常:', error)
return false
}
}
// 更新购物车商品数量
async updateCartItemQuantity(cartItemId: string, quantity: number): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法更新购物车')
return false
}
if (quantity < 1) {
// 数量小于1时删除商品
return await this.deleteCartItem(cartItemId)
}
const response = await supa
.from('ml_shopping_cart')
.update({
quantity: quantity,
updated_at: new Date().toISOString()
})
.eq('id', cartItemId)
.eq('user_id', userId)
.execute()
if (response.error) {
console.error('更新购物车商品数量失败:', response.error)
return false
}
return true
} catch (error) {
console.error('更新购物车商品数量异常:', error)
return false
}
}
// 更新购物车商品选中状态
async updateCartItemSelection(cartItemId: string, selected: boolean): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法更新购物车')
return false
}
const response = await supa
.from('ml_shopping_cart')
.update({
selected: selected,
updated_at: new Date().toISOString()
})
.eq('id', cartItemId)
.eq('user_id', userId)
.execute()
if (response.error) {
console.error('更新购物车商品选中状态失败:', response.error)
return false
}
return true
} catch (error) {
console.error('更新购物车商品选中状态异常:', error)
return false
}
}
// 批量更新购物车商品选中状态
async batchUpdateCartItemSelection(cartItemIds: string[], selected: boolean): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法更新购物车')
return false
}
const response = await supa
.from('ml_shopping_cart')
.update({
selected: selected,
updated_at: new Date().toISOString()
})
.eq('user_id', userId)
.in('id', cartItemIds)
.execute()
if (response.error) {
console.error('批量更新购物车商品选中状态失败:', response.error)
return false
}
return true
} catch (error) {
console.error('批量更新购物车商品选中状态异常:', error)
return false
}
}
// 删除购物车商品
async deleteCartItem(cartItemId: string): Promise<boolean> {
try {
console.log('正在执行删除购物车商品ID:', cartItemId)
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法删除购物车商品')
return false
}
const response = await supa
.from('ml_shopping_cart')
.eq('id', cartItemId)
.eq('user_id', userId)
.delete()
.execute()
if (response.error) {
console.error('删除购物车商品失败:', response.error)
return false
}
return true
} catch (error) {
console.error('删除购物车商品异常:', error)
return false
}
}
// 批量删除购物车商品
async batchDeleteCartItems(cartItemIds: string[]): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法删除购物车商品')
return false
}
const response = await supa
.from('ml_shopping_cart')
.eq('user_id', userId)
.in('id', cartItemIds)
.delete()
.execute()
if (response.error) {
console.error('批量删除购物车商品失败:', response.error)
return false
}
return true
} catch (error) {
console.error('批量删除购物车商品异常:', error)
return false
}
}
// 清空购物车
async clearCart(): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法清空购物车')
return false
}
const response = await supa
.from('ml_shopping_cart')
.eq('user_id', userId)
.delete()
.execute()
if (response.error) {
console.error('清空购物车失败:', response.error)
return false
}
return true
} catch (error) {
console.error('清空购物车异常:', error)
return false
}
}
// 获取当前用户的所有地址
async getAddresses(): Promise<UserAddress[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.warn('用户未登录,无法获取地址')
return []
}
const response = await supa
.from('ml_user_addresses')
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
.eq('user_id', userId)
.order('is_default', { ascending: false })
.order('created_at', { ascending: false })
.execute()
if (response.error) {
console.error('获取地址失败:', response.error)
return []
}
return response.data as UserAddress[]
} catch (error) {
console.error('获取地址异常:', error)
return []
}
}
// 根据ID获取地址详情
async getAddressById(addressId: string): Promise<UserAddress | null> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.warn('用户未登录,无法获取地址')
return null
}
const response = await supa
.from('ml_user_addresses')
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
.eq('id', addressId)
.eq('user_id', userId)
.single()
.execute()
if (response.error) {
console.error('获取地址详情失败:', response.error)
return null
}
return response.data as UserAddress
} catch (error) {
console.error('获取地址详情异常:', error)
return null
}
}
// 添加新地址
async addAddress(address: {
recipient_name: string
phone: string
province: string
city: string
district: string
detail_address: string
postal_code?: string
is_default?: boolean
}): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法添加地址')
return false
}
// 如果设置为默认地址,需要先取消其他默认地址
if (address.is_default) {
await this.clearDefaultAddress(userId)
}
const response = await supa
.from('ml_user_addresses')
.insert({
user_id: userId,
receiver_name: address.recipient_name,
receiver_phone: address.phone,
province: address.province,
city: address.city,
district: address.district,
address_detail: address.detail_address,
postal_code: address.postal_code || null,
is_default: address.is_default || false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.execute()
if (response.error) {
console.error('添加地址失败:', response.error)
return false
}
return true
} catch (error) {
console.error('添加地址异常:', error)
return false
}
}
// 更新地址
async updateAddress(addressId: string, address: {
recipient_name?: string
phone?: string
province?: string
city?: string
district?: string
detail_address?: string
postal_code?: string
is_default?: boolean
}): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法更新地址')
return false
}
// 如果设置为默认地址,需要先取消其他默认地址
if (address.is_default) {
await this.clearDefaultAddress(userId)
}
// 构造更新数据,映射字段名到数据库列名
const updateData = {}
if (address.recipient_name != null) updateData['receiver_name'] = address.recipient_name
if (address.phone != null) updateData['receiver_phone'] = address.phone
if (address.province != null) updateData['province'] = address.province
if (address.city != null) updateData['city'] = address.city
if (address.district != null) updateData['district'] = address.district
if (address.detail_address != null) updateData['address_detail'] = address.detail_address
if (address.postal_code != null) updateData['postal_code'] = address.postal_code
if (address.is_default != null) updateData['is_default'] = address.is_default
updateData['updated_at'] = new Date().toISOString()
const response = await supa
.from('ml_user_addresses')
.update(updateData)
.eq('id', addressId)
.eq('user_id', userId)
.execute()
if (response.error) {
console.error('更新地址失败:', response.error)
return false
}
return true
} catch (error) {
console.error('更新地址异常:', error)
return false
}
}
// 删除地址
async deleteAddress(addressId: string): Promise<boolean> {
try {
console.log('正在执行删除地址ID:', addressId)
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法删除地址')
return false
}
const response = await supa
.from('ml_user_addresses')
.eq('id', addressId)
.eq('user_id', userId)
.delete()
.execute()
if (response.error) {
console.error('删除地址失败:', response.error)
return false
}
return true
} catch (error) {
console.error('删除地址异常:', error)
return false
}
}
// 清除默认地址(内部使用)
private async clearDefaultAddress(userId: string): Promise<void> {
try {
await supa
.from('ml_user_addresses')
.update({
is_default: false,
updated_at: new Date().toISOString()
})
.eq('user_id', userId)
.eq('is_default', true)
.execute()
} catch (error) {
console.error('清除默认地址异常:', error)
}
}
// 获取用户资料
async getUserProfile(): Promise<any | null> {
try {
const userId = this.getCurrentUserId()
if (!userId) return null
// 联合查询 auth user 和 profile
// 由于 Supabase auth table 不可直接访问,这里查询 ml_user_profiles
const response = await supa
.from('ml_user_profiles')
.select('*')
.eq('user_id', userId)
.single()
.execute()
if (response.error) {
// 如果不存在 profile可能只有 auth user这里暂时返回空或创建默认
return null
}
return response.data
} catch (e) {
return null
}
}
// 创建订单
async createOrder(orderData: {
merchant_id: string,
product_amount: number,
shipping_fee: number,
total_amount: number,
shipping_address: any,
items: any[]
}): Promise<string | null> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('CreateOrder: User not logged in')
return null
}
// 生成订单号
const orderNo = 'ML' + Date.now() + Math.floor(Math.random() * 1000)
// 1. 创建主订单
const orderResponse = await supa
.from('ml_orders')
.insert({
user_id: userId,
merchant_id: orderData.merchant_id,
order_no: orderNo,
product_amount: orderData.product_amount,
shipping_fee: orderData.shipping_fee,
total_amount: orderData.total_amount,
paid_amount: 0,
shipping_address: typeof orderData.shipping_address === 'string' ? orderData.shipping_address : JSON.stringify(orderData.shipping_address),
order_status: 1, // 待付款
payment_status: 1, // 未支付
shipping_status: 1, // 未发货
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.select()
.single()
.execute()
if (orderResponse.error) {
console.error('创建订单失败:', orderResponse.error)
return null
}
const orderId = orderResponse.data['id'] as string
// 2. 创建订单项
const orderItems = orderData.items.map((item: any) => ({
order_id: orderId,
product_id: item.product_id,
sku_id: item.sku_id || null,
product_name: item.product_name,
sku_name: item.sku_name || '',
specifications: item.specifications ? (typeof item.specifications === 'string' ? item.specifications : JSON.stringify(item.specifications)) : '{}',
image_url: item.product_image || item.image_url,
price: item.price,
quantity: item.quantity,
total_amount: item.price * item.quantity,
created_at: new Date().toISOString()
}))
const itemsResponse = await supa
.from('ml_order_items')
.insert(orderItems)
.execute()
if (itemsResponse.error) {
console.error('创建订单项失败:', itemsResponse.error)
return null
}
// 3. 清除购物车中已购买的商品
const cartItemIds = orderData.items
.filter((item: any) => item.id && item.id.length > 10) // 假设UUID长度大于10区分临时ID
.map((item: any) => item.id as string)
if (cartItemIds.length > 0) {
await this.batchDeleteCartItems(cartItemIds)
}
return orderId
} catch (error) {
console.error('创建订单异常:', error)
return null
}
}
// 批量通过店铺创建订单
async createOrdersByShop(params: {
shipping_address: any,
shopGroups: any[],
deliveryFee: number,
discountAmount: number
}): Promise<{ success: boolean, orderIds: string[], error?: string }> {
try {
const orderIds: string[] = []
// 为每个店铺创建一个订单
for (const group of params.shopGroups) {
const shopItems = group.items as any[]
const productAmount = shopItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
// 简单平摊运费和优惠 (实际逻辑可能更复杂)
const ratio = productAmount / params.shopGroups.reduce((sum, g) => sum + g.items.reduce((s, i) => s + (i.price * i.quantity), 0), 0)
const shopShippingFee = params.deliveryFee * ratio
const shopDiscount = params.discountAmount * ratio
const shopTotal = productAmount + shopShippingFee - shopDiscount
const orderId = await this.createOrder({
merchant_id: group.merchant_id || group.shopId, // 兼容旧字段
product_amount: productAmount,
shipping_fee: shopShippingFee,
total_amount: shopTotal,
shipping_address: params.shipping_address,
items: shopItems
})
if (orderId) {
orderIds.push(orderId)
} else {
return { success: false, orderIds, error: `店铺 ${group.shopName} 订单创建失败` }
}
}
return { success: true, orderIds }
} catch (e) {
console.error('批量创建订单异常:', e)
return { success: false, orderIds: [], error: '系统异常' }
}
}
// 获取订单列表
async getOrders(status: number = 0): Promise<any[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
let query = supa
.from('ml_orders')
.select(`
*,
ml_order_items (*)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (status > 0) {
query = query.eq('order_status', status)
}
const response = await query.execute()
if (response.error) {
console.error('获取订单列表失败:', response.error)
return []
}
return response.data || []
} catch (error) {
console.error('获取订单列表异常:', error)
return []
}
}
// 获取订单详情
async getOrderDetail(orderId: string): Promise<any | null> {
try {
const userId = this.getCurrentUserId()
if (!userId) return null
const response = await supa
.from('ml_orders')
.select(`
*,
ml_order_items (*)
`)
.eq('id', orderId)
.eq('user_id', userId)
.single()
.execute()
if (response.error) {
return null
}
return response.data
} catch (e) {
return null
}
}
// 支付订单
async payOrder(orderId: string, paymentMethod: string, amount: number): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) return false
// 1. Update order status
const response = await supa
.from('ml_orders')
.update({
order_status: 2, // 待发货(已支付)
payment_status: 1, // Pay Success
payment_method: paymentMethod, // e.g. 'wechat', 'alipay'
payment_time: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', orderId)
.eq('user_id', userId)
.execute()
if (response.error) {
console.error('Pay order failed', response.error)
return false
}
// 2. Handle specific payment methods (e.g., deduct balance)
if (paymentMethod === 'balance') {
// await this.deductBalance(userId, amount)
}
return true
} catch (e) {
console.error('Pay order error', e)
return false
}
}
// 获取用户钱包余额
async getUserBalance(): Promise<number> {
try {
const userId = this.getCurrentUserId()
if (!userId) return 0
// Check if wallet table exists or uses profile
// Assume ml_user_wallets or field in profile
// For now returning mock high balance or check profile
const profile = await this.getUserProfile()
if (profile && typeof profile.balance === 'number') {
return profile.balance
}
return 0
} catch(e) {
return 0
}
}
// 收藏相关
async checkFavorite(productId: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
console.log(`[CheckFav] Checking for User: ${userId}, Product: ${productId}`)
if (!userId) return false
const response = await supa
.from('ml_user_favorites')
.select('*') // Select all to verify data
.eq('user_id', userId)
.eq('target_id', productId)
.eq('target_type', 1) // 1 for product
.limit(1)
.execute()
// console.log(`[CheckFav] Response: ${JSON.stringify(response)}`)
if (response.error) {
console.error(`[CheckFav] Error: ${JSON.stringify(response.error)}`)
return false
}
const data = response.data
if (Array.isArray(data)) {
if (data.length > 0) {
// Double check: ensure the returned item actually matches the product ID
// This guards against potential query filter failures
const item = data[0]
let targetId = ''
if (item instanceof UTSJSONObject) {
targetId = item.getString('target_id') || ''
} else {
targetId = (item['target_id'] as string) || ''
}
if (targetId !== '' && targetId !== productId) {
console.error(`[CheckFav] ID Mismatch! Query ${productId}, Got ${targetId}`)
return false
}
return true
}
} else if (data instanceof UTSJSONObject) {
// Handle single object return case (though limit(1) usually returns array)
let targetId = data.getString('target_id') || ''
if (targetId !== '' && targetId !== productId) {
return false
}
return true
}
return false
} catch(e) {
console.error(`[CheckFav] Exception: ${e}`)
return false
}
}
async toggleFavorite(productId: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) return false
console.log(`[ToggleFav] Toggling for ${productId}`)
// Check if exists
const exists = await this.checkFavorite(productId)
console.log(`[ToggleFav] Current status: ${exists}`)
if (exists) {
// Delete
const response = await supa
.from('ml_user_favorites')
.eq('user_id', userId)
.eq('target_id', productId)
.eq('target_type', 1)
.delete()
.execute()
if (response.error) {
console.error('取消收藏失败:', response.error)
return true // 仍然是收藏状态
}
return false // 已取消收藏
} else {
// Add
const response = await supa
.from('ml_user_favorites')
.insert({
user_id: userId,
target_id: productId,
target_type: 1,
created_at: new Date().toISOString()
})
.execute()
if (response.error) {
console.error('添加收藏失败:', response.error)
return false // 添加失败,仍未收藏
}
return true // 已收藏
}
} catch (e) {
console.error('切换收藏状态异常:', e)
// 发生异常时,尝试查询当前状态返回
return await this.checkFavorite(productId)
}
}
async getFavorites(): Promise<any[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
// 第一步:查询收藏列表
const response = await supa
.from('ml_user_favorites')
.select('*')
.eq('user_id', userId)
.eq('target_type', 1)
.order('created_at', { ascending: false })
.execute()
if (response.error) return []
const favorites = response.data as any[]
if (!favorites || favorites.length === 0) return []
// 第二步收集商品ID
const productIds: string[] = []
for (let i = 0; i < favorites.length; i++) {
let item = favorites[i]
let pid = ''
if (item instanceof UTSJSONObject) {
pid = item.getString('target_id') || ''
} else {
pid = (item['target_id'] as string) || ''
}
if (pid !== '') productIds.push(pid)
}
if (productIds.length === 0) return []
// 第三步:批量查询商品详情
const productRes = await supa
.from('ml_products')
.select('id, name, main_image_url, base_price, sale_count')
.in('id', productIds)
.execute()
if (productRes.error) return []
const products = productRes.data as any[]
const productMap = new Map<string, any>()
for (let i = 0; i < products.length; i++) {
let p = products[i]
let pid = ''
if (p instanceof UTSJSONObject) {
pid = p.getString('id') || ''
} else {
pid = (p['id'] as string) || ''
}
if (pid !== '') productMap.set(pid, p)
}
// 第四步:组合数据
const result: any[] = []
for (let i = 0; i < favorites.length; i++) {
let item = favorites[i]
// 深拷贝或重新构造 item避免修改原引用
let newItem: any
if (item instanceof UTSJSONObject) {
newItem = item.toMap()
} else {
newItem = { ...item }
}
let targetId = ''
if (item instanceof UTSJSONObject) {
targetId = item.getString('target_id') || ''
} else {
targetId = (item['target_id'] as string) || ''
}
const product = productMap.get(targetId)
if (product) {
// 将商品信息挂载到 ml_products 字段上,保持与前端代码兼容
newItem['ml_products'] = product
result.push(newItem)
}
}
return result
} catch (e) {
console.error('获取收藏列表异常:', e)
return []
}
}
// 足迹相关
async addFootprint(productId: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) return false
// 检查是否已存在
const checkRes = await supa
.from('ml_user_footprints')
.select('id')
.eq('user_id', userId)
.eq('product_id', productId)
.limit(1)
.execute()
if (checkRes.error) {
console.error('检查足迹失败', checkRes.error)
// 可能是表不存在,但我们还是尝试继续或返回
// 这里假设如果检查失败可能是网络问题或权限
// 如果表不存在,下面的 insert 也会失败
}
const data = checkRes.data
let exists = false
if (Array.isArray(data) && data.length > 0) exists = true
else if (data instanceof UTSJSONObject) exists = true // single object case
if (exists) {
// 更新时间
await supa
.from('ml_user_footprints')
.update({
updated_at: new Date().toISOString(),
created_at: new Date().toISOString() // 更新 created_at 以便排序(可选,视需求而定,通常足迹按访问时间倒序)
})
.eq('user_id', userId)
.eq('product_id', productId)
.execute()
} else {
// 新增
await supa
.from('ml_user_footprints')
.insert({
user_id: userId,
product_id: productId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.execute()
}
return true
} catch (e) {
console.error('添加足迹异常:', e)
return false
}
}
async getFootprints(): Promise<any[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
// 1. 获取足迹列表
const response = await supa
.from('ml_user_footprints')
.select('*')
.eq('user_id', userId)
.order('updated_at', { ascending: false }) // 按最后访问时间倒序
.limit(100) // 限制数量
.execute()
if (response.error) {
console.error('获取足迹列表失败:', response.error)
return []
}
const footprints = response.data as any[]
console.log('原始足迹数据条数:', footprints.length)
if (footprints.length > 0) {
console.log('第一条足迹:', JSON.stringify(footprints[0]))
}
if (!footprints || footprints.length === 0) return []
// 2. 收集商品ID
const productIds: string[] = []
for(let i=0; i<footprints.length; i++) {
let item = footprints[i]
let pid = ''
if (item instanceof UTSJSONObject) {
pid = item.getString('product_id') || ''
} else {
pid = (item['product_id'] as string) || ''
}
if (pid !== '') productIds.push(pid)
}
console.log('收集到的商品ID:', JSON.stringify(productIds))
if (productIds.length === 0) return []
// 3. 批量获取商品信息
let products: any[] = []
// 优先尝试查询 ml_products 表(基础表),因为它最可靠
const simpleRes = await supa
.from('ml_products')
.select('id, name, main_image_url, base_price, sale_count, merchant_id')
.in('id', productIds)
.execute()
if (!simpleRes.error && simpleRes.data != null) {
products = simpleRes.data as any[]
console.log('从 ml_products 获取到商品条数:', products.length)
} else {
console.error('从 ml_products 获取商品失败:', simpleRes.error)
}
// 如果需要更详细的店铺信息,可以在这里补充查询店铺,或者简化处理直接显示“未知店铺”
// 为了保证显示,我们先确保有商品基本信息
const productMap = new Map<string, any>()
for(let i=0; i<products.length; i++) {
let p = products[i]
let pid = ''
if (p instanceof UTSJSONObject) {
pid = p.getString('id') || ''
} else {
pid = (p['id'] as string) || ''
}
if (pid !== '') productMap.set(pid, p)
}
// 4. 组合数据
const result: any[] = []
for(let i=0; i<footprints.length; i++) {
let item = footprints[i]
let pid = ''
let viewTimeStr = ''
if (item instanceof UTSJSONObject) {
pid = item.getString('product_id') || ''
viewTimeStr = item.getString('updated_at') || item.getString('created_at') || ''
} else {
pid = (item['product_id'] as string) || ''
viewTimeStr = (item['updated_at'] as string) || (item['created_at'] as string) || ''
}
const product = productMap.get(pid)
if (product) {
// 构造前端需要的格式
let name = '', image = '', shopName = '自营店铺', shopId = ''
let price = 0, original_price = 0, sales = 0
if (product instanceof UTSJSONObject) {
name = product.getString('name') || '未知商品'
image = product.getString('main_image_url') || '/static/default-product.png'
price = product.getNumber('base_price') || 0
sales = product.getNumber('sale_count') || 0
shopId = product.getString('merchant_id') || ''
// 尝试从 view 获取 shopName或者这里暂时给默认值
} else {
name = (product['name'] as string) || '未知商品'
image = (product['main_image_url'] as string) || '/static/default-product.png'
price = (product['base_price'] as number) || 0
sales = (product['sale_count'] as number) || 0
shopId = (product['merchant_id'] as string) || ''
}
result.push({
id: pid,
name: name,
price: price,
original_price: original_price,
image: image,
sales: sales,
shopId: shopId,
shopName: shopName,
viewTime: new Date(viewTimeStr).getTime()
})
} else {
// console.warn('未找到商品信息 ID:', pid)
}
}
// console.log('最终返回足迹数量:', result.length)
return result
} catch (e) {
console.error('获取足迹异常:', e)
return []
}
}
async deleteFootprint(productId: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) return false
await supa
.from('ml_user_footprints')
.eq('user_id', userId)
.eq('product_id', productId)
.delete()
.execute()
return true
} catch (e) {
return false
}
}
async clearFootprints(): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) return false
await supa
.from('ml_user_footprints')
.eq('user_id', userId)
.delete()
.execute()
return true
} catch (e) {
return false
}
}
async getAddressList(): Promise<UserAddress[]> {
try {
const userId = this.getCurrentUserId()
if (!userId) return []
const response = await supa
.from('ml_user_addresses')
.select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
.eq('user_id', userId)
.order('is_default', { ascending: false })
.order('created_at', { ascending: false })
.execute()
if (response.error) {
console.error('获取地址列表失败:', response.error)
return []
}
return response.data as UserAddress[]
} catch (e) {
console.error('获取地址列表异常:', e)
return []
}
}
// 设置默认地址
async setDefaultAddress(addressId: string): Promise<boolean> {
try {
const userId = this.getCurrentUserId()
if (!userId) {
console.error('用户未登录,无法设置默认地址')
return false
}
// 先取消所有默认地址
await this.clearDefaultAddress(userId)
// 设置新的默认地址
const response = await supa
.from('ml_user_addresses')
.update({
is_default: true,
updated_at: new Date().toISOString()
})
.eq('id', addressId)
.eq('user_id', userId)
.execute()
if (response.error) {
console.error('设置默认地址失败:', response.error)
return false
}
return true
} catch (error) {
console.error('设置默认地址异常:', error)
return false
}
}
}
// 导出单例实例
export const supabaseService = new SupabaseService()
// 默认导出
export default supabaseService