10522 lines
360 KiB
Plaintext
10522 lines
360 KiB
Plaintext
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
|
||
import { APP_ROLE, CURRENT_CLIENT, SUPA_KEY, SUPA_URL } from '@/ak/config.uts'
|
||
import { ORDER_PAY_TIMEOUT_SECONDS, ORDER_STATUS_CANCELLED, ORDER_STATUS_PAID_OR_SHIPPING, ORDER_STATUS_PENDING, ORDER_STATUS_TIMEOUT_LEGACY, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_PAID, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type UnifiedOrderSource } from '@/utils/orderStatus.uts'
|
||
|
||
// 导出 supa 实例,供 services 层统一使用
|
||
export { supa }
|
||
import type { OrderOptions } from '@/components/supadb/aksupa.uts'
|
||
|
||
const OLD_URL = '192.168.1.61:18000'
|
||
const NEW_URL = '119.146.131.237:9126'
|
||
|
||
function fixImageUrl(url: string | null): string {
|
||
if (url == null) return ''
|
||
if (url.indexOf(OLD_URL) >= 0) {
|
||
return url.replace(OLD_URL, NEW_URL)
|
||
}
|
||
return url
|
||
}
|
||
|
||
function fixImageUrls(urls: any): string[] {
|
||
if (urls == null) return []
|
||
if (Array.isArray(urls)) {
|
||
const result: string[] = []
|
||
const arr = urls as any[]
|
||
for (let i = 0; i < arr.length; i++) {
|
||
try {
|
||
const urlStr = JSON.stringify(arr[i])
|
||
if (urlStr != null && urlStr.startsWith('"') && urlStr.endsWith('"')) {
|
||
const fixed = fixImageUrl(urlStr.substring(1, urlStr.length - 1))
|
||
if (fixed !== '') result.push(fixed)
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
return result
|
||
}
|
||
return []
|
||
}
|
||
|
||
function canUseConsumerData(): boolean {
|
||
return CURRENT_CLIENT === 'consumer' || CURRENT_CLIENT === 'full'
|
||
}
|
||
|
||
function maskConsumerKey(key: string): string {
|
||
const keyLen = key.length
|
||
if (keyLen <= 20) {
|
||
return '(too short)'
|
||
}
|
||
return key.substring(0, 10) + '...' + key.substring(keyLen - 8)
|
||
}
|
||
|
||
function logConsumerQueryStart(action: string, tableName: string, fieldList: string = '*'): boolean {
|
||
if (!canUseConsumerData()) {
|
||
console.error('[consumer-db] 已阻止非 consumer 数据请求')
|
||
console.error('[consumer-db] 当前端类型:', CURRENT_CLIENT)
|
||
console.error('[consumer-db] 当前应用角色:', APP_ROLE)
|
||
console.error('[consumer-db] 查询表:', tableName)
|
||
console.error('[consumer-db] 查询动作:', action)
|
||
return false
|
||
}
|
||
console.log('[consumer-db] 当前端类型:', CURRENT_CLIENT)
|
||
console.log('[consumer-db] 当前应用角色:', APP_ROLE)
|
||
console.log('[consumer-db] supabaseUrl 已加载:', SUPA_URL)
|
||
console.log('[consumer-db] supabaseKey 已加载:', maskConsumerKey(SUPA_KEY))
|
||
console.log('[consumer-db] 首页开始加载数据')
|
||
console.log('[consumer-db] 查询表:', tableName)
|
||
console.log('[consumer-db] 查询字段:', fieldList)
|
||
console.log('[consumer-db] 查询动作:', action)
|
||
return true
|
||
}
|
||
|
||
function logConsumerQuerySuccess(action: string, tableName: string, count: number): void {
|
||
console.log('[consumer-db] 查询成功,动作:', action)
|
||
console.log('[consumer-db] 查询成功,表:', tableName)
|
||
console.log('[consumer-db] 查询成功,数量:', count)
|
||
}
|
||
|
||
function logConsumerQueryFailure(action: string, tableName: string, error: any): void {
|
||
console.error('[consumer-db] 查询失败')
|
||
console.error('[consumer-db] 查询动作:', action)
|
||
console.error('[consumer-db] 查询表:', tableName)
|
||
if (error != null) {
|
||
try {
|
||
console.error('[consumer-db] error.message:', error.message)
|
||
} catch (e) {}
|
||
try {
|
||
console.error('[consumer-db] error.code:', error.code)
|
||
} catch (e) {}
|
||
try {
|
||
console.error('[consumer-db] error.details:', error.details)
|
||
} catch (e) {}
|
||
try {
|
||
console.error('[consumer-db] error.hint:', error.hint)
|
||
} catch (e) {}
|
||
}
|
||
console.error('[consumer-db] error.raw:', error)
|
||
}
|
||
|
||
// 使用单例 Supabase 客户端
|
||
// const supa = createClient(SUPA_URL, SUPA_KEY)
|
||
|
||
// 辅助函数:安全获取字符串值
|
||
function safeGetString(obj: UTSJSONObject, key: string): string {
|
||
try {
|
||
const rawVal = obj.get(key)
|
||
if (rawVal == null) return ''
|
||
const strVal = JSON.stringify(rawVal)
|
||
if (strVal == null) return ''
|
||
if (strVal.startsWith('"') && strVal.endsWith('"')) {
|
||
return strVal.substring(1, strVal.length - 1)
|
||
}
|
||
return strVal
|
||
} catch (e) {
|
||
console.error('safeGetString error for key:', key, e)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// 辅助函数:安全获取数值
|
||
function safeGetNumber(obj: UTSJSONObject, key: string): number {
|
||
try {
|
||
const rawVal = obj.get(key)
|
||
if (rawVal == null) return 0
|
||
try {
|
||
const numVal = rawVal as number
|
||
if (!isNaN(numVal)) return numVal
|
||
} catch (e) {}
|
||
return 0
|
||
} catch (e) {
|
||
console.error('safeGetNumber error for key:', key, e)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 辅助函数:安全获取布尔值
|
||
function safeGetBoolean(obj: UTSJSONObject, key: string): boolean {
|
||
try {
|
||
const rawVal = obj.get(key)
|
||
if (rawVal == null) return false
|
||
try {
|
||
const boolVal = rawVal as boolean
|
||
return boolVal
|
||
} catch (e) {}
|
||
return false
|
||
} catch (e) {
|
||
console.error('safeGetBoolean error for key:', key, e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 辅助函数:安全获取字符串数组
|
||
function safeGetStringArray(obj: UTSJSONObject, key: string): string[] {
|
||
try {
|
||
const rawVal = obj.get(key)
|
||
if (rawVal != null && Array.isArray(rawVal)) {
|
||
return rawVal as string[]
|
||
}
|
||
return [] as string[]
|
||
} catch (e) {
|
||
console.error('safeGetStringArray error for key:', key, e)
|
||
return [] as string[]
|
||
}
|
||
}
|
||
|
||
// 辅助函数:从原始数据解析商品
|
||
function parseProductFromRaw(item: any): Product {
|
||
try {
|
||
console.log('[parseProductFromRaw] 开始解析商品')
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
console.log('[parseProductFromRaw] JSON转换成功')
|
||
|
||
const mainImageUrl = fixImageUrl(safeGetString(itemObj, 'main_image_url'))
|
||
const imageUrls = fixImageUrls(safeGetStringArray(itemObj, 'image_urls'))
|
||
console.log('[parseProductFromRaw] 图片处理完成')
|
||
|
||
const result: Product = {
|
||
id: safeGetString(itemObj, 'id'),
|
||
name: safeGetString(itemObj, 'name'),
|
||
short_title: safeGetString(itemObj, 'short_title'),
|
||
subtitle: safeGetString(itemObj, 'subtitle'),
|
||
description: safeGetString(itemObj, 'description'),
|
||
base_price: safeGetNumber(itemObj, 'base_price'),
|
||
price: safeGetNumber(itemObj, 'base_price'),
|
||
original_price: safeGetNumber(itemObj, 'market_price'),
|
||
market_price: safeGetNumber(itemObj, 'market_price'),
|
||
main_image_url: mainImageUrl,
|
||
image_url: mainImageUrl,
|
||
images: imageUrls,
|
||
category_id: safeGetString(itemObj, 'category_id'),
|
||
brand_id: safeGetString(itemObj, 'brand_id'),
|
||
merchant_id: safeGetString(itemObj, 'merchant_id'),
|
||
total_stock: safeGetNumber(itemObj, 'total_stock'),
|
||
stock: safeGetNumber(itemObj, 'total_stock'),
|
||
sale_count: safeGetNumber(itemObj, 'sale_count'),
|
||
status: safeGetNumber(itemObj, 'status'),
|
||
is_featured: safeGetBoolean(itemObj, 'is_featured'),
|
||
is_new: safeGetBoolean(itemObj, 'is_new'),
|
||
is_hot: safeGetBoolean(itemObj, 'is_hot'),
|
||
card_tags: safeGetStringArray(itemObj, 'card_tags'),
|
||
service_tags: safeGetStringArray(itemObj, 'service_tags'),
|
||
selling_points: safeGetStringArray(itemObj, 'selling_points'),
|
||
display_sales_text: safeGetString(itemObj, 'display_sales_text'),
|
||
compliance_type: safeGetString(itemObj, 'compliance_type'),
|
||
device_class: safeGetString(itemObj, 'device_class'),
|
||
specification: safeGetString(itemObj, 'specification'),
|
||
usage: safeGetString(itemObj, 'usage'),
|
||
side_effects: safeGetString(itemObj, 'side_effects'),
|
||
precautions: safeGetString(itemObj, 'precautions'),
|
||
expiry_date: safeGetString(itemObj, 'expiry_date'),
|
||
storage_conditions: safeGetString(itemObj, 'storage_conditions'),
|
||
approval_number: safeGetString(itemObj, 'approval_number'),
|
||
created_at: safeGetString(itemObj, 'created_at')
|
||
}
|
||
console.log('[parseProductFromRaw] 商品解析成功:', result.name)
|
||
return result
|
||
} catch (e) {
|
||
console.error('parseProductFromRaw error:', e)
|
||
return {
|
||
id: '',
|
||
name: '',
|
||
description: '',
|
||
base_price: 0,
|
||
price: 0,
|
||
original_price: 0,
|
||
market_price: 0,
|
||
main_image_url: '',
|
||
image_url: '',
|
||
images: [] as string[],
|
||
category_id: '',
|
||
brand_id: '',
|
||
merchant_id: '',
|
||
total_stock: 0,
|
||
stock: 0,
|
||
sale_count: 0,
|
||
status: 0,
|
||
is_featured: false,
|
||
is_new: false,
|
||
is_hot: false,
|
||
specification: '',
|
||
usage: '',
|
||
side_effects: '',
|
||
precautions: '',
|
||
expiry_date: '',
|
||
storage_conditions: '',
|
||
approval_number: '',
|
||
created_at: ''
|
||
} as Product
|
||
}
|
||
}
|
||
|
||
// 类型定义
|
||
export type Brand = {
|
||
id: string
|
||
name: string
|
||
logo_url: string
|
||
description: string
|
||
}
|
||
|
||
export type Category = {
|
||
id: string
|
||
name: string
|
||
icon: string
|
||
description: string
|
||
color: string
|
||
parent_id?: string
|
||
level?: number
|
||
slug?: string
|
||
sort_order?: number
|
||
image_url?: string
|
||
scene?: string
|
||
category_type?: string
|
||
compliance_type?: string
|
||
is_active?: boolean
|
||
created_at?: string
|
||
}
|
||
|
||
export type 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_url?: string
|
||
image_urls?: string
|
||
video_urls?: string
|
||
images?: string[]
|
||
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
|
||
rating?: number
|
||
review_count?: number
|
||
brand_id?: string
|
||
shop_id?: string
|
||
tags?: string
|
||
attributes?: string
|
||
short_title?: string
|
||
card_tags?: string[]
|
||
service_tags?: string[]
|
||
selling_points?: string[]
|
||
display_sales_text?: string
|
||
compliance_type?: string
|
||
device_class?: string
|
||
specification?: string
|
||
usage?: string
|
||
side_effects?: string
|
||
precautions?: string
|
||
expiry_date?: string
|
||
storage_conditions?: string
|
||
approval_number?: string
|
||
created_at?: string
|
||
updated_at?: string
|
||
price?: number
|
||
original_price?: number
|
||
stock?: number
|
||
sales?: number
|
||
cover?: string
|
||
brand_name?: string
|
||
category_name?: string
|
||
shop_name?: string
|
||
merchant_name?: string
|
||
}
|
||
|
||
export type 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 type 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 type 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
|
||
label?: string
|
||
latitude?: number
|
||
longitude?: number
|
||
coordinate_type?: string
|
||
created_at?: string
|
||
updated_at?: string
|
||
}
|
||
|
||
export type UserCoupon = {
|
||
id: string
|
||
template_id: string
|
||
template_name: string
|
||
amount: number
|
||
min_spend: number
|
||
expire_at: string
|
||
status: number
|
||
discount_type: number
|
||
coupon_type: number
|
||
used_at: string
|
||
received_at: string
|
||
merchant_id: string
|
||
shop_name: string
|
||
shop_logo: string
|
||
scope_type: string
|
||
consumer_deleted_at?: string
|
||
}
|
||
|
||
export type ChatRoom = {
|
||
id: string
|
||
user_id: string
|
||
merchant_id: string
|
||
shop_name: string
|
||
shop_logo?: string
|
||
last_message?: string
|
||
last_message_at?: string
|
||
unread_count: number
|
||
is_top: boolean
|
||
created_at?: string
|
||
updated_at?: string
|
||
}
|
||
|
||
export type 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 type 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 type PaginatedResponse<T> = {
|
||
data: T[]
|
||
total: number
|
||
page: number
|
||
limit: number
|
||
hasmore: boolean
|
||
}
|
||
|
||
export type 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
|
||
}
|
||
|
||
export type FavoriteSkuItem = {
|
||
sku_id: string
|
||
product_id: string
|
||
sku_name: string
|
||
spec_text: string
|
||
sku_image_url: string
|
||
sale_price: number
|
||
stock_quantity: number
|
||
status: number
|
||
}
|
||
|
||
export type FavoritePurchaseItem = {
|
||
favorite_id: string
|
||
target_id: string
|
||
target_type: string
|
||
merchant_id: string
|
||
merchant_name: string
|
||
merchant_logo_url: string
|
||
title: string
|
||
image_url: string
|
||
current_price: number
|
||
original_price: number
|
||
has_original_price: boolean
|
||
display_tags: string[]
|
||
description_text: string
|
||
availability_status: string
|
||
has_skus: boolean
|
||
sku_list: FavoriteSkuItem[]
|
||
selected_sku_id: string
|
||
selected_sku_text: string
|
||
selected_sku_price: number
|
||
selected_sku_stock: number
|
||
quantity: number
|
||
purchase_selected: boolean
|
||
manage_selected: boolean
|
||
can_purchase: boolean
|
||
can_appoint: boolean
|
||
}
|
||
|
||
export type FavoriteDisplayItem = FavoritePurchaseItem
|
||
|
||
export type FavoriteGroup = {
|
||
group_id: string
|
||
group_name: string
|
||
group_logo_url: string
|
||
group_tags: string[]
|
||
items: FavoritePurchaseItem[]
|
||
}
|
||
|
||
export type AddAddressParams = {
|
||
recipient_name: string
|
||
phone: string
|
||
province: string
|
||
city: string
|
||
district: string
|
||
detail_address: string
|
||
postal_code?: string
|
||
is_default?: boolean
|
||
label?: string
|
||
latitude?: number
|
||
longitude?: number
|
||
coordinate_type?: string
|
||
}
|
||
|
||
export type UpdateAddressParams = {
|
||
recipient_name?: string
|
||
phone?: string
|
||
province?: string
|
||
city?: string
|
||
district?: string
|
||
detail_address?: string
|
||
postal_code?: string
|
||
is_default?: boolean
|
||
label?: string
|
||
latitude?: number
|
||
longitude?: number
|
||
coordinate_type?: string
|
||
}
|
||
|
||
export type CreateOrderParams = {
|
||
merchant_id: string
|
||
product_amount: number
|
||
shipping_fee: number
|
||
total_amount: number
|
||
shipping_address: any
|
||
items: any[]
|
||
}
|
||
|
||
export type ShopOrderParams = {
|
||
shipping_address: any
|
||
shopGroups: any[]
|
||
deliveryFee: number
|
||
discountAmount: number
|
||
}
|
||
|
||
const FAVORITE_TARGET_PRODUCT = 1
|
||
const FAVORITE_TARGET_SHOP = 2
|
||
const FAVORITE_TARGET_SERVICE = 3
|
||
const DEFAULT_FAVORITE_IMAGE = '/static/images/default.png'
|
||
|
||
function normalizeUtsObject(item: any): UTSJSONObject {
|
||
if (item instanceof UTSJSONObject) {
|
||
return item as UTSJSONObject
|
||
}
|
||
return JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
}
|
||
|
||
function parseStringArrayValue(rawValue: any): string[] {
|
||
const result = [] as string[]
|
||
if (rawValue == null) {
|
||
return result
|
||
}
|
||
|
||
if (Array.isArray(rawValue)) {
|
||
const rawList = rawValue as any[]
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const rawItem = rawList[i]
|
||
if (typeof rawItem == 'string') {
|
||
const value = fixImageUrl(rawItem as string)
|
||
if (value != '') {
|
||
result.push(value)
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
if (typeof rawValue == 'string') {
|
||
const rawText = rawValue as string
|
||
if (rawText == '') {
|
||
return result
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(rawText)
|
||
if (Array.isArray(parsed)) {
|
||
const parsedList = parsed as any[]
|
||
for (let i = 0; i < parsedList.length; i++) {
|
||
const parsedItem = parsedList[i]
|
||
if (typeof parsedItem == 'string') {
|
||
const value = fixImageUrl(parsedItem as string)
|
||
if (value != '') {
|
||
result.push(value)
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
} catch (e) {
|
||
}
|
||
const singleValue = fixImageUrl(rawText)
|
||
if (singleValue != '') {
|
||
result.push(singleValue)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
function buildFavoriteSkuSpecText(rawSpecifications: string, skuCode: string): string {
|
||
if (rawSpecifications != null && rawSpecifications != '') {
|
||
try {
|
||
const specs = JSON.parse(rawSpecifications) as UTSJSONObject
|
||
let specText = ''
|
||
for (const key in specs) {
|
||
const value = specs[key]
|
||
if (value != null) {
|
||
specText += (specText == '' ? '' : ' | ') + value.toString()
|
||
}
|
||
}
|
||
if (specText != '') {
|
||
return specText
|
||
}
|
||
} catch (e) {
|
||
console.error('[buildFavoriteSkuSpecText] 解析规格失败:', e)
|
||
}
|
||
}
|
||
return skuCode != '' ? skuCode : '默认规格'
|
||
}
|
||
|
||
function emptyProductPage(page: number, limit: number): PaginatedResponse<Product> {
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
function buildAnyStringList(values: string[]): any[] {
|
||
const result: any[] = []
|
||
for (let i = 0; i < values.length; i++) {
|
||
if (values[i] != '') {
|
||
result.push(values[i])
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
const FOOTPRINT_TABLE_CANDIDATES = ['ml_user_footprints', 'ml_browse_history']
|
||
|
||
function getErrorMessage(error: any): string {
|
||
if (error == null) return ''
|
||
try {
|
||
const message = error.message
|
||
if (message != null) return message as string
|
||
} catch (e) {}
|
||
try {
|
||
return JSON.stringify(error)
|
||
} catch (e) {}
|
||
return ''
|
||
}
|
||
|
||
function isMissingDeletedAtError(error: any): boolean {
|
||
const message = getErrorMessage(error)
|
||
return message.indexOf('deleted_at') >= 0 && (message.indexOf('column') >= 0 || message.indexOf('schema cache') >= 0)
|
||
}
|
||
|
||
function parseMedicalMallCategory(item: UTSJSONObject): Category {
|
||
const icon = safeGetString(item, 'icon')
|
||
const imageUrl = safeGetString(item, 'image_url')
|
||
const color = safeGetString(item, 'color')
|
||
return {
|
||
id: safeGetString(item, 'id'),
|
||
name: safeGetString(item, 'name'),
|
||
icon: icon != '' ? icon : imageUrl,
|
||
description: safeGetString(item, 'description'),
|
||
color: color != '' ? color : '#16a085',
|
||
parent_id: safeGetString(item, 'parent_id'),
|
||
level: safeGetNumber(item, 'level'),
|
||
slug: safeGetString(item, 'category_type'),
|
||
sort_order: safeGetNumber(item, 'sort_order'),
|
||
image_url: imageUrl,
|
||
scene: safeGetString(item, 'scene'),
|
||
category_type: safeGetString(item, 'category_type'),
|
||
compliance_type: safeGetString(item, 'compliance_type'),
|
||
is_active: safeGetBoolean(item, 'is_active'),
|
||
created_at: safeGetString(item, 'created_at')
|
||
} as Category
|
||
}
|
||
|
||
function getMedicalMallCategoryKeywords(categoryId: string): string[] {
|
||
if (categoryId == 'med_device') return ['医疗器械', '血压计', '血糖仪', '血氧仪', '体温计', '制氧机', '雾化器', '轮椅', '拐杖', '护理床']
|
||
if (categoryId == 'blood_pressure_monitor') return ['血压计', '血压监测', '电子血压计']
|
||
if (categoryId == 'blood_glucose_meter') return ['血糖仪', '血糖测试', '采血笔']
|
||
if (categoryId == 'oximeter') return ['血氧仪', '血氧']
|
||
if (categoryId == 'thermometer') return ['体温计', '额温枪', '测温']
|
||
if (categoryId == 'oxygen_concentrator') return ['制氧机', '吸氧']
|
||
if (categoryId == 'nebulizer') return ['雾化器', '雾化']
|
||
if (categoryId == 'wheelchair_crutch') return ['轮椅', '拐杖', '助行器']
|
||
if (categoryId == 'nursing_bed') return ['护理床', '病床']
|
||
|
||
if (categoryId == 'otc_medicine') return ['感冒药', '退烧药', '止咳', '咽喉', '肠胃', '皮肤', '止痛', '跌打', '儿童用药', '家庭常备药']
|
||
if (categoryId == 'cold_fever') return ['感冒药', '退烧', '发烧', '退热贴', '感冒']
|
||
if (categoryId == 'cough_throat') return ['咳嗽', '咽喉', '润喉']
|
||
if (categoryId == 'stomach_medicine') return ['肠胃', '胃药', '益生菌']
|
||
if (categoryId == 'skin_external') return ['皮肤', '外用', '软膏', '喷剂']
|
||
if (categoryId == 'eye_ear_nose') return ['眼药', '滴眼', '鼻炎', '口腔', '耳']
|
||
if (categoryId == 'pain_relief') return ['止痛', '镇痛']
|
||
if (categoryId == 'trauma_sprain') return ['跌打', '扭伤', '损伤', '喷雾']
|
||
if (categoryId == 'child_medicine') return ['儿童', '小儿']
|
||
|
||
if (categoryId == 'rehab_care') return ['康复', '护理', '敷料', '护具', '支具', '理疗', '热敷', '训练']
|
||
if (categoryId == 'postop_rehab') return ['术后', '恢复带', '康复']
|
||
if (categoryId == 'wound_dressing' || categoryId == 'wound_care') return ['伤口', '敷料', '纱布', '创可贴', '护理包']
|
||
if (categoryId == 'brace_support') return ['护具', '支具', '护腰', '护膝', '支撑带']
|
||
if (categoryId == 'rehab_training') return ['康复训练', '训练器']
|
||
if (categoryId == 'physiotherapy_hot') return ['理疗', '热敷', '暖贴', '热敷贴']
|
||
if (categoryId == 'mobility_training') return ['行动训练', '步行训练']
|
||
|
||
if (categoryId == 'chronic_monitor') return ['慢病', '血压', '血糖', '心脑血管', '呼吸', '体脂', '家庭检测']
|
||
if (categoryId == 'hypertension') return ['高血压', '血压']
|
||
if (categoryId == 'diabetes') return ['糖尿病', '血糖']
|
||
if (categoryId == 'cardiovascular') return ['心脑血管', '心率', '心电']
|
||
if (categoryId == 'respiratory_health') return ['呼吸', '雾化', '氧', '制氧']
|
||
if (categoryId == 'weight_bodyfat') return ['体重', '体脂']
|
||
if (categoryId == 'home_testing') return ['检测', '试纸']
|
||
|
||
if (categoryId == 'elderly_aid') return ['适老', '长者', '防滑', '扶手', '助浴', '失禁', '生活辅助']
|
||
if (categoryId == 'anti_slip') return ['防滑', '防跌']
|
||
if (categoryId == 'elderly_bathroom') return ['卫浴', '扶手', '浴室']
|
||
if (categoryId == 'eating_aid') return ['助餐', '餐具']
|
||
if (categoryId == 'bathing_aid') return ['助浴', '洗澡椅', '沐浴']
|
||
if (categoryId == 'incontinence_care') return ['失禁', '护理垫', '纸尿裤']
|
||
if (categoryId == 'daily_living_aid') return ['生活辅助', '起身', '坐便']
|
||
|
||
if (categoryId == 'nutrition_health') return ['维生素', '钙片', '蛋白粉', '营养', '益生菌', '免疫']
|
||
if (categoryId == 'vitamin') return ['维生素', '维C', '维D']
|
||
if (categoryId == 'calcium') return ['钙片', '钙', '钙铁锌', '锌', '硒']
|
||
if (categoryId == 'protein') return ['蛋白', '蛋白粉']
|
||
if (categoryId == 'elderly_nutrition') return ['老人营养', '中老年营养']
|
||
if (categoryId == 'gut_health') return ['益生菌', '肠道']
|
||
if (categoryId == 'immune_support') return ['免疫', '免疫支持']
|
||
|
||
if (categoryId == 'protection_disinfection') return ['口罩', '消毒', '酒精', '湿巾', '手套', '急救包', '棉签', '纱布']
|
||
if (categoryId == 'mask') return ['口罩']
|
||
if (categoryId == 'disinfectant') return ['消毒液', '消毒', '酒精']
|
||
if (categoryId == 'alcohol_wipes') return ['酒精湿巾', '湿巾']
|
||
if (categoryId == 'gloves') return ['手套']
|
||
if (categoryId == 'first_aid') return ['急救包']
|
||
if (categoryId == 'dressing_tools' || categoryId == 'nursing_consumables') return ['棉签', '纱布', '耗材']
|
||
|
||
if (categoryId == 'tcm_health') return ['中医', '艾灸', '拔罐', '养生茶', '贴敷', '药膳', '按摩', '泡脚']
|
||
if (categoryId == 'moxibustion') return ['艾灸', '拔罐', '艾条', '温灸']
|
||
if (categoryId == 'herbal_drink') return ['养生茶', '茶饮', '草本']
|
||
if (categoryId == 'tcm_patch') return ['贴敷', '理疗贴', '草本贴']
|
||
if (categoryId == 'medicated_diet') return ['药膳', '滋补']
|
||
if (categoryId == 'massage_tools') return ['按摩', '理疗', '刮痧']
|
||
if (categoryId == 'foot_bath') return ['泡脚', '足浴']
|
||
|
||
if (categoryId == 'home_care_daily') return ['护理', '伤口', '口腔', '皮肤', '清洁', '耗材', '健康工具']
|
||
if (categoryId == 'oral_care') return ['口腔', '牙', '漱口']
|
||
if (categoryId == 'skin_care') return ['皮肤护理', '修护', '护肤']
|
||
if (categoryId == 'cleaning_care') return ['清洁护理', '清洁']
|
||
if (categoryId == 'home_health_tool') return ['健康工具', '家庭健康']
|
||
|
||
if (categoryId == 'all_medical' || categoryId == 'recommend') return ['血压计', '血糖仪', '血氧仪', '体温计', '制氧机', '雾化器', '轮椅', '护理床', '感冒药', '退热贴', '止咳', '胃药', '创可贴', '敷料', '纱布', '护具', '康复', '理疗', '慢病', '口罩', '消毒', '酒精', '急救包', '艾灸', '拔罐', '维生素', '钙片', '蛋白粉', '益生菌', '老人营养', '适老', '助浴', '护理']
|
||
return [] as string[]
|
||
}
|
||
|
||
function containsMedicalMallKeyword(source: string, keywords: string[]): boolean {
|
||
if (source == '' || keywords.length == 0) {
|
||
return false
|
||
}
|
||
for (let i = 0; i < keywords.length; i++) {
|
||
if (keywords[i] != '' && source.indexOf(keywords[i]) != -1) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
function buildMedicalMallProductText(item: UTSJSONObject): string {
|
||
const tags = safeGetStringArray(item, 'tags')
|
||
let tagText = ''
|
||
for (let i = 0; i < tags.length; i++) {
|
||
if (tags[i] != '') {
|
||
tagText += ' ' + tags[i]
|
||
}
|
||
}
|
||
return (
|
||
safeGetString(item, 'name') + ' ' +
|
||
safeGetString(item, 'subtitle') + ' ' +
|
||
safeGetString(item, 'description') + ' ' +
|
||
safeGetString(item, 'category_name') + ' ' +
|
||
safeGetString(item, 'specification') + ' ' +
|
||
safeGetString(item, 'usage') + ' ' +
|
||
tagText
|
||
)
|
||
}
|
||
|
||
function isRxHiddenProduct(item: UTSJSONObject): boolean {
|
||
return safeGetString(item, 'compliance_type') == 'rx_hidden'
|
||
}
|
||
|
||
function matchesMedicalMallFallback(item: UTSJSONObject, categoryId: string): boolean {
|
||
if (isRxHiddenProduct(item)) {
|
||
return false
|
||
}
|
||
const sourceText = buildMedicalMallProductText(item)
|
||
return containsMedicalMallKeyword(sourceText, getMedicalMallCategoryKeywords(categoryId))
|
||
}
|
||
|
||
function sortProductsByIdOrder(products: Product[], orderedIds: string[]): Product[] {
|
||
const productMap = new Map<string, Product>()
|
||
for (let i = 0; i < products.length; i++) {
|
||
productMap.set(products[i].id, products[i])
|
||
}
|
||
const sorted: Product[] = []
|
||
for (let i = 0; i < orderedIds.length; i++) {
|
||
const item = productMap.get(orderedIds[i])
|
||
if (item != null) {
|
||
sorted.push(item)
|
||
}
|
||
}
|
||
return sorted
|
||
}
|
||
|
||
function dedupeProducts(products: Product[]): Product[] {
|
||
const deduped: Product[] = []
|
||
const seenIds: string[] = []
|
||
for (let i = 0; i < products.length; i++) {
|
||
const productId = products[i].id
|
||
if (productId != '' && seenIds.indexOf(productId) != -1) {
|
||
continue
|
||
}
|
||
if (productId != '') {
|
||
seenIds.push(productId)
|
||
}
|
||
deduped.push(products[i])
|
||
}
|
||
return deduped
|
||
}
|
||
|
||
export type ShopOrderResponse = {
|
||
success: boolean
|
||
orderIds: string[]
|
||
error?: string
|
||
}
|
||
|
||
export type RefundResponse = {
|
||
success: boolean
|
||
message: string
|
||
}
|
||
|
||
export type ConfirmReceiptResponse = {
|
||
success: boolean
|
||
error?: string
|
||
}
|
||
|
||
export type GetOrdersByCursorParams = {
|
||
cursor: string
|
||
limit: number
|
||
status: number
|
||
keyword: string
|
||
}
|
||
|
||
export type GetOrdersByCursorResult = {
|
||
list: UTSJSONObject[]
|
||
nextCursor: string
|
||
hasMore: boolean
|
||
}
|
||
|
||
export type GetUnifiedOrdersByCursorParams = {
|
||
cursor: string
|
||
limit: number
|
||
bizType: string
|
||
statusTab: string
|
||
keyword: string
|
||
}
|
||
|
||
export type GetUnifiedOrdersByCursorResult = {
|
||
list: UTSJSONObject[]
|
||
nextCursor: string
|
||
hasMore: boolean
|
||
}
|
||
|
||
export type OrderCountsResult = {
|
||
all: number
|
||
pending: number
|
||
shipping: number
|
||
delivering: number
|
||
completed: number
|
||
aftersale: number
|
||
cancelled: number
|
||
}
|
||
|
||
class SupabaseService {
|
||
// 获取当前用户ID
|
||
public getCurrentUserId(): string | null {
|
||
try {
|
||
// 1. 优先从 Supabase 会话获取
|
||
const session = supa.getSession()
|
||
if (session != null && session.user != null) {
|
||
return session.user.getString('id')
|
||
}
|
||
|
||
// 移除基于 storage 的后备获取,严格只认当前 Tab 独立 session
|
||
return null
|
||
|
||
//cyh
|
||
// 2. 尝试从 Storage 恢复 Session (针对 App 重启后内存丢失的情况)
|
||
// 注意:这里无法异步调用 hydrate,所以只能依赖 UI 层或 init 层的预加载
|
||
// 但我们可以返回本地存储 ID 作为 fallback,前提是 Token 有效
|
||
|
||
// 后备:尝试从本地存储获取
|
||
// const userId = uni.getStorageSync('user_id')
|
||
// return userId != null ? userId as string : null
|
||
|
||
} catch (e) {
|
||
console.error('获取用户ID失败:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 确保会话有效 (异步)
|
||
async ensureSession(): Promise<string | null> {
|
||
let session = supa.getSession()
|
||
if (session.user == null) {
|
||
console.log('Session user is null, attempting to hydrate from storage...')
|
||
await supa.hydrateSessionFromStorage()
|
||
session = supa.getSession()
|
||
}
|
||
|
||
if (session.user != null) {
|
||
// 同步 user_id 到 storage 保持一致
|
||
const uid = session.user!!.getString('id')
|
||
if (uid != null) {
|
||
uni.setStorageSync('user_id', uid)
|
||
return uid
|
||
}
|
||
}
|
||
return this.getCurrentUserId()
|
||
}
|
||
|
||
// 获取所有分类
|
||
async getCategories(): Promise<Category[]> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_categories')
|
||
.select('*')
|
||
.order('name', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取分类失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return []
|
||
}
|
||
|
||
const categories: Category[] = []
|
||
const rawList = rawData as any[]
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const catObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const idVal = catObj.get('id')
|
||
const nameVal = catObj.get('name')
|
||
const iconVal = catObj.get('icon')
|
||
const iconUrlVal = catObj.get('icon_url')
|
||
const descVal = catObj.get('description')
|
||
const colorVal = catObj.get('color')
|
||
const parentIdVal = catObj.get('parent_id')
|
||
const levelVal = catObj.get('level')
|
||
|
||
const cat: Category = {
|
||
id: (typeof idVal == 'string') ? (idVal as string) : '',
|
||
name: (typeof nameVal == 'string') ? (nameVal as string) : '',
|
||
icon: (typeof iconVal == 'string') ? (iconVal as string) : ((typeof iconUrlVal == 'string') ? (iconUrlVal as string) : ''),
|
||
description: (typeof descVal == 'string') ? (descVal as string) : '',
|
||
color: (typeof colorVal == 'string') ? (colorVal as string) : '#4CAF50',
|
||
parent_id: (typeof parentIdVal == 'string') ? (parentIdVal as string) : null,
|
||
level: (typeof levelVal == 'number') ? (levelVal as number) : 0
|
||
} as Category
|
||
categories.push(cat)
|
||
}
|
||
return categories
|
||
} catch (error) {
|
||
console.error('获取分类异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 根据ID获取单个分类
|
||
async getCategoryById(categoryId: string): Promise<Category | null> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_categories')
|
||
.select('*')
|
||
.eq('id', categoryId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取分类失败:', response.error)
|
||
return null
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return null
|
||
}
|
||
|
||
// 处理数组返回值
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) {
|
||
return null
|
||
}
|
||
|
||
const item = rawList[0]
|
||
const catObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const idVal = catObj.get('id')
|
||
const nameVal = catObj.get('name')
|
||
const iconVal = catObj.get('icon')
|
||
const iconUrlVal = catObj.get('icon_url')
|
||
const descVal = catObj.get('description')
|
||
const colorVal = catObj.get('color')
|
||
const parentIdVal = catObj.get('parent_id')
|
||
const levelVal = catObj.get('level')
|
||
|
||
const cat: Category = {
|
||
id: (typeof idVal == 'string') ? (idVal as string) : '',
|
||
name: (typeof nameVal == 'string') ? (nameVal as string) : '',
|
||
icon: (typeof iconVal == 'string') ? (iconVal as string) : ((typeof iconUrlVal == 'string') ? (iconUrlVal as string) : ''),
|
||
description: (typeof descVal == 'string') ? (descVal as string) : '',
|
||
color: (typeof colorVal == 'string') ? (colorVal as string) : '#4CAF50',
|
||
parent_id: (typeof parentIdVal == 'string') ? (parentIdVal as string) : null,
|
||
level: (typeof levelVal == 'number') ? (levelVal as number) : 0
|
||
} as Category
|
||
return cat
|
||
} catch (error) {
|
||
console.error('获取分类异常:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 获取一级分类
|
||
async getParentCategories(): Promise<Category[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getParentCategories', 'ml_categories', '*')) {
|
||
return []
|
||
}
|
||
const response = await supa
|
||
.from('ml_categories')
|
||
.select('*')
|
||
.eq('level', 1)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getParentCategories', 'ml_categories', response.error)
|
||
console.error('获取一级分类失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return []
|
||
}
|
||
|
||
const categories: Category[] = []
|
||
const rawList = rawData as Array<UTSJSONObject>
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const icon = this.getCategoryIcon(item)
|
||
|
||
// 安全获取属性
|
||
const idVal = item['id']
|
||
const nameVal = item['name']
|
||
const descVal = item['description']
|
||
const colorVal = item['color']
|
||
const slugVal = item['slug']
|
||
|
||
const cat: Category = {
|
||
id: (typeof idVal == 'string') ? (idVal as string) : '',
|
||
name: (typeof nameVal == 'string') ? (nameVal as string) : '',
|
||
icon: icon,
|
||
description: (typeof descVal == 'string') ? (descVal as string) : '',
|
||
color: (typeof colorVal == 'string') ? (colorVal as string) : '#ff5000',
|
||
level: 1,
|
||
slug: (typeof slugVal == 'string') ? (slugVal as string) : ''
|
||
}
|
||
categories.push(cat)
|
||
}
|
||
logConsumerQuerySuccess('getParentCategories', 'ml_categories', categories.length)
|
||
return categories
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getParentCategories', 'ml_categories', error)
|
||
console.error('获取一级分类异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取子分类
|
||
async getSubCategories(parentId: string): Promise<Category[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getSubCategories', 'ml_categories', '*')) {
|
||
return []
|
||
}
|
||
console.log('[getSubCategories] 开始获取子分类, parentId:', parentId)
|
||
const response = await supa
|
||
.from('ml_categories')
|
||
.select('*')
|
||
.eq('level', 2)
|
||
.eq('parent_id', parentId)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
|
||
console.log('[getSubCategories] 查询完成')
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getSubCategories', 'ml_categories', response.error)
|
||
console.error('获取子分类失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
console.log('[getSubCategories] 数据为空')
|
||
return []
|
||
}
|
||
|
||
const categories: Category[] = []
|
||
const rawList = rawData as UTSJSONObject[]
|
||
console.log('[getSubCategories] 原始数据条数:', rawList.length)
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const icon = this.getCategoryIcon(itemObj)
|
||
const cat: Category = {
|
||
id: safeGetString(itemObj, 'id'),
|
||
name: safeGetString(itemObj, 'name'),
|
||
icon: icon,
|
||
description: safeGetString(itemObj, 'description'),
|
||
color: safeGetString(itemObj, 'color').length > 0 ? safeGetString(itemObj, 'color') : '#ff5000',
|
||
level: 2,
|
||
parent_id: safeGetString(itemObj, 'parent_id'),
|
||
slug: safeGetString(itemObj, 'slug')
|
||
}
|
||
categories.push(cat)
|
||
}
|
||
console.log('[getSubCategories] 返回分类数量:', categories.length)
|
||
logConsumerQuerySuccess('getSubCategories', 'ml_categories', categories.length)
|
||
return categories
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getSubCategories', 'ml_categories', error)
|
||
console.error('获取子分类异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取分类图标的辅助方法
|
||
getCategoryIcon(item: UTSJSONObject): string {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const icon = safeGetString(itemObj, 'icon')
|
||
if (icon.length > 0) {
|
||
return icon
|
||
}
|
||
const iconUrl = safeGetString(itemObj, 'icon_url')
|
||
if (iconUrl.length > 0) {
|
||
return iconUrl
|
||
}
|
||
const name = safeGetString(itemObj, 'name')
|
||
if (name.includes('数码') || name.includes('电器') || name.includes('手机')) return '📱'
|
||
if (name.includes('服装') || name.includes('衣服') || name.includes('鞋')) return '👕'
|
||
if (name.includes('食品') || name.includes('水果') || name.includes('零食')) return '🍎'
|
||
if (name.includes('美妆') || name.includes('护肤') || name.includes('化妆')) return '💄'
|
||
if (name.includes('母婴') || name.includes('婴儿') || name.includes('儿童')) return '👶'
|
||
if (name.includes('家居') || name.includes('家具') || name.includes('装饰')) return '🏠'
|
||
if (name.includes('图书') || name.includes('文具')) return '📚'
|
||
if (name.includes('运动') || name.includes('户外') || name.includes('健身')) return '⚽'
|
||
if (name.includes('医药') || name.includes('保健') || name.includes('健康')) return '💊'
|
||
return '📦'
|
||
}
|
||
|
||
// 获取所有品牌
|
||
async getBrands(): Promise<Brand[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getBrands', 'ml_brands', 'id, name, logo_url, description, is_active')) {
|
||
return []
|
||
}
|
||
console.log('[getBrands] 开始获取品牌数据...')
|
||
const response = await supa
|
||
.from('ml_brands')
|
||
.select('id, name, logo_url, description, is_active')
|
||
.order('name', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getBrands', 'ml_brands', response.error)
|
||
console.error('获取品牌失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
console.log('[getBrands] 数据为空')
|
||
return []
|
||
}
|
||
|
||
const brands: Brand[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getBrands] 数据条数:', rawList.length)
|
||
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const brandObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const idVal = brandObj.get('id')
|
||
const nameVal = brandObj.get('name')
|
||
const logoVal = brandObj.get('logo_url')
|
||
const descVal = brandObj.get('description')
|
||
const isActiveVal = brandObj.get('is_active')
|
||
|
||
let isActiveBool: boolean = true
|
||
if (isActiveVal != null) {
|
||
if (typeof isActiveVal == 'boolean') {
|
||
isActiveBool = isActiveVal as boolean
|
||
} else if (typeof isActiveVal == 'number') {
|
||
isActiveBool = (isActiveVal as number) === 1
|
||
}
|
||
}
|
||
if (!isActiveBool) {
|
||
continue
|
||
}
|
||
|
||
const brand: Brand = {
|
||
id: (typeof idVal == 'string') ? (idVal as string) : '',
|
||
name: (typeof nameVal == 'string') ? (nameVal as string) : '',
|
||
logo_url: (typeof logoVal == 'string') ? (logoVal as string) : '',
|
||
description: (typeof descVal == 'string') ? (descVal as string) : ''
|
||
} as Brand
|
||
brands.push(brand)
|
||
}
|
||
console.log('[getBrands] 返回品牌数量:', brands.length)
|
||
logConsumerQuerySuccess('getBrands', 'ml_brands', brands.length)
|
||
return brands
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getBrands', 'ml_brands', error)
|
||
console.error('获取品牌异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取指定分类的商品
|
||
async getProductsByCategory(
|
||
categoryId: string,
|
||
page: number = 1,
|
||
limit: number = 20
|
||
): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getProductsByCategory', 'ml_products_detail_view', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
console.log('[getProductsByCategory] 开始查询,分类ID:', categoryId, '页码:', page)
|
||
|
||
// 在数据库层面进行分类过滤
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('category_id', categoryId)
|
||
.eq('status', '1') // 使用字符串 '1' 而不是整数 1
|
||
.order('sale_count', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
.execute()
|
||
|
||
console.log('[getProductsByCategory] 查询完成,total:', response.total)
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getProductsByCategory', 'ml_products_detail_view', response.error)
|
||
console.error('获取商品失败:', response.error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getProductsByCategory] 返回数据条数:', rawList.length)
|
||
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
products.push(parseProductFromRaw(item))
|
||
}
|
||
logConsumerQuerySuccess('getProductsByCategory', 'ml_products_detail_view', products.length)
|
||
|
||
return {
|
||
data: products,
|
||
total: response.total ?? products.length,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getProductsByCategory', 'ml_products_detail_view', error)
|
||
console.error('获取商品异常:', error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
}
|
||
|
||
async getMedicalMallParentCategories(): Promise<Category[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getMedicalMallParentCategories', 'medical_mall_categories', '*')) {
|
||
return []
|
||
}
|
||
const response = await supa
|
||
.from('medical_mall_categories')
|
||
.select('*')
|
||
.is('parent_id', null)
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
logConsumerQueryFailure('getMedicalMallParentCategories', 'medical_mall_categories', response.error)
|
||
return []
|
||
}
|
||
|
||
const rows = response.data as UTSJSONObject[]
|
||
const categories: Category[] = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
categories.push(parseMedicalMallCategory(JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject))
|
||
}
|
||
logConsumerQuerySuccess('getMedicalMallParentCategories', 'medical_mall_categories', categories.length)
|
||
return categories
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getMedicalMallParentCategories', 'medical_mall_categories', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
async getMedicalMallCategoryById(categoryId: string): Promise<Category | null> {
|
||
try {
|
||
if (categoryId == '') {
|
||
return null
|
||
}
|
||
const response = await supa
|
||
.from('medical_mall_categories')
|
||
.select('*')
|
||
.eq('id', categoryId)
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
logConsumerQueryFailure('getMedicalMallCategoryById', 'medical_mall_categories', response.error)
|
||
return null
|
||
}
|
||
|
||
const rows = response.data as UTSJSONObject[]
|
||
if (rows.length == 0) {
|
||
return null
|
||
}
|
||
|
||
return parseMedicalMallCategory(JSON.parse(JSON.stringify(rows[0])) as UTSJSONObject)
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getMedicalMallCategoryById', 'medical_mall_categories', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async getMedicalMallSubCategories(parentId: string): Promise<Category[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getMedicalMallSubCategories', 'medical_mall_categories', '*')) {
|
||
return []
|
||
}
|
||
const response = await supa
|
||
.from('medical_mall_categories')
|
||
.select('*')
|
||
.eq('parent_id', parentId)
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
logConsumerQueryFailure('getMedicalMallSubCategories', 'medical_mall_categories', response.error)
|
||
return []
|
||
}
|
||
|
||
const rows = response.data as UTSJSONObject[]
|
||
const categories: Category[] = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
categories.push(parseMedicalMallCategory(JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject))
|
||
}
|
||
logConsumerQuerySuccess('getMedicalMallSubCategories', 'medical_mall_categories', categories.length)
|
||
return categories
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getMedicalMallSubCategories', 'medical_mall_categories', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
async getMedicalMallCategoryChildrenIds(parentId: string): Promise<string[]> {
|
||
try {
|
||
let response: any
|
||
if (parentId == 'all_medical') {
|
||
response = await supa
|
||
.from('medical_mall_categories')
|
||
.select('id')
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.eq('level', 2)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
} else {
|
||
response = await supa
|
||
.from('medical_mall_categories')
|
||
.select('id')
|
||
.eq('parent_id', parentId)
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
}
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return []
|
||
}
|
||
|
||
const rows = response.data as UTSJSONObject[]
|
||
const ids: string[] = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const row = JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject
|
||
const id = safeGetString(row, 'id')
|
||
if (id != '') {
|
||
ids.push(id)
|
||
}
|
||
}
|
||
return ids
|
||
} catch (error) {
|
||
console.error('获取医疗商城子分类 ID 失败:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
async getMedicalMallProductsByCategory(categoryId: string, page: number = 1, limit: number = 20): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (categoryId == 'recommend') {
|
||
return await this.getMedicalMallSmartRecommendations(page, limit)
|
||
}
|
||
if (!logConsumerQueryStart('getMedicalMallProductsByCategory', 'medical_mall_product_categories', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const targetCategoryIds: string[] = []
|
||
if (categoryId == 'all_medical') {
|
||
const allChildren = await this.getMedicalMallCategoryChildrenIds('all_medical')
|
||
for (let i = 0; i < allChildren.length; i++) {
|
||
targetCategoryIds.push(allChildren[i])
|
||
}
|
||
} else {
|
||
const categoryResponse = await supa
|
||
.from('medical_mall_categories')
|
||
.select('id, level')
|
||
.eq('id', categoryId)
|
||
.eq('is_active', true)
|
||
.is('deleted_at', null)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
targetCategoryIds.push(categoryId)
|
||
if (categoryResponse.error == null && categoryResponse.data != null) {
|
||
const categoryRows = categoryResponse.data as UTSJSONObject[]
|
||
if (categoryRows.length > 0) {
|
||
const categoryObj = JSON.parse(JSON.stringify(categoryRows[0])) as UTSJSONObject
|
||
if (safeGetNumber(categoryObj, 'level') == 1) {
|
||
const childIds = await this.getMedicalMallCategoryChildrenIds(categoryId)
|
||
for (let i = 0; i < childIds.length; i++) {
|
||
if (targetCategoryIds.indexOf(childIds[i]) == -1) {
|
||
targetCategoryIds.push(childIds[i])
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const linkedIds: string[] = []
|
||
if (targetCategoryIds.length > 0) {
|
||
const linkResponse = await supa
|
||
.from('medical_mall_product_categories')
|
||
.select('product_id, sort_order, created_at')
|
||
.in('category_id', buildAnyStringList(targetCategoryIds))
|
||
.is('deleted_at', null)
|
||
.order('sort_order', { ascending: true })
|
||
.order('created_at', { ascending: false })
|
||
.limit(500)
|
||
.execute()
|
||
|
||
if (linkResponse.error == null && linkResponse.data != null) {
|
||
const linkRows = linkResponse.data as UTSJSONObject[]
|
||
for (let i = 0; i < linkRows.length; i++) {
|
||
const row = JSON.parse(JSON.stringify(linkRows[i])) as UTSJSONObject
|
||
const productId = safeGetString(row, 'product_id')
|
||
if (productId != '' && linkedIds.indexOf(productId) == -1) {
|
||
linkedIds.push(productId)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let finalProducts: Product[] = []
|
||
if (linkedIds.length > 0) {
|
||
const productResponse = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.in('id', buildAnyStringList(linkedIds))
|
||
.eq('status', '1')
|
||
.execute()
|
||
|
||
if (productResponse.error == null && productResponse.data != null) {
|
||
const rows = productResponse.data as any[]
|
||
const parsed: Product[] = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const rowObj = JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject
|
||
if (isRxHiddenProduct(rowObj)) {
|
||
continue
|
||
}
|
||
parsed.push(parseProductFromRaw(rows[i]))
|
||
}
|
||
finalProducts = sortProductsByIdOrder(parsed, linkedIds)
|
||
}
|
||
}
|
||
|
||
if (finalProducts.length == 0) {
|
||
const fallbackResponse = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.eq('status', '1')
|
||
.order('sale_count', { ascending: false })
|
||
.order('created_at', { ascending: false })
|
||
.limit(300)
|
||
.execute()
|
||
|
||
if (fallbackResponse.error == null && fallbackResponse.data != null) {
|
||
const fallbackRows = fallbackResponse.data as any[]
|
||
for (let i = 0; i < fallbackRows.length; i++) {
|
||
const rowObj = JSON.parse(JSON.stringify(fallbackRows[i])) as UTSJSONObject
|
||
if (!matchesMedicalMallFallback(rowObj, categoryId)) {
|
||
continue
|
||
}
|
||
finalProducts.push(parseProductFromRaw(fallbackRows[i]))
|
||
}
|
||
}
|
||
}
|
||
|
||
const deduped = dedupeProducts(finalProducts)
|
||
const startIndex = (page - 1) * limit
|
||
const endIndex = startIndex + limit
|
||
return {
|
||
data: deduped.slice(startIndex, endIndex),
|
||
total: deduped.length,
|
||
page,
|
||
limit,
|
||
hasmore: deduped.length > endIndex
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getMedicalMallProductsByCategory', 'medical_mall_product_categories', error)
|
||
console.error('获取医疗商城分类商品失败:', error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
async getMedicalMallSmartRecommendations(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getMedicalMallSmartRecommendations', 'medical_mall_product_categories', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const linkedIds: string[] = []
|
||
const linkResponse = await supa
|
||
.from('medical_mall_product_categories')
|
||
.select('product_id, is_primary, sort_order, created_at')
|
||
.is('deleted_at', null)
|
||
.order('is_primary', { ascending: false })
|
||
.order('sort_order', { ascending: true })
|
||
.order('created_at', { ascending: false })
|
||
.limit(500)
|
||
.execute()
|
||
|
||
if (linkResponse.error == null && linkResponse.data != null) {
|
||
const linkRows = linkResponse.data as UTSJSONObject[]
|
||
for (let i = 0; i < linkRows.length; i++) {
|
||
const row = JSON.parse(JSON.stringify(linkRows[i])) as UTSJSONObject
|
||
const productId = safeGetString(row, 'product_id')
|
||
if (productId != '' && linkedIds.indexOf(productId) == -1) {
|
||
linkedIds.push(productId)
|
||
}
|
||
}
|
||
}
|
||
|
||
const merged: Product[] = []
|
||
const addedIds = new Set<string>()
|
||
|
||
if (linkedIds.length > 0) {
|
||
const linkedProductResponse = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.in('id', buildAnyStringList(linkedIds))
|
||
.eq('status', '1')
|
||
.execute()
|
||
|
||
if (linkedProductResponse.error == null && linkedProductResponse.data != null) {
|
||
const rows = linkedProductResponse.data as any[]
|
||
const parsed: Product[] = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const rowObj = JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject
|
||
if (isRxHiddenProduct(rowObj)) {
|
||
continue
|
||
}
|
||
parsed.push(parseProductFromRaw(rows[i]))
|
||
}
|
||
const ordered = sortProductsByIdOrder(parsed, linkedIds)
|
||
for (let i = 0; i < ordered.length; i++) {
|
||
if (!addedIds.has(ordered[i].id)) {
|
||
merged.push(ordered[i])
|
||
addedIds.add(ordered[i].id)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (merged.length < page * limit + limit) {
|
||
const fallbackResponse = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.eq('status', '1')
|
||
.order('sale_count', { ascending: false })
|
||
.order('created_at', { ascending: false })
|
||
.limit(300)
|
||
.execute()
|
||
|
||
if (fallbackResponse.error == null && fallbackResponse.data != null) {
|
||
const rows = fallbackResponse.data as any[]
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const rowObj = JSON.parse(JSON.stringify(rows[i])) as UTSJSONObject
|
||
if (!matchesMedicalMallFallback(rowObj, 'recommend')) {
|
||
continue
|
||
}
|
||
const product = parseProductFromRaw(rows[i])
|
||
if (!addedIds.has(product.id)) {
|
||
merged.push(product)
|
||
addedIds.add(product.id)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const startIndex = (page - 1) * limit
|
||
const endIndex = startIndex + limit
|
||
return {
|
||
data: merged.slice(startIndex, endIndex),
|
||
total: merged.length,
|
||
page,
|
||
limit,
|
||
hasmore: merged.length > endIndex
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getMedicalMallSmartRecommendations', 'medical_mall_product_categories', error)
|
||
console.error('获取医疗商城推荐商品失败:', error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
// 根据商品ID获取SKU列表
|
||
async getProductSkus(productId: string): Promise<ProductSku[]> {
|
||
try {
|
||
console.log('[getProductSkus] 开始获取SKU,商品ID:', productId)
|
||
const response = await supa
|
||
.from('ml_product_skus')
|
||
.select('*')
|
||
.eq('product_id', productId)
|
||
.eq('status', '1')
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取商品SKU失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return []
|
||
|
||
const skus: ProductSku[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getProductSkus] 获取到SKU数量:', rawList.length)
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const skuObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const rawId = skuObj.get('id')
|
||
const rawSkuCode = skuObj.get('sku_code')
|
||
const rawProdId = skuObj.get('product_id')
|
||
const rawPrice = skuObj.get('price')
|
||
const rawStock = skuObj.get('stock')
|
||
const rawImageUrl = skuObj.get('image_url')
|
||
const rawSpecs = skuObj.get('specifications')
|
||
|
||
let specsStr = ''
|
||
if (rawSpecs != null) {
|
||
try {
|
||
if (typeof rawSpecs == 'string') {
|
||
specsStr = rawSpecs as string
|
||
} else {
|
||
specsStr = JSON.stringify(rawSpecs)
|
||
}
|
||
} catch(e) {
|
||
console.error('解析SKU规格失败', e)
|
||
}
|
||
}
|
||
|
||
const sku: ProductSku = {
|
||
id: (typeof rawId == 'string') ? (rawId as string) : '',
|
||
product_id: (typeof rawProdId == 'string') ? (rawProdId as string) : '',
|
||
sku_code: (typeof rawSkuCode == 'string') ? (rawSkuCode as string) : '',
|
||
specifications: specsStr,
|
||
price: (typeof rawPrice == 'number') ? (rawPrice as number) : 0,
|
||
stock: (typeof rawStock == 'number') ? (rawStock as number) : 0,
|
||
image_url: (typeof rawImageUrl == 'string') ? (rawImageUrl as string) : '',
|
||
status: 1
|
||
}
|
||
skus.push(sku)
|
||
}
|
||
return skus
|
||
} 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 {
|
||
const keywordLower = keyword.toLowerCase()
|
||
const encodedKeyword = encodeURIComponent(keywordLower)
|
||
const orString = `name.ilike.%${encodedKeyword}%,description.ilike.%${encodedKeyword}%,subtitle.ilike.%${encodedKeyword}%,brand_name.ilike.%${encodedKeyword}%`
|
||
console.log('[searchProducts] 搜索关键词:', keyword, '编码后:', encodedKeyword)
|
||
console.log('[searchProducts] or条件:', orString)
|
||
|
||
let query = supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', 1)
|
||
.or(orString)
|
||
|
||
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()
|
||
|
||
let dataLength = 0
|
||
try {
|
||
const respData = response.data
|
||
if (respData != null && Array.isArray(respData)) {
|
||
dataLength = (respData as any[]).length
|
||
}
|
||
} catch (e) {
|
||
console.error('[searchProducts] 获取数据长度失败:', e)
|
||
}
|
||
let statusNum = 0
|
||
try {
|
||
statusNum = response.status as number
|
||
} catch (e) {}
|
||
console.log('[searchProducts] 响应状态:', statusNum, '数据条数:', dataLength)
|
||
|
||
let hasError = false
|
||
try {
|
||
hasError = response.error != null
|
||
} catch (e) {}
|
||
if (hasError) {
|
||
console.error('[searchProducts] 搜索商品失败:', response.error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
const rawData = response.data
|
||
console.log('[searchProducts] rawData:', rawData != null ? 'not null' : 'null')
|
||
if (rawData == null) {
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
const products: Product[] = []
|
||
let rawList: any[] = []
|
||
try {
|
||
rawList = rawData as any[]
|
||
console.log('[searchProducts] rawList长度:', rawList.length)
|
||
} catch (e) {
|
||
console.error('[searchProducts] 转换rawList失败:', e)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
console.log('[searchProducts] 处理第', i + 1, '个商品')
|
||
products.push(parseProductFromRaw(item))
|
||
}
|
||
|
||
let totalNum = 0
|
||
try {
|
||
totalNum = response.total as number
|
||
} catch (e) {}
|
||
let hasmoreVal = false
|
||
try {
|
||
hasmoreVal = response.hasmore as boolean
|
||
} catch (e) {}
|
||
|
||
return {
|
||
data: products,
|
||
total: totalNum > 0 ? totalNum : products.length,
|
||
page,
|
||
limit,
|
||
hasmore: hasmoreVal
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索商品异常:', error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 搜索店铺
|
||
async searchShops(
|
||
keyword: string,
|
||
page: number = 1,
|
||
limit: number = 20
|
||
): Promise<PaginatedResponse<Shop>> {
|
||
try {
|
||
const encodedKeyword = encodeURIComponent(keyword)
|
||
const response = await supa
|
||
.from('ml_shops')
|
||
.select('*', { count: 'exact' })
|
||
.ilike('shop_name', `%${encodedKeyword}%`)
|
||
.order('product_count', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('搜索店铺失败:', response.error)
|
||
return { data: [] as Shop[], total: 0, page, limit, hasmore: false }
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return { data: [] as Shop[], total: 0, page, limit, hasmore: false }
|
||
}
|
||
|
||
const shops: Shop[] = []
|
||
const dataList = rawData as any[]
|
||
for (let i = 0; i < dataList.length; i++) {
|
||
const item = dataList[i]
|
||
const shopObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
// 手动过滤 status
|
||
const rawStatus = shopObj.get('status')
|
||
let statusNum: number = 0
|
||
if (typeof rawStatus == 'number') {
|
||
statusNum = rawStatus as number
|
||
}
|
||
if (statusNum !== 1) continue
|
||
|
||
// 手动创建 Shop 对象,避免安卓端类型转换错误
|
||
const shop: Shop = {
|
||
id: shopObj.getString('id') ?? '',
|
||
merchant_id: shopObj.getString('merchant_id') ?? '',
|
||
shop_name: shopObj.getString('shop_name') ?? '',
|
||
shop_logo: shopObj.getString('shop_logo'),
|
||
shop_banner: shopObj.getString('shop_banner'),
|
||
description: shopObj.getString('description'),
|
||
contact_name: shopObj.getString('contact_name'),
|
||
contact_phone: shopObj.getString('contact_phone'),
|
||
rating_avg: shopObj.getNumber('rating_avg'),
|
||
total_sales: shopObj.getNumber('total_sales'),
|
||
product_count: shopObj.getNumber('product_count'),
|
||
total_sales_count: shopObj.getNumber('total_sales_count'),
|
||
created_at: shopObj.getString('created_at')
|
||
}
|
||
shops.push(shop)
|
||
}
|
||
|
||
return {
|
||
data: shops,
|
||
total: response.total ?? shops.length,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索店铺异常:', error)
|
||
return { data: [] as Shop[], total: 0, page, limit, hasmore: false }
|
||
}
|
||
}
|
||
|
||
// 获取单个商品详情
|
||
async getProductById(productId: string): Promise<Product | null> {
|
||
try {
|
||
console.log('[getProductById] 开始获取商品详情,ID:', productId)
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.eq('id', productId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取商品详情失败:', response.error)
|
||
return null
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
console.log('[getProductById] 数据为空')
|
||
return null
|
||
}
|
||
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) {
|
||
console.log('[getProductById] 未找到商品')
|
||
return null
|
||
}
|
||
|
||
const item = rawList[0]
|
||
const product = parseProductFromRaw(item)
|
||
console.log('[getProductById] 成功获取商品:', product.name)
|
||
return product
|
||
} catch (error) {
|
||
console.error('获取商品详情异常:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// --- 关注店铺相关 ---
|
||
|
||
// 检查是否已关注店铺
|
||
async isShopFollowed(shopId: string, userId: string): Promise<boolean> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_shop_follows')
|
||
.select('id', { count: 'exact' })
|
||
.eq('shop_id', shopId)
|
||
.eq('user_id', userId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
return (res.total != null && res.total! > 0)
|
||
} catch (e) {
|
||
console.error('Check follow error:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 关注店铺
|
||
async followShop(shopId: string, userId: string): Promise<boolean> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_shop_follows')
|
||
.insert({
|
||
user_id: userId,
|
||
shop_id: shopId
|
||
})
|
||
.execute()
|
||
|
||
return res.error == null
|
||
} catch (e) {
|
||
console.error('Follow shop error:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 取消关注
|
||
async unfollowShop(shopId: string, userId: string): Promise<boolean> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_shop_follows')
|
||
.eq('shop_id', shopId)
|
||
.eq('user_id', userId)
|
||
.delete()
|
||
.execute()
|
||
|
||
return res.error == null
|
||
} catch (e) {
|
||
console.error('Unfollow shop error:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取我关注的店铺列表
|
||
async getFollowedShops(userId: string): Promise<any[]> {
|
||
try {
|
||
// 关联查询店铺信息
|
||
const res = await supa
|
||
.from('ml_shop_follows')
|
||
.select('*, ml_shops(*)')
|
||
.eq('user_id', userId)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('getFollowedShops error:', res.error)
|
||
return []
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('getFollowedShops exception:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 根据商户ID获取店铺信息
|
||
async getShopByMerchantId(merchantId: string): Promise<Shop | null> {
|
||
try {
|
||
console.log('[getShopByMerchantId] 开始获取店铺信息,merchantId:', merchantId)
|
||
// 1. Try querying by merchant_id
|
||
let response = await supa
|
||
.from('ml_shops')
|
||
.select('*')
|
||
.eq('merchant_id', merchantId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error == null && response.data != null && (response.data as any[]).length > 0) {
|
||
const shopData = (response.data as any[])[0]
|
||
const shop = this.parseShopFromRaw(shopData)
|
||
console.log('[getShopByMerchantId] 通过 merchant_id 找到店铺:', shop.shop_name)
|
||
return shop
|
||
}
|
||
|
||
// 2. Fallback: Try querying by id (Maybe the passed ID is the Shop ID?)
|
||
console.log('getShopByMerchantId: merchant_id not found, trying id...', merchantId)
|
||
response = await supa
|
||
.from('ml_shops')
|
||
.select('*')
|
||
.eq('id', merchantId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error == null && response.data != null && (response.data as any[]).length > 0) {
|
||
console.log('Found shop by ID instead of MerchantID')
|
||
const shopData = (response.data as any[])[0]
|
||
const shop = this.parseShopFromRaw(shopData)
|
||
return shop
|
||
}
|
||
|
||
if (response.error != null) {
|
||
console.error('获取店铺信息失败:', response.error)
|
||
}
|
||
return null
|
||
} catch (error) {
|
||
console.error('获取店铺信息异常:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 解析店铺数据
|
||
parseShopFromRaw(item: any): Shop {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = itemObj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val
|
||
return ''
|
||
}
|
||
|
||
const getSafeNumber = (key: string): number => {
|
||
const val = itemObj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val == 'number') return val
|
||
return 0
|
||
}
|
||
|
||
return {
|
||
id: getSafeString('id'),
|
||
merchant_id: getSafeString('merchant_id'),
|
||
shop_name: getSafeString('shop_name'),
|
||
shop_logo: getSafeString('shop_logo'),
|
||
shop_banner: getSafeString('shop_banner'),
|
||
description: getSafeString('description'),
|
||
contact_name: getSafeString('contact_name'),
|
||
contact_phone: getSafeString('contact_phone'),
|
||
rating_avg: getSafeNumber('rating_avg'),
|
||
total_sales: getSafeNumber('total_sales'),
|
||
product_count: getSafeNumber('product_count'),
|
||
total_sales_count: getSafeNumber('total_sales_count'),
|
||
created_at: getSafeString('created_at')
|
||
} as Shop
|
||
}
|
||
|
||
// 根据商户ID获取商品列表
|
||
async getProductsByMerchantId(merchantId: string, page: number = 1, limit: number = 20): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
console.log('getProductsByMerchantId querying for:', merchantId)
|
||
|
||
// 1. Try fetching from view strictly first
|
||
let query = supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('merchant_id', merchantId)
|
||
// .eq('status', 1) // Temporarily disabled status check to see if products exist at all
|
||
.order('created_at', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
|
||
const response = await query.execute()
|
||
|
||
// 检查视图结果:如果有错误 OR 数据为空,都尝试去查原始表
|
||
if (response.error != null || (response.data != null && (response.data as any[]).length === 0)) {
|
||
if (response.error != null) {
|
||
console.error('获取商户商品失败 (View):', response.error)
|
||
} else {
|
||
console.log('View returned 0 products, trying raw table fallback...')
|
||
}
|
||
|
||
// Fallback: Try raw table
|
||
console.log('Falling back to raw ml_products table...')
|
||
const query2 = supa
|
||
.from('ml_products')
|
||
.select('*', { count: 'exact' })
|
||
.eq('merchant_id', merchantId)
|
||
// .eq('status', 1) // Also disabled here
|
||
.order('created_at', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
|
||
const res2 = await query2.execute()
|
||
if (res2.error != null) {
|
||
console.error('获取商户商品失败 (Raw):', res2.error)
|
||
return {data:[] as Product[], total:0, page, limit, hasmore:false}
|
||
}
|
||
|
||
console.log(`Fallback (Raw) found: ${(res2.data as any[]).length} products`)
|
||
|
||
const mappedData: Product[] = []
|
||
const rawData = res2.data as any[]
|
||
for(let i = 0; i < rawData.length; i++) {
|
||
const item = JSON.parse(JSON.stringify(rawData[i])) as UTSJSONObject
|
||
const images: string[] = []
|
||
|
||
const mainImageUrl = fixImageUrl(item.getString('main_image_url'))
|
||
if (mainImageUrl != null && mainImageUrl !== '') {
|
||
images.push(mainImageUrl)
|
||
}
|
||
|
||
const imageUrlsRaw = item.get('image_urls')
|
||
if (imageUrlsRaw != null) {
|
||
try {
|
||
if (Array.isArray(imageUrlsRaw)) {
|
||
const arr = imageUrlsRaw as string[]
|
||
if (arr.length > 0 && images.length === 0) {
|
||
for (let j = 0; j < arr.length; j++) {
|
||
const fixedUrl = fixImageUrl(arr[j])
|
||
if (fixedUrl !== '') images.push(fixedUrl)
|
||
}
|
||
}
|
||
} else {
|
||
const rawUrlStr = imageUrlsRaw as string
|
||
if (rawUrlStr.startsWith('[')) {
|
||
const parsed = JSON.parse(rawUrlStr)
|
||
if (Array.isArray(parsed) && images.length === 0) {
|
||
for (let j = 0; j < parsed.length; j++) {
|
||
const fixedUrl = fixImageUrl(parsed[j] as string)
|
||
if (fixedUrl !== '') images.push(fixedUrl)
|
||
}
|
||
}
|
||
} else {
|
||
const fixedUrl = fixImageUrl(rawUrlStr)
|
||
if (fixedUrl !== '' && images.indexOf(fixedUrl) === -1) images.push(fixedUrl)
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('解析图片数组失败:', e)
|
||
}
|
||
}
|
||
|
||
if (images.length === 0) {
|
||
images.push('/static/default-product.png')
|
||
}
|
||
|
||
let safePrice = item.getNumber('base_price')
|
||
if (safePrice == null) {
|
||
const p = item.getNumber('price')
|
||
safePrice = p != null ? p : 0
|
||
}
|
||
|
||
let safeOriginalPrice = item.getNumber('market_price')
|
||
if (safeOriginalPrice == null) {
|
||
const op = item.getNumber('original_price')
|
||
safeOriginalPrice = op != null ? op : safePrice
|
||
}
|
||
|
||
let safeStock = item.getNumber('total_stock')
|
||
if (safeStock == null) {
|
||
let as_ = item.getNumber('available_stock')
|
||
if (as_ == null) {
|
||
const s = item.getNumber('stock')
|
||
safeStock = s != null ? s : 0
|
||
} else {
|
||
safeStock = as_
|
||
}
|
||
}
|
||
|
||
let safeSales = item.getNumber('sale_count')
|
||
if (safeSales == null) {
|
||
const s = item.getNumber('sales')
|
||
safeSales = s != null ? s : 0
|
||
}
|
||
|
||
const product: Product = {
|
||
id: item.getString('id') ?? '',
|
||
category_id: item.getString('category_id') ?? '',
|
||
merchant_id: item.getString('merchant_id') ?? '',
|
||
name: item.getString('name') ?? '',
|
||
description: item.getString('description') ?? '',
|
||
images: images,
|
||
price: safePrice,
|
||
original_price: safeOriginalPrice,
|
||
stock: safeStock,
|
||
sales: safeSales,
|
||
status: item.getNumber('status') ?? 1,
|
||
created_at: item.getString('created_at') ?? '',
|
||
base_price: safePrice,
|
||
market_price: safeOriginalPrice,
|
||
main_image_url: images.length > 0 ? images[0] : '',
|
||
sale_count: safeSales,
|
||
total_stock: safeStock
|
||
} as Product
|
||
mappedData.push(product)
|
||
}
|
||
|
||
return {
|
||
data: mappedData,
|
||
total: res2.total ?? 0,
|
||
page,
|
||
limit,
|
||
hasmore: res2.hasmore ?? false
|
||
}
|
||
}
|
||
|
||
console.log(`Merchant products found: ${(response.data as any[]).length}`)
|
||
|
||
const viewData = response.data as any[]
|
||
const parsedProducts: Product[] = []
|
||
for (let i = 0; i < viewData.length; i++) {
|
||
parsedProducts.push(parseProductFromRaw(viewData[i]))
|
||
}
|
||
|
||
return {
|
||
data: parsedProducts,
|
||
total: response.total ?? 0,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
console.error('获取商户商品异常:', error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 根据店铺ID获取商品列表(新增)
|
||
async getProductsByShopId(shopId: string, page: number = 1, limit: number = 20): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
console.log('getProductsByShopId querying for:', shopId)
|
||
|
||
// 1. Try fetching from view with shop_id
|
||
let query = supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('shop_id', shopId)
|
||
.order('created_at', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
|
||
const response = await query.execute()
|
||
|
||
// 检查视图结果:如果有错误 OR 数据为空,都尝试去查原始表
|
||
if (response.error != null || (response.data != null && (response.data as any[]).length === 0)) {
|
||
if (response.error != null) {
|
||
console.error('获取店铺商品失败 (View):', response.error)
|
||
} else {
|
||
console.log('View returned 0 products, trying raw table fallback...')
|
||
}
|
||
|
||
// Fallback: Try raw table with shop_id
|
||
console.log('Falling back to raw ml_products table with shop_id...')
|
||
const query2 = supa
|
||
.from('ml_products')
|
||
.select('*', { count: 'exact' })
|
||
.eq('shop_id', shopId)
|
||
.order('created_at', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
|
||
const res2 = await query2.execute()
|
||
if (res2.error != null) {
|
||
console.error('获取店铺商品失败 (Raw):', res2.error)
|
||
return {data:[] as Product[], total:0, page, limit, hasmore:false}
|
||
}
|
||
|
||
console.log(`Fallback (Raw) found: ${(res2.data as any[]).length} products`)
|
||
|
||
const mappedData: Product[] = []
|
||
const rawData = res2.data as any[]
|
||
for(let i = 0; i < rawData.length; i++) {
|
||
const item = JSON.parse(JSON.stringify(rawData[i])) as UTSJSONObject
|
||
const images: string[] = []
|
||
|
||
const mainImageUrl = fixImageUrl(item.getString('main_image_url'))
|
||
if (mainImageUrl != null && mainImageUrl !== '') {
|
||
images.push(mainImageUrl)
|
||
}
|
||
|
||
const imageUrlsRaw = item.get('image_urls')
|
||
if (imageUrlsRaw != null) {
|
||
try {
|
||
if (Array.isArray(imageUrlsRaw)) {
|
||
const arr = imageUrlsRaw as string[]
|
||
if (arr.length > 0 && images.length === 0) {
|
||
for (let j = 0; j < arr.length; j++) {
|
||
const fixedUrl = fixImageUrl(arr[j])
|
||
if (fixedUrl !== '') images.push(fixedUrl)
|
||
}
|
||
}
|
||
} else {
|
||
const rawUrlStr = imageUrlsRaw as string
|
||
if (rawUrlStr.startsWith('[')) {
|
||
const parsed = JSON.parse(rawUrlStr)
|
||
if (Array.isArray(parsed) && images.length === 0) {
|
||
for (let j = 0; j < parsed.length; j++) {
|
||
const fixedUrl = fixImageUrl(parsed[j] as string)
|
||
if (fixedUrl !== '') images.push(fixedUrl)
|
||
}
|
||
}
|
||
} else {
|
||
const fixedUrl = fixImageUrl(rawUrlStr)
|
||
if (fixedUrl !== '' && images.indexOf(fixedUrl) === -1) images.push(fixedUrl)
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('解析图片数组失败:', e)
|
||
}
|
||
}
|
||
|
||
if (images.length === 0) {
|
||
images.push('/static/default-product.png')
|
||
}
|
||
|
||
let safePrice = item.getNumber('base_price')
|
||
if (safePrice == null) {
|
||
const p = item.getNumber('price')
|
||
safePrice = p != null ? p : 0
|
||
}
|
||
|
||
let safeOriginalPrice = item.getNumber('market_price')
|
||
if (safeOriginalPrice == null) {
|
||
const op = item.getNumber('original_price')
|
||
safeOriginalPrice = op != null ? op : safePrice
|
||
}
|
||
|
||
let safeStock = item.getNumber('total_stock')
|
||
if (safeStock == null) {
|
||
let as_ = item.getNumber('available_stock')
|
||
if (as_ == null) {
|
||
const s = item.getNumber('stock')
|
||
safeStock = s != null ? s : 0
|
||
} else {
|
||
safeStock = as_
|
||
}
|
||
}
|
||
|
||
let safeSales = item.getNumber('sale_count')
|
||
if (safeSales == null) {
|
||
const s = item.getNumber('sales')
|
||
safeSales = s != null ? s : 0
|
||
}
|
||
|
||
const product: Product = {
|
||
id: item.getString('id') ?? '',
|
||
category_id: item.getString('category_id') ?? '',
|
||
merchant_id: item.getString('merchant_id') ?? '',
|
||
name: item.getString('name') ?? '',
|
||
description: item.getString('description') ?? '',
|
||
images: images,
|
||
price: safePrice,
|
||
original_price: safeOriginalPrice,
|
||
stock: safeStock,
|
||
sales: safeSales,
|
||
status: item.getNumber('status') ?? 1,
|
||
created_at: item.getString('created_at') ?? '',
|
||
base_price: safePrice,
|
||
market_price: safeOriginalPrice,
|
||
main_image_url: images.length > 0 ? images[0] : '',
|
||
sale_count: safeSales,
|
||
total_stock: safeStock
|
||
} as Product
|
||
mappedData.push(product)
|
||
}
|
||
|
||
return {
|
||
data: mappedData,
|
||
total: res2.total ?? 0,
|
||
page,
|
||
limit,
|
||
hasmore: res2.hasmore ?? false
|
||
}
|
||
}
|
||
|
||
console.log(`Shop products found: ${(response.data as any[]).length}`)
|
||
|
||
const viewData = response.data as any[]
|
||
const parsedProducts: Product[] = []
|
||
for (let i = 0; i < viewData.length; i++) {
|
||
parsedProducts.push(parseProductFromRaw(viewData[i]))
|
||
}
|
||
|
||
return {
|
||
data: parsedProducts,
|
||
total: response.total ?? 0,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
console.error('获取店铺商品异常:', error)
|
||
return {
|
||
data: [] as Product[],
|
||
total: 0,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取热销商品(按销量排序)
|
||
async getHotProducts(limit: number = 10): Promise<Product[]> {
|
||
try {
|
||
console.log('[getHotProducts] 开始获取热销商品...')
|
||
|
||
// 在数据库层面过滤 status,获取更多数据以便手动过滤 is_hot
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*')
|
||
.eq('status', '1') // 使用字符串 '1'
|
||
.order('sale_count', { ascending: false })
|
||
.limit(limit * 5) // 获取更多数据以便过滤
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取热销商品失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
console.log('[getHotProducts] 原始数据条数:', rawData != null ? (rawData as any[]).length : 0)
|
||
if (rawData == null) {
|
||
console.log('[getHotProducts] 数据为空')
|
||
return []
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
// 手动过滤 is_hot
|
||
const rawIsHot = prodObj.get('is_hot')
|
||
let isHotBool: boolean = false
|
||
if (typeof rawIsHot == 'boolean') {
|
||
isHotBool = rawIsHot as boolean
|
||
} else if (typeof rawIsHot == 'number') {
|
||
isHotBool = (rawIsHot as number) == 1
|
||
}
|
||
if (!isHotBool) continue
|
||
|
||
products.push(parseProductFromRaw(item))
|
||
|
||
// 达到目标数量就停止
|
||
if (products.length >= limit) break
|
||
}
|
||
console.log('[getHotProducts] 最终返回商品数:', products.length)
|
||
return products
|
||
} catch (error) {
|
||
console.error('获取热销商品异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取按销量排序的商品(所有商品,不限制 is_hot)
|
||
async getProductsBySales(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getProductsBySales', 'ml_products_detail_view', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
console.log('[getProductsBySales] 开始获取销量排序商品...')
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', '1')
|
||
.order('sale_count', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getProductsBySales', 'ml_products_detail_view', response.error)
|
||
console.error('获取销量排序商品失败:', response.error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
products.push(parseProductFromRaw(item))
|
||
}
|
||
console.log('[getProductsBySales] 返回商品数:', products.length)
|
||
logConsumerQuerySuccess('getProductsBySales', 'ml_products_detail_view', products.length)
|
||
return {
|
||
data: products,
|
||
total: response.total ?? products.length,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getProductsBySales', 'ml_products_detail_view', error)
|
||
console.error('获取销量排序商品异常:', error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
// 获取按价格排序的商品(升序:从低到高)
|
||
async getProductsByPrice(page: number = 1, limit: number = 10, ascending: boolean = true): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getProductsByPrice', 'ml_products_detail_view', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', '1') // 在数据库层面过滤
|
||
.order('base_price', { ascending })
|
||
.page(page)
|
||
.limit(limit)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getProductsByPrice', 'ml_products_detail_view', response.error)
|
||
console.error('获取价格排序商品失败:', response.error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
products.push(parseProductFromRaw(item))
|
||
}
|
||
logConsumerQuerySuccess('getProductsByPrice', 'ml_products_detail_view', products.length)
|
||
return {
|
||
data: products,
|
||
total: response.total ?? products.length,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getProductsByPrice', 'ml_products_detail_view', error)
|
||
console.error('获取价格排序商品异常:', error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
// 获取新品(按创建时间排序,最新的在前)
|
||
async getProductsByNewest(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getProductsByNewest', 'ml_products_detail_view', '*')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
console.log('[getProductsByNewest] 开始获取新品...')
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('*', { count: 'exact' })
|
||
.eq('status', '1')
|
||
.order('is_new', { ascending: false })
|
||
.order('created_at', { ascending: false })
|
||
.page(page)
|
||
.limit(limit)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getProductsByNewest', 'ml_products_detail_view', response.error)
|
||
console.error('获取新品失败:', response.error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
products.push(parseProductFromRaw(item))
|
||
}
|
||
|
||
console.log('[getProductsByNewest] 返回商品数:', products.length)
|
||
logConsumerQuerySuccess('getProductsByNewest', 'ml_products_detail_view', products.length)
|
||
return {
|
||
data: products,
|
||
total: response.total ?? products.length,
|
||
page,
|
||
limit,
|
||
hasmore: response.hasmore ?? false
|
||
}
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getProductsByNewest', 'ml_products_detail_view', error)
|
||
console.error('获取新品异常:', error)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
// 获取推荐商品(is_featured=true)
|
||
async getRecommendedProducts(limit: number = 10): Promise<Product[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getRecommendedProducts', 'ml_products_detail_view', 'id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')) {
|
||
return []
|
||
}
|
||
console.log('[getRecommendedProducts] 开始获取推荐商品...')
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||
.eq('status', '1') // 在数据库层面过滤
|
||
.order('sale_count', { ascending: false })
|
||
.limit(limit * 5) // 获取更多数据以便过滤 is_featured
|
||
.execute()
|
||
|
||
console.log('[getRecommendedProducts] 查询完成')
|
||
|
||
if (response.error != null) {
|
||
logConsumerQueryFailure('getRecommendedProducts', 'ml_products_detail_view', response.error)
|
||
console.error('获取推荐商品失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
console.log('[getRecommendedProducts] 数据为空')
|
||
return []
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getRecommendedProducts] 数据条数:', rawList.length)
|
||
|
||
for (let i: number = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const rawFeatured = prodObj.get('is_featured')
|
||
|
||
let isFeaturedBool: boolean = false
|
||
if (typeof rawFeatured == 'boolean') {
|
||
isFeaturedBool = rawFeatured as boolean
|
||
} else if (typeof rawFeatured == 'number') {
|
||
isFeaturedBool = (rawFeatured as number) == 1
|
||
}
|
||
if (!isFeaturedBool) {
|
||
continue
|
||
}
|
||
|
||
products.push(parseProductFromRaw(item))
|
||
if (products.length >= limit) break
|
||
}
|
||
logConsumerQuerySuccess('getRecommendedProducts', 'ml_products_detail_view', products.length)
|
||
return products
|
||
} catch (error) {
|
||
logConsumerQueryFailure('getRecommendedProducts', 'ml_products_detail_view', error)
|
||
console.error('获取推荐商品异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
async getRecommendProducts(params: UTSJSONObject): Promise<Array<UTSJSONObject>> {
|
||
try {
|
||
const categoryId = params.getString('categoryId') ?? ''
|
||
const limitRaw = params.getNumber('limit')
|
||
const offsetRaw = params.getNumber('offset')
|
||
const limit = limitRaw != null && limitRaw > 0 ? limitRaw : 8
|
||
const offset = offsetRaw != null && offsetRaw >= 0 ? offsetRaw : 0
|
||
const excludeProductIdsRaw = params.get('excludeProductIds')
|
||
const excludeProductIds: Array<string> = []
|
||
|
||
if (excludeProductIdsRaw != null) {
|
||
const normalized = JSON.parse(JSON.stringify(excludeProductIdsRaw))
|
||
if (Array.isArray(normalized)) {
|
||
for (let i = 0; i < normalized.length; i++) {
|
||
const item = normalized[i]
|
||
if (item != null) {
|
||
const itemText = '' + item
|
||
if (itemText !== '') {
|
||
excludeProductIds.push(itemText)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const collected: Array<UTSJSONObject> = []
|
||
const collectedIds: Array<string> = []
|
||
const targetCount = offset + limit
|
||
const fetchLimit = limit > 20 ? limit : 20
|
||
|
||
const appendRows = (rows: any[]): void => {
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const product = parseProductFromRaw(rows[i])
|
||
const productId = product.id ?? ''
|
||
if (productId === '') {
|
||
continue
|
||
}
|
||
if (excludeProductIds.indexOf(productId) >= 0) {
|
||
continue
|
||
}
|
||
if (collectedIds.indexOf(productId) >= 0) {
|
||
continue
|
||
}
|
||
collectedIds.push(productId)
|
||
collected.push(JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
||
}
|
||
}
|
||
|
||
const fetchBatch = async (preferCategory: boolean): Promise<boolean> => {
|
||
let page = 1
|
||
while (collected.length < targetCount && page <= 10) {
|
||
let rows: any[] = []
|
||
if (preferCategory && categoryId !== '') {
|
||
const response = await this.getProductsByCategory(categoryId, page, fetchLimit)
|
||
rows = response.data as any[]
|
||
} else {
|
||
const response = await this.getProductsBySales(page, fetchLimit)
|
||
rows = response.data as any[]
|
||
}
|
||
|
||
if (rows == null) {
|
||
return true
|
||
}
|
||
if (rows.length === 0) {
|
||
return true
|
||
}
|
||
|
||
appendRows(rows)
|
||
|
||
if (rows.length < fetchLimit) {
|
||
return true
|
||
}
|
||
page += 1
|
||
}
|
||
return true
|
||
}
|
||
|
||
if (categoryId !== '') {
|
||
await fetchBatch(true)
|
||
}
|
||
if (collected.length < targetCount) {
|
||
await fetchBatch(false)
|
||
}
|
||
|
||
const result: Array<UTSJSONObject> = []
|
||
const end = collected.length < targetCount ? collected.length : targetCount
|
||
for (let i = offset; i < end; i++) {
|
||
result.push(collected[i])
|
||
}
|
||
return result
|
||
} 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[]> {
|
||
if (!logConsumerQueryStart('getDiscountProducts', 'ml_products_detail_view', '(none)')) {
|
||
return [] as Product[]
|
||
}
|
||
logConsumerQuerySuccess('getDiscountProducts', 'ml_products_detail_view', 0)
|
||
return [] as Product[] // 暂无特价字段
|
||
}
|
||
|
||
// 获取当前用户的购物车商品(关联商品和店铺信息)
|
||
async getCartItems(): Promise<CartItem[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
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 != null) {
|
||
console.error('获取购物车失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const cartData = response.data as any[]
|
||
// console.log('Raw Cart Data:', JSON.stringify(cartData))
|
||
|
||
if (cartData == null || 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 {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
pid = itemObj.getString('product_id') ?? ''
|
||
sid = itemObj.getString('sku_id') ?? ''
|
||
}
|
||
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) {
|
||
// Convert string array to any array for .in()
|
||
const productIdsAny: any[] = []
|
||
for(let i=0; i<productIds.length; i++) {
|
||
productIdsAny.push(productIds[i])
|
||
}
|
||
|
||
const productRes = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.in('id', productIdsAny)
|
||
.execute()
|
||
|
||
console.log('[getCartItems] 商品查询结果:', productRes.error != null ? 'error' : 'success', productRes.error)
|
||
if (productRes.error == null && productRes.data != null) {
|
||
const products = productRes.data as any[]
|
||
console.log('[getCartItems] 商品数量:', products.length)
|
||
for (let i = 0; i < products.length; i++) {
|
||
let p = products[i]
|
||
let pid: string = ''
|
||
let pname: string = ''
|
||
|
||
if (p instanceof UTSJSONObject) {
|
||
pid = p.getString('id') ?? ''
|
||
pname = p.getString('name') ?? ''
|
||
} else {
|
||
const pObj = JSON.parse(JSON.stringify(p)) as UTSJSONObject
|
||
pid = pObj.getString('id') ?? ''
|
||
pname = pObj.getString('name') ?? ''
|
||
}
|
||
|
||
console.log('[getCartItems] 商品:', pid, pname)
|
||
|
||
if (pid !== '') {
|
||
productMap.set(pid, p)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量查询店铺信息
|
||
const shopMap = new Map<string, string>()
|
||
const merchantIds: string[] = []
|
||
// 遍历 productMap 获取 merchant_id
|
||
productMap.forEach((p: any, pid: string) => {
|
||
let mid: string = ''
|
||
if (p instanceof UTSJSONObject) {
|
||
mid = p.getString('merchant_id') ?? ''
|
||
} else {
|
||
const pObj = JSON.parse(JSON.stringify(p)) as UTSJSONObject
|
||
mid = pObj.getString('merchant_id') ?? ''
|
||
}
|
||
if (mid !== '' && !merchantIds.includes(mid)) {
|
||
merchantIds.push(mid)
|
||
}
|
||
})
|
||
|
||
if (merchantIds.length > 0) {
|
||
const merchantIdsAny: any[] = []
|
||
for(let i=0; i<merchantIds.length; i++) {
|
||
merchantIdsAny.push(merchantIds[i])
|
||
}
|
||
const shopRes = await supa
|
||
.from('ml_shops')
|
||
.select('merchant_id,shop_name')
|
||
.in('merchant_id', merchantIdsAny)
|
||
.execute()
|
||
|
||
if (shopRes.error == null && shopRes.data != null) {
|
||
const shops = shopRes.data as any[]
|
||
for (let i = 0; i < shops.length; i++) {
|
||
let s = shops[i]
|
||
let mid: string = ''
|
||
let sname: string = ''
|
||
if (s instanceof UTSJSONObject) {
|
||
mid = s.getString('merchant_id') ?? ''
|
||
sname = s.getString('shop_name') ?? ''
|
||
} else {
|
||
const sObj = JSON.parse(JSON.stringify(s)) as UTSJSONObject
|
||
mid = sObj.getString('merchant_id') ?? ''
|
||
sname = sObj.getString('shop_name') ?? ''
|
||
}
|
||
if (mid !== '') {
|
||
shopMap.set(mid, sname)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量查询 SKU 详情
|
||
const skuMap = new Map<string, any>()
|
||
if (skuIds.length > 0) {
|
||
const skuIdsAny: any[] = []
|
||
for(let i=0; i<skuIds.length; i++) {
|
||
skuIdsAny.push(skuIds[i])
|
||
}
|
||
const skuRes = await supa
|
||
.from('ml_product_skus')
|
||
.select('*')
|
||
.in('id', skuIdsAny)
|
||
.execute()
|
||
|
||
if (skuRes.error == null && 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 {
|
||
const sObj = JSON.parse(JSON.stringify(s)) as UTSJSONObject
|
||
sid = sObj.getString('id') ?? ''
|
||
}
|
||
|
||
if (sid !== '') {
|
||
skuMap.set(sid, s)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理返回数据,构建CartItem数组
|
||
const cartItems: CartItem[] = []
|
||
if ((cartData as any[]) != null) { // Simplify: cartData is already any[]
|
||
const cartArray = cartData as any[]
|
||
for (let i = 0; i < cartArray.length; i++) {
|
||
let item = cartArray[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 = ''
|
||
let cartMerchantId: 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') ?? ''
|
||
cartMerchantId = item.getString('merchant_id') ?? ''
|
||
} else {
|
||
const iObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
itemId = iObj.getString('id') ?? ''
|
||
userIdVal = iObj.getString('user_id') ?? ''
|
||
productId = iObj.getString('product_id') ?? ''
|
||
skuId = iObj.getString('sku_id') ?? ''
|
||
quantity = iObj.getNumber('quantity') ?? 0
|
||
selected = iObj.getBoolean('selected') ?? false
|
||
createdAt = iObj.getString('created_at') ?? ''
|
||
updatedAt = iObj.getString('updated_at') ?? ''
|
||
cartMerchantId = iObj.getString('merchant_id') ?? ''
|
||
}
|
||
|
||
const product = productMap.get(productId)
|
||
const sku = (skuId !== '' && skuMap.has(skuId)) ? skuMap.get(skuId) : null
|
||
|
||
console.log('[getCartItems] 处理购物车项:', itemId, 'productId:', productId, 'product存在:', product != null, 'sku存在:', sku != null)
|
||
|
||
let merchantId: string = cartMerchantId
|
||
let productName: string = ''
|
||
let productImage: string = ''
|
||
let productPrice: number = 0
|
||
let productSpec: string = ''
|
||
let shopNameStr: string = '未知店铺'
|
||
|
||
if (product != null) {
|
||
console.log('[getCartItems] product类型:', typeof product, 'instanceof UTSJSONObject:', product instanceof UTSJSONObject)
|
||
if (product instanceof UTSJSONObject) {
|
||
// 优先使用购物车中的 merchant_id,如果没有则使用商品中的
|
||
if (merchantId == '') {
|
||
merchantId = product.getString('merchant_id') ?? ''
|
||
}
|
||
productName = product.getString('name') ?? ''
|
||
productImage = product.getString('main_image_url') ?? ''
|
||
productPrice = product.getNumber('base_price') ?? 0
|
||
console.log('[getCartItems] 从UTSJSONObject获取: name=', productName, 'price=', productPrice)
|
||
} else {
|
||
const pObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
|
||
if (merchantId == '') {
|
||
merchantId = pObj.getString('merchant_id') ?? ''
|
||
}
|
||
productName = pObj.getString('name') ?? ''
|
||
productImage = pObj.getString('main_image_url') ?? ''
|
||
productPrice = pObj.getNumber('base_price') ?? 0
|
||
console.log('[getCartItems] 从普通对象获取: name=', productName, 'price=', productPrice)
|
||
}
|
||
// 从 shopMap 获取店铺名称
|
||
if (merchantId !== '' && shopMap.has(merchantId)) {
|
||
shopNameStr = shopMap.get(merchantId) ?? '未知店铺'
|
||
}
|
||
}
|
||
|
||
// 如果有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) {
|
||
if (typeof specRaw === 'string') {
|
||
productSpec = specRaw
|
||
} else if (specRaw instanceof UTSJSONObject) {
|
||
const keys = ['规格', '颜色', '尺码', '容量', '版本', '型号']
|
||
const result: string[] = []
|
||
for (let k = 0; k < keys.length; k++) {
|
||
const key = keys[k]
|
||
const val = specRaw.get(key)
|
||
if (val != null && val !== '') {
|
||
result.push(`${val}`)
|
||
}
|
||
}
|
||
if (result.length > 0) {
|
||
productSpec = result.join(' ')
|
||
} else {
|
||
const allKeys = UTSJSONObject.keys(specRaw)
|
||
const parts: string[] = []
|
||
for(let k = 0; k < allKeys.length; k++) {
|
||
let val = specRaw.get(allKeys[k])
|
||
if (val != null) {
|
||
parts.push(`${val}`)
|
||
}
|
||
}
|
||
productSpec = parts.join(' ')
|
||
}
|
||
} else {
|
||
try {
|
||
let jsonStr = JSON.stringify(specRaw)
|
||
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, ' ').replace(/:/g, ' ')
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
} else {
|
||
const sObj = JSON.parse(JSON.stringify(sku)) as UTSJSONObject
|
||
const skuPrice = sObj.getNumber('price') ?? 0
|
||
if (skuPrice > 0) productPrice = skuPrice
|
||
|
||
const skuImg = sObj.getString('image_url') ?? ''
|
||
if (skuImg !== '') productImage = skuImg
|
||
|
||
const specRaw = sObj.get('specifications')
|
||
if (specRaw != null) {
|
||
if (typeof specRaw === 'string') {
|
||
productSpec = specRaw
|
||
} else if (specRaw instanceof UTSJSONObject) {
|
||
const keys = ['规格', '颜色', '尺码', '容量', '版本', '型号']
|
||
const result: string[] = []
|
||
for (let k = 0; k < keys.length; k++) {
|
||
const key = keys[k]
|
||
const val = specRaw.get(key)
|
||
if (val != null && val !== '') {
|
||
result.push(`${val}`)
|
||
}
|
||
}
|
||
if (result.length > 0) {
|
||
productSpec = result.join(' ')
|
||
} else {
|
||
const allKeys = UTSJSONObject.keys(specRaw)
|
||
const parts: string[] = []
|
||
for(let k = 0; k < allKeys.length; k++) {
|
||
let val = specRaw.get(allKeys[k])
|
||
if (val != null) {
|
||
parts.push(`${val}`)
|
||
}
|
||
}
|
||
productSpec = parts.join(' ')
|
||
}
|
||
} else {
|
||
try {
|
||
let jsonStr = JSON.stringify(specRaw)
|
||
productSpec = jsonStr.replace(/["{}]/g, '').replace(/,/g, ' ').replace(/:/g, ' ')
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let shopIdStr = merchantId != '' ? 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
|
||
} as CartItem)
|
||
}
|
||
}
|
||
|
||
return cartItems
|
||
} catch (error) {
|
||
console.error('获取购物车异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 标记单条通知为已读 (ml_notifications)
|
||
async markUserNotificationRead(notificationId: string): Promise<boolean> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_notifications')
|
||
.update({ is_read: true })
|
||
.eq('id', notificationId)
|
||
.execute()
|
||
return res.error == null
|
||
} catch (e) {
|
||
console.error('[markUserNotificationRead] 标记通知已读失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 标记聊天会话为已读
|
||
async markChatRoomRead(merchantId: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
const res = await supa
|
||
.from('ml_chat_rooms')
|
||
.update({ unread_count: 0 })
|
||
.eq('user_id', userId)
|
||
.eq('merchant_id', merchantId)
|
||
.execute()
|
||
return res.error == null
|
||
} catch (e) {
|
||
console.error('[markChatRoomRead] 标记会话已读失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 一键已读:所有通知 + 所有会话
|
||
async markAllMessagesRead(): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const noteRes = await supa
|
||
.from('ml_notifications')
|
||
.update({ is_read: true })
|
||
.eq('user_id', userId)
|
||
.eq('is_read', false)
|
||
.execute()
|
||
if (noteRes.error != null) {
|
||
console.error('[markAllMessagesRead] 通知已读失败:', noteRes.error)
|
||
}
|
||
|
||
const roomRes = await supa
|
||
.from('ml_chat_rooms')
|
||
.update({ unread_count: 0 })
|
||
.eq('user_id', userId)
|
||
.gt('unread_count', 0)
|
||
.execute()
|
||
if (roomRes.error != null) {
|
||
console.error('[markAllMessagesRead] 会话已读失败:', roomRes.error)
|
||
}
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('[markAllMessagesRead] 一键已读失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 软删除消息(前端兜底:后端表缺少软删除字段时仅 console.warn)
|
||
async softDeleteMessages(messageIds: Array<string>): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
if (messageIds.length === 0) return true
|
||
|
||
// TODO: 后端 ml_notifications / ml_chat_rooms 表当前缺少 consumer_deleted_at / deleted_at / user_deleted_at 字段
|
||
// 现阶段不执行数据库删除/更新,仅打印警告。如需持久化软删除,请先在相关表增加对应字段并补充 API。
|
||
console.warn('[softDeleteMessages] 后端缺少软删除字段/API,本次跳过数据库操作。messageIds:', messageIds)
|
||
return true
|
||
} catch (e) {
|
||
console.error('[softDeleteMessages] 软删除失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取用户通知 (系统、活动、订单)
|
||
async getUserNotifications(type: string | null = null): Promise<Notification[]> {
|
||
try {
|
||
console.log('[getUserNotifications] 开始获取通知')
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return []
|
||
|
||
let query = supa
|
||
.from('ml_notifications')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
|
||
if (type != null) {
|
||
query = query.eq('type', type)
|
||
}
|
||
|
||
const response = await query.order('created_at', { ascending: false }).execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取通知失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return []
|
||
|
||
const notifications: Notification[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getUserNotifications] 获取到通知数量:', rawList.length)
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const noteObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = noteObj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val
|
||
return ''
|
||
}
|
||
|
||
const getSafeNumber = (key: string): number => {
|
||
const val = noteObj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val == 'number') return val
|
||
return 0
|
||
}
|
||
|
||
const getSafeBoolean = (key: string): boolean => {
|
||
const val = noteObj.get(key)
|
||
if (val == null) return false
|
||
if (typeof val == 'boolean') return val
|
||
if (typeof val == 'number') return (val as number) == 1
|
||
return false
|
||
}
|
||
|
||
const note: Notification = {
|
||
id: getSafeString('id'),
|
||
user_id: getSafeString('user_id'),
|
||
type: getSafeString('type'),
|
||
title: getSafeString('title'),
|
||
content: getSafeString('content'),
|
||
is_read: getSafeBoolean('is_read'),
|
||
icon_url: getSafeString('icon_url'),
|
||
link_url: getSafeString('link_url'),
|
||
extra_data: getSafeString('extra_data'),
|
||
created_at: getSafeString('created_at')
|
||
}
|
||
notifications.push(note)
|
||
}
|
||
return notifications
|
||
} catch (e) {
|
||
console.error('获取通知异常:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取聊天会话列表
|
||
async getChatRooms(): Promise<ChatRoom[]> {
|
||
try {
|
||
console.log('[getChatRooms] 开始获取聊天会话')
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return []
|
||
|
||
const response = await supa
|
||
.from('ml_chat_rooms')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.order('updated_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取聊天会话失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return []
|
||
|
||
const rooms: ChatRoom[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getChatRooms] 获取到会话数量:', rawList.length)
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const roomObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = roomObj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val
|
||
return ''
|
||
}
|
||
|
||
const getSafeNumber = (key: string): number => {
|
||
const val = roomObj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val == 'number') return val
|
||
return 0
|
||
}
|
||
|
||
const getSafeBoolean = (key: string): boolean => {
|
||
const val = roomObj.get(key)
|
||
if (val == null) return false
|
||
if (typeof val == 'boolean') return val
|
||
if (typeof val == 'number') return (val as number) == 1
|
||
return false
|
||
}
|
||
|
||
const room: ChatRoom = {
|
||
id: getSafeString('id'),
|
||
user_id: getSafeString('user_id'),
|
||
merchant_id: getSafeString('merchant_id'),
|
||
shop_name: getSafeString('shop_name'),
|
||
shop_logo: getSafeString('shop_logo'),
|
||
last_message: getSafeString('last_message'),
|
||
last_message_at: getSafeString('last_message_at'),
|
||
unread_count: getSafeNumber('unread_count'),
|
||
is_top: getSafeBoolean('is_top'),
|
||
created_at: getSafeString('created_at'),
|
||
updated_at: getSafeString('updated_at')
|
||
}
|
||
rooms.push(room)
|
||
}
|
||
return rooms
|
||
} catch (e) {
|
||
console.error('获取聊天会话异常:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取用户聊天消息
|
||
async getUserChatMessages(): Promise<ChatMessage[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) 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 != null) {
|
||
console.error('获取聊天记录失败:', response.error)
|
||
return []
|
||
}
|
||
return response.data as ChatMessage[]
|
||
} catch (e) {
|
||
console.error('获取聊天记录异常:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 获取与特定商家的聊天记录 (合并版本)
|
||
async getChatMessages(merchantId: string, page: number = 1, pageSize: number = 20): Promise<ChatMessage[]> {
|
||
try {
|
||
console.log('[getChatMessages] 开始获取聊天记录,merchantId:', merchantId, 'page:', page)
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return []
|
||
|
||
const fromIndex = (page - 1) * pageSize
|
||
const toIndex = fromIndex + pageSize - 1
|
||
|
||
// 使用 or 组合精确条件查询:(我发给商家) OR (商家发给我)
|
||
const queryStr = `and(sender_id.eq.${userId},receiver_id.eq.${merchantId}),and(sender_id.eq.${merchantId},receiver_id.eq.${userId})`
|
||
|
||
const response = await supa
|
||
.from('ml_chat_messages')
|
||
.select('*')
|
||
.or(queryStr)
|
||
.order('created_at', { ascending: false }) // 最新在前
|
||
.range(fromIndex, toIndex)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('getChatMessages error:', response.error)
|
||
return []
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return []
|
||
|
||
const messages: ChatMessage[] = []
|
||
const rawList = rawData as any[]
|
||
console.log('[getChatMessages] 获取到消息数量:', rawList.length)
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const msgObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = msgObj.get(key)
|
||
if (val == null) return ''
|
||
return val.toString()
|
||
}
|
||
|
||
const getSafeBoolean = (key: string): boolean => {
|
||
const val = msgObj.get(key)
|
||
if (val == null) return false
|
||
if (typeof val == 'boolean') return val as boolean
|
||
return (val.toString() == '1' || val.toString() == 'true')
|
||
}
|
||
|
||
const msg: ChatMessage = {
|
||
id: getSafeString('id'),
|
||
session_id: getSafeString('session_id'),
|
||
sender_id: getSafeString('sender_id'),
|
||
receiver_id: getSafeString('receiver_id'),
|
||
content: getSafeString('content'),
|
||
msg_type: getSafeString('msg_type'),
|
||
is_read: getSafeBoolean('is_read'),
|
||
is_from_user: getSafeBoolean('is_from_user'),
|
||
extra_data: getSafeString('extra_data'),
|
||
created_at: getSafeString('created_at')
|
||
}
|
||
messages.push(msg)
|
||
}
|
||
return messages
|
||
} catch (e) {
|
||
console.error('获取聊天记录异常:', e)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 发送聊天消息
|
||
async sendChatMessage(content: string, toId: string | null = null, type: string = 'text'): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const payload = {
|
||
sender_id: userId,
|
||
content: content,
|
||
msg_type: type,
|
||
is_from_user: true,
|
||
created_at: new Date().toISOString()
|
||
} as UTSJSONObject
|
||
if (toId != null) {
|
||
payload.set('receiver_id', toId)
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_chat_messages')
|
||
.insert(payload)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('发送消息失败:', response.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('发送消息异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 模拟客服回复
|
||
async simulateServiceReply(content: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const response = await supa
|
||
.from('ml_chat_messages')
|
||
.insert({
|
||
receiver_id: userId,
|
||
content: content,
|
||
msg_type: 'text',
|
||
is_from_user: false,
|
||
created_at: new Date().toISOString()
|
||
})
|
||
.execute()
|
||
return response.error == null
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 添加商品到购物车
|
||
async addToCart(productId: string, quantity: number = 1, skuId: string = '', merchantId: string = ''): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法添加商品到购物车')
|
||
return false
|
||
}
|
||
|
||
const realSkuId = (skuId != null && skuId.length > 0) ? skuId : null
|
||
const realMerchantId = (merchantId != null && merchantId.length > 0) ? merchantId : null
|
||
|
||
// 检查商品是否已在购物车中
|
||
// 注意:必须处理 sku_id 为空的情况,使用 is.null 过滤器
|
||
let query = supa
|
||
.from('ml_shopping_cart')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.eq('product_id', productId)
|
||
|
||
if (realSkuId != null) {
|
||
query = query.eq('sku_id', realSkuId)
|
||
} 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: AkReqResponse<any>
|
||
if (existingItem != null) {
|
||
// 商品已存在,更新数量
|
||
console.log('Found existing cart item:', JSON.stringify(existingItem))
|
||
|
||
// 确保 existingItem.id 存在
|
||
let itemId: string | null = null
|
||
let itemQty: any | null = null
|
||
|
||
if (existingItem instanceof UTSJSONObject) {
|
||
itemId = existingItem.getString('id')
|
||
itemQty = existingItem.getNumber('quantity')
|
||
} else {
|
||
const obj = JSON.parse(JSON.stringify(existingItem)) as UTSJSONObject
|
||
itemId = obj.getString('id')
|
||
itemQty = obj.getNumber('quantity')
|
||
}
|
||
|
||
if (itemId != null) {
|
||
let currentQty = 0
|
||
if (typeof itemQty === 'number') {
|
||
currentQty = itemQty as number
|
||
} else {
|
||
const qStr = `${itemQty ?? 0}`
|
||
currentQty = parseInt(qStr)
|
||
}
|
||
const newQty = currentQty + quantity
|
||
|
||
response = await supa
|
||
.from('ml_shopping_cart')
|
||
.update({
|
||
quantity: newQty,
|
||
merchant_id: realMerchantId,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', itemId)
|
||
.execute()
|
||
} else {
|
||
console.error('购物车已有商品但缺少ID,无法更新. Data:', JSON.stringify(existingItem))
|
||
return false
|
||
}
|
||
} else {
|
||
// 商品不存在,添加新记录
|
||
const cartPayload = new UTSJSONObject()
|
||
cartPayload.set('user_id', userId)
|
||
cartPayload.set('product_id', productId)
|
||
cartPayload.set('sku_id', realSkuId)
|
||
cartPayload.set('quantity', quantity)
|
||
cartPayload.set('selected', true)
|
||
cartPayload.set('created_at', new Date().toISOString())
|
||
cartPayload.set('updated_at', new Date().toISOString())
|
||
if (realMerchantId != null) {
|
||
cartPayload.set('merchant_id', realMerchantId)
|
||
}
|
||
|
||
response = await supa
|
||
.from('ml_shopping_cart')
|
||
.insert(cartPayload)
|
||
.execute()
|
||
}
|
||
|
||
if (response.error != null) {
|
||
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 == null) {
|
||
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 != null) {
|
||
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 == null) {
|
||
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 != null) {
|
||
console.error('更新购物车商品选中状态失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('更新购物车商品选中状态异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 批量更新购物车商品选中状态
|
||
async batchUpdateCartItemSelection(cartItemIds: string[], selected: boolean): Promise<boolean> {
|
||
try {
|
||
console.log('[batchUpdateCartItemSelection] 开始批量更新')
|
||
console.log('[batchUpdateCartItemSelection] cartItemIds:', JSON.stringify(cartItemIds))
|
||
console.log('[batchUpdateCartItemSelection] cartItemIds length:', cartItemIds.length)
|
||
console.log('[batchUpdateCartItemSelection] selected:', selected)
|
||
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
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 as any[])
|
||
.execute()
|
||
|
||
console.log('[batchUpdateCartItemSelection] response.error:', response.error)
|
||
console.log('[batchUpdateCartItemSelection] response.data:', JSON.stringify(response.data))
|
||
|
||
if (response.error != null) {
|
||
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 == null) {
|
||
console.error('用户未登录,无法删除购物车商品')
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_shopping_cart')
|
||
.eq('id', cartItemId)
|
||
.eq('user_id', userId)
|
||
.delete()
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('删除购物车商品失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('删除购物车商品异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 批量删除购物车商品
|
||
async batchDeleteCartItems(cartItemIds: string[]): Promise<boolean> {
|
||
try {
|
||
console.log('[batchDeleteCartItems] 开始删除, ids:', cartItemIds.length)
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法删除购物车商品')
|
||
return false
|
||
}
|
||
|
||
console.log('[batchDeleteCartItems] userId:', userId)
|
||
const response = await supa
|
||
.from('ml_shopping_cart')
|
||
.eq('user_id', userId)
|
||
.in('id', cartItemIds as any[])
|
||
.delete()
|
||
.execute()
|
||
|
||
console.log('[batchDeleteCartItems] response.error:', response.error)
|
||
if (response.error != null) {
|
||
console.error('批量删除购物车商品失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
console.log('[batchDeleteCartItems] 删除成功')
|
||
return true
|
||
} catch (error) {
|
||
console.error('批量删除购物车商品异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 清空购物车
|
||
async clearCart(): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法清空购物车')
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_shopping_cart')
|
||
.eq('user_id', userId)
|
||
.delete()
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('清空购物车失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('清空购物车异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取当前用户的所有地址
|
||
async getAddresses(): Promise<UserAddress[]> {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.warn('[getAddresses] 用户未登录,无法获取地址')
|
||
return []
|
||
}
|
||
|
||
try {
|
||
console.log('[getAddresses] 查询地址, userId:', userId)
|
||
|
||
const response = await supa
|
||
.from('ml_user_addresses')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.order('is_default', { ascending: false })
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
console.log('[getAddresses] response.error:', response.error)
|
||
console.log('[getAddresses] response.data:', JSON.stringify(response.data))
|
||
|
||
if (response.error != null) {
|
||
console.error('[getAddresses] 获取地址失败:', response.error)
|
||
return []
|
||
}
|
||
|
||
const data = response.data
|
||
if (data == null) {
|
||
return []
|
||
}
|
||
|
||
const result: UserAddress[] = []
|
||
const rawData = data as any[]
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i]
|
||
let itemObj: UTSJSONObject
|
||
if (item instanceof UTSJSONObject) {
|
||
itemObj = item as UTSJSONObject
|
||
} else {
|
||
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
}
|
||
|
||
const addr: UserAddress = {
|
||
id: itemObj.getString('id') ?? '',
|
||
user_id: itemObj.getString('user_id') ?? '',
|
||
recipient_name: itemObj.getString('receiver_name') ?? itemObj.getString('recipient_name') ?? '',
|
||
phone: itemObj.getString('receiver_phone') ?? itemObj.getString('phone') ?? '',
|
||
province: itemObj.getString('province') ?? '',
|
||
city: itemObj.getString('city') ?? '',
|
||
district: itemObj.getString('district') ?? '',
|
||
detail_address: itemObj.getString('address_detail') ?? itemObj.getString('detail_address') ?? '',
|
||
is_default: itemObj.getBoolean('is_default') ?? false,
|
||
label: itemObj.getString('label') ?? '',
|
||
latitude: itemObj.getNumber('latitude') ?? 0,
|
||
longitude: itemObj.getNumber('longitude') ?? 0,
|
||
coordinate_type: itemObj.getString('coordinate_type') ?? 'gcj02',
|
||
created_at: itemObj.getString('created_at') ?? '',
|
||
updated_at: itemObj.getString('updated_at') ?? ''
|
||
}
|
||
result.push(addr)
|
||
}
|
||
|
||
console.log('[getAddresses] 返回地址数量:', result.length)
|
||
return result
|
||
} catch (error) {
|
||
console.error('[getAddresses] 获取地址异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 根据ID获取地址详情
|
||
async getAddressById(addressId: string): Promise<UserAddress | null> {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.warn('用户未登录,无法获取地址')
|
||
return null
|
||
}
|
||
|
||
try {
|
||
const query = 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()
|
||
|
||
const response = await query.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取地址详情失败:', response.error)
|
||
return null
|
||
}
|
||
|
||
return response.data as UserAddress
|
||
} catch (error) {
|
||
console.error('获取地址详情异常:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 添加新地址
|
||
async addAddress(address: AddAddressParams): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法添加地址')
|
||
return false
|
||
}
|
||
|
||
// 如果设置为默认地址,需要先取消其他默认地址
|
||
if (address.is_default == true) {
|
||
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,
|
||
latitude: address.latitude ?? null,
|
||
longitude: address.longitude ?? null,
|
||
coordinate_type: address.coordinate_type ?? 'gcj02',
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('添加地址失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('添加地址异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 更新地址
|
||
async updateAddress(addressId: string, address: UpdateAddressParams): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法更新地址')
|
||
return false
|
||
}
|
||
|
||
// 如果设置为默认地址,需要先取消其他默认地址
|
||
if (address.is_default == true) {
|
||
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
|
||
if (address.label != null) updateData['label'] = address.label
|
||
if (address.latitude != null) updateData['latitude'] = address.latitude
|
||
if (address.longitude != null) updateData['longitude'] = address.longitude
|
||
if (address.coordinate_type != null) updateData['coordinate_type'] = address.coordinate_type
|
||
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 != null) {
|
||
console.error('更新地址失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('更新地址异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 确认收货
|
||
async confirmReceipt(orderId: string): Promise<ConfirmReceiptResponse> {
|
||
console.log('[confirmReceipt] 开始确认收货, orderId:', orderId)
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
console.log('[confirmReceipt] userId:', userId)
|
||
if (userId == null) {
|
||
return { success: false, error: '用户未登录' }
|
||
}
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('order_status', 4)
|
||
updateData.set('delivered_at', new Date().toISOString())
|
||
updateData.set('completed_at', new Date().toISOString())
|
||
updateData.set('updated_at', new Date().toISOString())
|
||
|
||
console.log('[confirmReceipt] 准备更新订单状态, updateData:', JSON.stringify(updateData))
|
||
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.update(updateData)
|
||
.eq('id', orderId)
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
|
||
console.log('[confirmReceipt] response.status:', response.status)
|
||
console.log('[confirmReceipt] response.error:', response.error)
|
||
console.log('[confirmReceipt] response.data:', JSON.stringify(response.data))
|
||
|
||
// 检查 HTTP 状态码
|
||
if (response.status != null && response.status >= 400) {
|
||
// 尝试从 response.data 中提取错误信息
|
||
let errorMsg = '请求失败'
|
||
if (response.data != null) {
|
||
try {
|
||
const errorData = response.data as UTSJSONObject
|
||
const msg = errorData.getString('message')
|
||
if (msg != null) {
|
||
errorMsg = msg
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
console.log('[confirmReceipt] HTTP错误:', response.status, errorMsg)
|
||
return { success: false, error: errorMsg }
|
||
}
|
||
|
||
if (response.error != null) {
|
||
return { success: false, error: response.error.message }
|
||
}
|
||
|
||
// 检查 response.data 是否包含错误代码
|
||
if (response.data != null) {
|
||
try {
|
||
const respData = response.data as UTSJSONObject
|
||
const errorCode = respData.getString('code')
|
||
if (errorCode != null) {
|
||
const errorMsg = respData.getString('message') ?? '数据库错误'
|
||
console.log('[confirmReceipt] 数据库错误:', errorCode, errorMsg)
|
||
return { success: false, error: errorMsg }
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
// 检查是否有数据被更新
|
||
if (response.data == null || (Array.isArray(response.data) && response.data.length === 0)) {
|
||
console.log('[confirmReceipt] 没有数据被更新,可能订单不存在或无权限')
|
||
return { success: false, error: '订单不存在或无权限更新' }
|
||
}
|
||
|
||
console.log('[confirmReceipt] 确认收货成功')
|
||
return { success: true }
|
||
} catch (e: any) {
|
||
console.error('[confirmReceipt] 异常:', e)
|
||
return { success: false, error: e.message }
|
||
}
|
||
}
|
||
|
||
// 取消订单
|
||
async cancelOrder(orderId: string, reason: string = '用户取消订单'): Promise<boolean> {
|
||
return await this.cancelUnifiedOrder(orderId, 'goods', reason)
|
||
}
|
||
|
||
// 删除订单
|
||
async deleteOrder(orderId: string): Promise<boolean> {
|
||
return await this.softDeleteUnifiedOrderForConsumer(orderId, 'goods')
|
||
}
|
||
|
||
async softDeleteOrderForConsumer(orderId: string): Promise<boolean> {
|
||
return await this.softDeleteUnifiedOrderForConsumer(orderId, 'goods')
|
||
}
|
||
|
||
// 确认收货
|
||
async confirmOrderReceived(orderId: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.update({
|
||
order_status: 4,
|
||
shipping_status: 3,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', orderId)
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('确认收货失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('确认收货异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 删除地址
|
||
async deleteAddress(addressId: string): Promise<boolean> {
|
||
try {
|
||
console.log('正在执行删除地址,ID:', addressId)
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('用户未登录,无法删除地址')
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_user_addresses')
|
||
.eq('id', addressId)
|
||
.eq('user_id', userId)
|
||
.delete()
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
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 == null) return null
|
||
|
||
// 联合查询 auth user 和 profile
|
||
// 由于 Supabase auth table 不可直接访问,这里查询 ml_user_profiles
|
||
// 注意:使用 limit(1) 替代 single() 以避免 Android 端类型转换错误
|
||
const response = await supa
|
||
.from('ml_user_profiles')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
// 如果不存在 profile,可能只有 auth user,这里暂时返回空或创建默认
|
||
return null
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return null
|
||
|
||
// 作为数组处理
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) return null
|
||
|
||
return rawList[0]
|
||
} catch (e) {
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 创建订单
|
||
async createOrder(orderData: CreateOrderParams): Promise<string | null> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('CreateOrder: User not logged in')
|
||
return null
|
||
}
|
||
|
||
const orderNo = 'ML' + Date.now() + Math.floor(Math.random() * 1000)
|
||
|
||
let merchantId = orderData.merchant_id
|
||
console.log('[CreateOrder] 原始 merchant_id:', merchantId)
|
||
if (merchantId == null || merchantId == '' || merchantId == 'unknown') {
|
||
console.warn('[CreateOrder] merchant_id 为空或无效,将使用 userId 作为 fallback')
|
||
merchantId = userId
|
||
}
|
||
console.log('[CreateOrder] 最终使用的 merchant_id:', merchantId)
|
||
|
||
let shippingAddrStr = '{}'
|
||
if (orderData.shipping_address != null) {
|
||
if (typeof orderData.shipping_address === 'string') {
|
||
shippingAddrStr = orderData.shipping_address
|
||
} else {
|
||
shippingAddrStr = JSON.stringify(orderData.shipping_address)
|
||
}
|
||
}
|
||
|
||
const orderPayload = new UTSJSONObject()
|
||
orderPayload.set('user_id', userId)
|
||
orderPayload.set('merchant_id', merchantId)
|
||
orderPayload.set('order_no', orderNo)
|
||
orderPayload.set('product_amount', orderData.product_amount)
|
||
orderPayload.set('shipping_fee', orderData.shipping_fee)
|
||
orderPayload.set('total_amount', orderData.total_amount)
|
||
orderPayload.set('paid_amount', 0)
|
||
orderPayload.set('shipping_address', shippingAddrStr)
|
||
orderPayload.set('order_status', ORDER_STATUS_PENDING)
|
||
orderPayload.set('payment_status', PAYMENT_STATUS_UNPAID)
|
||
orderPayload.set('pay_expire_at', new Date(Date.now() + ORDER_PAY_TIMEOUT_SECONDS * 1000).toISOString())
|
||
orderPayload.set('consumer_deleted_at', null)
|
||
orderPayload.set('shipping_status', 1)
|
||
orderPayload.set('created_at', new Date().toISOString())
|
||
orderPayload.set('updated_at', new Date().toISOString())
|
||
|
||
console.log('[CreateOrder] 插入订单数据:', JSON.stringify(orderPayload))
|
||
console.log('[CreateOrder] 期望的订单号:', orderNo)
|
||
|
||
const orderResponse = await supa
|
||
.from('ml_orders')
|
||
.insert(orderPayload)
|
||
.execute()
|
||
|
||
console.log('[CreateOrder] insert 完成')
|
||
console.log('[CreateOrder] orderResponse.error:', orderResponse.error)
|
||
|
||
if (orderResponse.error != null) {
|
||
console.error('[CreateOrder] 创建订单失败:', orderResponse.error)
|
||
return null
|
||
}
|
||
|
||
console.log('[CreateOrder] 开始查询新创建的订单, order_no:', orderNo)
|
||
|
||
const queryResponse = await supa
|
||
.from('ml_orders')
|
||
.select('id, order_no')
|
||
.eq('order_no', orderNo)
|
||
.execute()
|
||
|
||
console.log('[CreateOrder] queryResponse.error:', queryResponse.error)
|
||
console.log('[CreateOrder] queryResponse.data:', JSON.stringify(queryResponse.data))
|
||
|
||
if (queryResponse.error != null) {
|
||
console.error('[CreateOrder] 查询订单失败:', queryResponse.error)
|
||
return null
|
||
}
|
||
|
||
const queryData = queryResponse.data as any
|
||
let orderId = ''
|
||
|
||
console.log('[CreateOrder] queryData 类型:', typeof queryData, '是否数组:', Array.isArray(queryData))
|
||
|
||
if (Array.isArray(queryData) && queryData.length > 0) {
|
||
console.log('[CreateOrder] queryData 长度:', queryData.length)
|
||
const firstItemRaw = queryData[0]
|
||
console.log('[CreateOrder] firstItemRaw 类型:', typeof firstItemRaw)
|
||
|
||
// 将 firstItemRaw 转换为可访问的对象
|
||
const firstItemStr = JSON.stringify(firstItemRaw)
|
||
const firstItemParsed = JSON.parse(firstItemStr)
|
||
if (firstItemParsed == null) {
|
||
console.error('[CreateOrder] 解析订单数据失败')
|
||
return null
|
||
}
|
||
const firstItem = firstItemParsed as UTSJSONObject
|
||
orderId = (firstItem.getString('id') ?? '') as string
|
||
console.log('[CreateOrder] 找到新创建的订单, id:', orderId)
|
||
} else {
|
||
console.error('[CreateOrder] 未找到新创建的订单,插入可能失败')
|
||
return null
|
||
}
|
||
|
||
console.log('[CreateOrder] 订单创建成功, orderId:', orderId)
|
||
|
||
const orderItems: UTSJSONObject[] = []
|
||
console.log('[CreateOrder] orderData.items 类型:', typeof orderData.items, '是否数组:', Array.isArray(orderData.items))
|
||
|
||
if (orderData.items == null) {
|
||
console.error('[CreateOrder] orderData.items 为 null!')
|
||
return orderId
|
||
}
|
||
|
||
const rawItems = orderData.items as any[]
|
||
console.log('[CreateOrder] rawItems 长度:', rawItems.length)
|
||
|
||
if (rawItems.length === 0) {
|
||
console.warn('[CreateOrder] rawItems 为空数组,没有商品项需要插入')
|
||
return orderId
|
||
}
|
||
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
const rawItem = rawItems[i]
|
||
const itemStr = JSON.stringify(rawItem)
|
||
const itemParsed = JSON.parse(itemStr)
|
||
if (itemParsed == null) {
|
||
console.error('[CreateOrder] 商品项解析失败')
|
||
continue
|
||
}
|
||
const item = itemParsed as UTSJSONObject
|
||
|
||
const itemJson = new UTSJSONObject()
|
||
|
||
let pId = item.get('product_id')
|
||
if (pId == null) {
|
||
pId = item.get('id')
|
||
}
|
||
const productId = (pId ?? '') as string
|
||
|
||
itemJson.set('order_id', orderId)
|
||
itemJson.set('product_id', productId)
|
||
|
||
const skuIdVal = item.get('sku_id')
|
||
if (skuIdVal != null && skuIdVal !== '') {
|
||
itemJson.set('sku_id', skuIdVal as string)
|
||
}
|
||
|
||
const productName = (item.get('product_name') ?? '') as string
|
||
itemJson.set('product_name', productName)
|
||
|
||
const sName = item.get('sku_name')
|
||
itemJson.set('sku_name', (sName ?? '') as string)
|
||
|
||
const specVal = item.get('specifications')
|
||
let skuSnapshot = '{}'
|
||
if (specVal != null) {
|
||
if (typeof specVal === 'string') {
|
||
skuSnapshot = specVal as string
|
||
} else {
|
||
skuSnapshot = JSON.stringify(specVal)
|
||
}
|
||
}
|
||
itemJson.set('sku_snapshot', skuSnapshot)
|
||
itemJson.set('specifications', skuSnapshot)
|
||
|
||
const img1 = item.get('product_image')
|
||
const img2 = item.get('image_url')
|
||
let imgUrl = ((img1 ?? img2 ?? '') as string)
|
||
while (imgUrl.indexOf('`') >= 0) {
|
||
imgUrl = imgUrl.replace('`', '')
|
||
}
|
||
itemJson.set('image_url', imgUrl)
|
||
|
||
const iPriceRaw = item.getNumber('price') ?? 0
|
||
const iMemberPrice = item.getNumber('member_price') ?? 0
|
||
// 优先使用会员价
|
||
const iPrice = (iMemberPrice > 0 && iMemberPrice < iPriceRaw) ? iMemberPrice : iPriceRaw
|
||
const iQty = item.getNumber('quantity') ?? 1
|
||
itemJson.set('price', iPrice)
|
||
itemJson.set('quantity', iQty)
|
||
itemJson.set('total_amount', iPrice * iQty)
|
||
itemJson.set('created_at', new Date().toISOString())
|
||
|
||
orderItems.push(itemJson)
|
||
}
|
||
|
||
console.log('[CreateOrder] 插入订单项数量:', orderItems.length)
|
||
|
||
for (let j: number = 0; j < orderItems.length; j++) {
|
||
console.log('[CreateOrder] 开始插入订单项', j)
|
||
const itemJson = orderItems[j]
|
||
// 将 UTSJSONObject 转换为普通对象
|
||
console.log('[CreateOrder] 序列化订单项...')
|
||
const itemObjStr = JSON.stringify(itemJson)
|
||
console.log('[CreateOrder] 订单项 JSON:', itemObjStr)
|
||
const itemObjParsed = JSON.parse(itemObjStr)
|
||
console.log('[CreateOrder] itemObjParsed 类型:', typeof itemObjParsed)
|
||
if (itemObjParsed == null) {
|
||
console.error('[CreateOrder] 订单项转换失败')
|
||
continue
|
||
}
|
||
|
||
// 使用 UTSJSONObject 而不是 Record<string, any>
|
||
const itemObj = itemObjParsed as UTSJSONObject
|
||
console.log('[CreateOrder] 执行 insert...')
|
||
|
||
const itemsResponse = await supa
|
||
.from('ml_order_items')
|
||
.insert(itemObj)
|
||
.execute()
|
||
|
||
console.log('[CreateOrder] insert 完成, error:', itemsResponse.error)
|
||
if (itemsResponse.error != null) {
|
||
console.error('[CreateOrder] 创建订单项失败:', itemsResponse.error)
|
||
}
|
||
}
|
||
|
||
console.log('[CreateOrder] 订单项创建成功')
|
||
|
||
const cartItemIds: string[] = []
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
const rawItem = rawItems[i]
|
||
const itemParsed = JSON.parse(JSON.stringify(rawItem))
|
||
if (itemParsed == null) continue
|
||
const item = itemParsed as UTSJSONObject
|
||
const iid = item.getString('id')
|
||
if (iid != null && iid.length > 10) {
|
||
cartItemIds.push(iid)
|
||
}
|
||
}
|
||
|
||
if (cartItemIds.length > 0) {
|
||
await this.batchDeleteCartItems(cartItemIds)
|
||
}
|
||
|
||
return orderId
|
||
} catch (error) {
|
||
console.error('[CreateOrder] 创建订单异常:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async ensurePayExpireAt(orderId: string): Promise<string> {
|
||
return await this.ensureUnifiedPayExpireAt(orderId, 'goods')
|
||
}
|
||
|
||
async markOrderPaymentCancelled(orderId: string): Promise<boolean> {
|
||
return await this.markUnifiedOrderPaymentCancelled(orderId, 'goods')
|
||
}
|
||
|
||
async expireOrder(orderId: string): Promise<boolean> {
|
||
return await this.expireUnifiedOrder(orderId, 'goods')
|
||
}
|
||
|
||
// 批量通过店铺创建订单
|
||
async createOrdersByShop(params: ShopOrderParams): Promise<ShopOrderResponse> {
|
||
try {
|
||
const orderIds: string[] = []
|
||
const groups = params.shopGroups as any[]
|
||
|
||
let grandTotal = 0.0
|
||
for(let k = 0; k < groups.length; k++) {
|
||
const g = JSON.parse(JSON.stringify(groups[k])) as UTSJSONObject
|
||
const gItemsRaw = g.get('items')
|
||
if (gItemsRaw == null) continue
|
||
const gItems = gItemsRaw as any[]
|
||
|
||
for(let gi = 0; gi < gItems.length; gi++) {
|
||
const it = JSON.parse(JSON.stringify(gItems[gi])) as UTSJSONObject
|
||
// 优先使用会员价
|
||
let itPrice = it.getNumber('price') ?? 0
|
||
const itMemberPrice = it.getNumber('member_price') ?? 0
|
||
if (itMemberPrice > 0 && itMemberPrice < itPrice) {
|
||
itPrice = itMemberPrice
|
||
}
|
||
const itQty = it.getNumber('quantity') ?? 1
|
||
grandTotal += itPrice * itQty
|
||
}
|
||
}
|
||
|
||
// 为每个店铺创建一个订单
|
||
for (let i = 0; i < groups.length; i++) {
|
||
const group = JSON.parse(JSON.stringify(groups[i])) as UTSJSONObject
|
||
const shopItemsRaw = group.get('items')
|
||
if (shopItemsRaw == null) continue
|
||
const shopItems = shopItemsRaw as any[]
|
||
|
||
let productAmount = 0.0
|
||
for(let j = 0; j < shopItems.length; j++) {
|
||
const sItem = JSON.parse(JSON.stringify(shopItems[j])) as UTSJSONObject
|
||
// 优先使用会员价
|
||
let siPrice = sItem.getNumber('price') ?? 0
|
||
const siMemberPrice = sItem.getNumber('member_price') ?? 0
|
||
if (siMemberPrice > 0 && siMemberPrice < siPrice) {
|
||
siPrice = siMemberPrice
|
||
}
|
||
const siQty = sItem.getNumber('quantity') ?? 1
|
||
productAmount += siPrice * siQty
|
||
}
|
||
|
||
// 简单平摊运费和优惠 (实际逻辑可能更复杂)
|
||
const ratio = grandTotal > 0 ? (productAmount / grandTotal) : 0
|
||
const shopShippingFee = params.deliveryFee * ratio
|
||
const shopDiscount = params.discountAmount * ratio
|
||
const shopTotal = productAmount + shopShippingFee - shopDiscount
|
||
|
||
const mId = group.getString('merchant_id')
|
||
const sId = group.getString('shopId')
|
||
const shopName = group.getString('shopName')
|
||
|
||
console.log('[createOrdersByShop] 店铺组信息:', {
|
||
merchant_id: mId,
|
||
shopId: sId,
|
||
shopName: shopName
|
||
})
|
||
|
||
const finalMerchantId = (mId != null && mId != '') ? mId : (sId ?? '')
|
||
console.log('[createOrdersByShop] 最终使用的 merchant_id:', finalMerchantId)
|
||
|
||
// 将 shopItems 转换为普通对象数组
|
||
const plainItems: any[] = []
|
||
for(let k = 0; k < shopItems.length; k++) {
|
||
const plainItemRaw = JSON.parse(JSON.stringify(shopItems[k]))
|
||
if (plainItemRaw != null) {
|
||
plainItems.push(plainItemRaw as any)
|
||
}
|
||
}
|
||
console.log('[createOrdersByShop] plainItems 数量:', plainItems.length)
|
||
|
||
const orderId = await this.createOrder({
|
||
merchant_id: finalMerchantId,
|
||
product_amount: productAmount,
|
||
shipping_fee: shopShippingFee,
|
||
total_amount: shopTotal,
|
||
shipping_address: params.shipping_address,
|
||
items: plainItems
|
||
})
|
||
|
||
if (orderId != null) {
|
||
orderIds.push(orderId)
|
||
} else {
|
||
return { success: false, orderIds, error: `店铺 ${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 == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
// 关联查询店铺表获取店铺名称
|
||
let query = supa
|
||
.from('ml_orders')
|
||
.select('*, ml_order_items(*), ml_shops(shop_name)')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.order('created_at', { ascending: false })
|
||
|
||
if (status > 0) {
|
||
query = query.eq('order_status', status)
|
||
}
|
||
|
||
const response = await query.execute()
|
||
|
||
console.log('[getOrders] response.error:', response.error)
|
||
if (response.data != null && Array.isArray(response.data)) {
|
||
console.log('[getOrders] 订单数量:', response.data.length)
|
||
}
|
||
|
||
if (response.error != null) {
|
||
console.error('获取订单列表失败:', response.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const data = response.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
// 修复订单项中的图片URL
|
||
const orders = data as any[]
|
||
for (let i = 0; i < orders.length; i++) {
|
||
const order = orders[i]
|
||
const orderStr = JSON.stringify(order)
|
||
const orderObj = JSON.parse(orderStr) as UTSJSONObject
|
||
const itemsRaw = orderObj.get('ml_order_items')
|
||
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||
const items = itemsRaw as any[]
|
||
for (let j = 0; j < items.length; j++) {
|
||
const item = items[j]
|
||
const itemStr = JSON.stringify(item)
|
||
const itemObj = JSON.parse(itemStr) as UTSJSONObject
|
||
const imgUrl = itemObj.getString('image_url')
|
||
if (imgUrl != null) {
|
||
itemObj['image_url'] = fixImageUrl(imgUrl)
|
||
}
|
||
const prodImg = itemObj.getString('product_image')
|
||
if (prodImg != null) {
|
||
itemObj['product_image'] = fixImageUrl(prodImg)
|
||
}
|
||
items[j] = itemObj
|
||
}
|
||
orderObj['ml_order_items'] = items
|
||
orders[i] = orderObj
|
||
}
|
||
}
|
||
|
||
return orders
|
||
} catch (error) {
|
||
console.error('获取订单列表异常:', error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
private normalizeOrderList(orderList: UTSJSONObject[]): UTSJSONObject[] {
|
||
for (let i = 0; i < orderList.length; i++) {
|
||
const order = orderList[i]
|
||
const orderStr = JSON.stringify(order)
|
||
const orderObj = JSON.parse(orderStr) as UTSJSONObject
|
||
const itemsRaw = orderObj.get('ml_order_items')
|
||
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||
const items = itemsRaw as UTSJSONObject[]
|
||
for (let j = 0; j < items.length; j++) {
|
||
const item = items[j]
|
||
const itemStr = JSON.stringify(item)
|
||
const itemObj = JSON.parse(itemStr) as UTSJSONObject
|
||
const imgUrl = itemObj.getString('image_url')
|
||
if (imgUrl != null) {
|
||
itemObj['image_url'] = fixImageUrl(imgUrl)
|
||
}
|
||
const prodImg = itemObj.getString('product_image')
|
||
if (prodImg != null) {
|
||
itemObj['product_image'] = fixImageUrl(prodImg)
|
||
}
|
||
items[j] = itemObj
|
||
}
|
||
orderObj['ml_order_items'] = items
|
||
orderList[i] = orderObj
|
||
}
|
||
}
|
||
return orderList
|
||
}
|
||
|
||
private appendUniqueString(list: string[], value: string | null): void {
|
||
if (value == null || value == '') {
|
||
return
|
||
}
|
||
for (let i = 0; i < list.length; i++) {
|
||
if (list[i] == value) {
|
||
return
|
||
}
|
||
}
|
||
list.push(value)
|
||
}
|
||
|
||
private async getKeywordMatchedOrderIds(userId: string, keyword: string): Promise<string[]> {
|
||
const matchedIds: string[] = []
|
||
const keywordPattern = '%' + keyword + '%'
|
||
|
||
try {
|
||
const orderResponse = await supa
|
||
.from('ml_orders')
|
||
.select('id')
|
||
.eq('user_id', userId)
|
||
.ilike('order_no', keywordPattern)
|
||
.limit(200)
|
||
.execute()
|
||
|
||
if (orderResponse.error == null && orderResponse.data != null && Array.isArray(orderResponse.data)) {
|
||
const orderRows = orderResponse.data as UTSJSONObject[]
|
||
for (let i = 0; i < orderRows.length; i++) {
|
||
const rowStr = JSON.stringify(orderRows[i])
|
||
const rowObj = JSON.parse(rowStr) as UTSJSONObject
|
||
this.appendUniqueString(matchedIds, rowObj.getString('id'))
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[getKeywordMatchedOrderIds] 订单号搜索异常:', error)
|
||
}
|
||
|
||
try {
|
||
const itemResponse = await supa
|
||
.from('ml_order_items')
|
||
.select('order_id')
|
||
.ilike('product_name', keywordPattern)
|
||
.limit(200)
|
||
.execute()
|
||
|
||
if (itemResponse.error == null && itemResponse.data != null && Array.isArray(itemResponse.data)) {
|
||
const itemRows = itemResponse.data as UTSJSONObject[]
|
||
for (let i = 0; i < itemRows.length; i++) {
|
||
const rowStr = JSON.stringify(itemRows[i])
|
||
const rowObj = JSON.parse(rowStr) as UTSJSONObject
|
||
this.appendUniqueString(matchedIds, rowObj.getString('order_id'))
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[getKeywordMatchedOrderIds] 商品名搜索异常:', error)
|
||
}
|
||
|
||
return matchedIds
|
||
}
|
||
|
||
private normalizeServiceStatus(status: string): string {
|
||
if (status == 'ORDER_CREATED') return 'created'
|
||
if (status == 'ORDER_ASSIGNED') return 'assigned'
|
||
if (status == 'ORDER_ACCEPTED') return 'accepted'
|
||
if (status == 'ORDER_REJECTED') return 'rejected'
|
||
if (status == 'ORDER_CHECKED_IN') return 'arrived'
|
||
if (status == 'ORDER_IN_SERVICE') return 'in_service'
|
||
if (status == 'ORDER_COMPLETED' || status == 'ACCEPTANCE_PENDING') return 'pending_acceptance'
|
||
if (status == 'ACCEPTED') return 'accepted_by_user'
|
||
if (status == 'SETTLEMENT_READY' || status == 'ARCHIVED') return 'settled'
|
||
if (status == 'ORDER_CANCELLED') return 'cancelled'
|
||
if (status == 'ORDER_EXCEPTION' || status == 'ACCEPTANCE_REJECTED') return 'exception'
|
||
if (status == 'created' || status == 'submitted') return 'created'
|
||
if (status == 'paid') return 'paid'
|
||
if (status == 'assigned' || status == 'pending_dispatch' || status == 'pending_assignment') return 'assigned'
|
||
if (status == 'accepted' || status == 'pending_accept') return 'accepted'
|
||
if (status == 'rejected') return 'rejected'
|
||
if (status == 'departed' || status == 'waiting_departure' || status == 'on_the_way') return 'departed'
|
||
if (status == 'arrived' || status == 'checked_in') return 'arrived'
|
||
if (status == 'in_service' || status == 'serving') return 'in_service'
|
||
if (status == 'completed') return 'completed'
|
||
if (status == 'pending_acceptance' || status == 'pending_confirm' || status == 'pending_submit') return 'pending_acceptance'
|
||
if (status == 'accepted_by_user') return 'accepted_by_user'
|
||
if (status == 'reviewed') return 'reviewed'
|
||
if (status == 'settled') return 'settled'
|
||
if (status == 'cancelled') return 'cancelled'
|
||
return 'exception'
|
||
}
|
||
|
||
private getUnifiedServiceStatusNumber(status: string): number {
|
||
const normalizedStatus = this.normalizeServiceStatus(status)
|
||
if (normalizedStatus == 'created') return 1
|
||
if (normalizedStatus == 'paid' || normalizedStatus == 'assigned') return 2
|
||
if (normalizedStatus == 'accepted' || normalizedStatus == 'departed') return 3
|
||
if (normalizedStatus == 'arrived' || normalizedStatus == 'in_service') return 4
|
||
if (normalizedStatus == 'completed' || normalizedStatus == 'pending_acceptance' || normalizedStatus == 'accepted_by_user' || normalizedStatus == 'reviewed' || normalizedStatus == 'settled') return 5
|
||
if (normalizedStatus == 'cancelled' || normalizedStatus == 'rejected' || normalizedStatus == 'exception') return 8
|
||
return 3
|
||
}
|
||
|
||
private matchesServiceStatusTab(status: string, statusTab: string, dispatchStatus: string = ''): boolean {
|
||
if (statusTab == 'all') return true
|
||
const normalizedStatus = this.normalizeServiceStatus(status)
|
||
if (statusTab == 'pending') return normalizedStatus == 'created'
|
||
if (statusTab == 'accepted') {
|
||
if (normalizedStatus == 'paid' && dispatchStatus != 'assigned' && dispatchStatus != 'failed') {
|
||
return true
|
||
}
|
||
return normalizedStatus == 'assigned'
|
||
}
|
||
if (statusTab == 'scheduled') return normalizedStatus == 'accepted' || normalizedStatus == 'departed'
|
||
if (statusTab == 'inservice') return normalizedStatus == 'arrived' || normalizedStatus == 'in_service'
|
||
if (statusTab == 'completed') return normalizedStatus == 'completed' || normalizedStatus == 'pending_acceptance' || normalizedStatus == 'accepted_by_user' || normalizedStatus == 'reviewed' || normalizedStatus == 'settled'
|
||
if (statusTab == 'aftersale') return false
|
||
if (statusTab == 'inprogress') {
|
||
if (normalizedStatus == 'paid' && dispatchStatus != 'failed') {
|
||
return true
|
||
}
|
||
return normalizedStatus == 'assigned' || normalizedStatus == 'accepted' || normalizedStatus == 'departed' || normalizedStatus == 'arrived' || normalizedStatus == 'in_service'
|
||
}
|
||
return true
|
||
}
|
||
|
||
private formatServiceAppointmentText(value: string): string {
|
||
if (value == '') {
|
||
return ''
|
||
}
|
||
if (value.indexOf('上午') >= 0 || value.indexOf('下午') >= 0 || value.indexOf('晚上') >= 0) {
|
||
return value
|
||
}
|
||
const parsed = Date.parse(value)
|
||
if (!isNaN(parsed)) {
|
||
const date = new Date(parsed)
|
||
let year = date.getFullYear()
|
||
const currentYear = new Date().getFullYear()
|
||
if (year < currentYear - 1) {
|
||
year = currentYear
|
||
}
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hour = String(date.getHours()).padStart(2, '0')
|
||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
|
||
}
|
||
const monthDayMatch = value.match(/(\d{2})\/(\d{2})/)
|
||
const timeMatch = value.match(/(\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?)/)
|
||
if (monthDayMatch != null) {
|
||
const month = monthDayMatch[1] ?? ''
|
||
const day = monthDayMatch[2] ?? ''
|
||
const timeText = timeMatch != null ? (timeMatch[1] ?? '') : ''
|
||
if (month != '' && day != '') {
|
||
return String(new Date().getFullYear()) + '-' + month + '-' + day + (timeText != '' ? ' ' + timeText.replace(/\s+/g, '') : '')
|
||
}
|
||
}
|
||
return value.replace('T', ' ')
|
||
}
|
||
|
||
private buildUnifiedServiceOrder(rawOrder: any): UTSJSONObject {
|
||
const orderObj = JSON.parse(JSON.stringify(rawOrder)) as UTSJSONObject
|
||
const addressSnapshotRaw = orderObj.get('address_snapshot_json')
|
||
const serviceSnapshotRaw = orderObj.get('service_snapshot_json')
|
||
const pricingSnapshotRaw = orderObj.get('pricing_snapshot_json')
|
||
let addressObj: UTSJSONObject | null = null
|
||
let serviceObj: UTSJSONObject | null = null
|
||
let pricingObj: UTSJSONObject | null = null
|
||
|
||
try {
|
||
if (addressSnapshotRaw != null) {
|
||
const addressText = JSON.stringify(addressSnapshotRaw)
|
||
if (addressText.startsWith('"')) {
|
||
addressObj = JSON.parse(orderObj.getString('address_snapshot_json') ?? '{}') as UTSJSONObject
|
||
} else {
|
||
addressObj = JSON.parse(addressText) as UTSJSONObject
|
||
}
|
||
}
|
||
} catch (e) {
|
||
addressObj = null
|
||
}
|
||
|
||
try {
|
||
if (serviceSnapshotRaw != null) {
|
||
const serviceText = JSON.stringify(serviceSnapshotRaw)
|
||
if (serviceText.startsWith('"')) {
|
||
serviceObj = JSON.parse(orderObj.getString('service_snapshot_json') ?? '{}') as UTSJSONObject
|
||
} else {
|
||
serviceObj = JSON.parse(serviceText) as UTSJSONObject
|
||
}
|
||
}
|
||
} catch (e) {
|
||
serviceObj = null
|
||
}
|
||
|
||
try {
|
||
if (pricingSnapshotRaw != null) {
|
||
const pricingText = JSON.stringify(pricingSnapshotRaw)
|
||
if (pricingText.startsWith('"')) {
|
||
pricingObj = JSON.parse(orderObj.getString('pricing_snapshot_json') ?? '{}') as UTSJSONObject
|
||
} else {
|
||
pricingObj = JSON.parse(pricingText) as UTSJSONObject
|
||
}
|
||
}
|
||
} catch (e) {
|
||
pricingObj = null
|
||
}
|
||
|
||
const normalizedStatus = this.getUnifiedServiceStatusNumber(orderObj.getString('status') ?? '')
|
||
const fullAddress = addressObj != null ? (addressObj.getString('fullAddress') ?? '') : ''
|
||
const providerName = orderObj.getString('staff_name') ?? ''
|
||
const serviceName = orderObj.getString('service_name') ?? (serviceObj != null ? (serviceObj.getString('serviceName') ?? '') : '')
|
||
let servicePrice = orderObj.getNumber('total_amount') ?? 0
|
||
if (servicePrice <= 0 && orderObj.getNumber('payable_amount') != null) {
|
||
servicePrice = orderObj.getNumber('payable_amount') ?? 0
|
||
}
|
||
if (servicePrice <= 0 && pricingObj != null) {
|
||
servicePrice = pricingObj.getNumber('price') ?? 0
|
||
}
|
||
if (servicePrice <= 0 && serviceObj != null) {
|
||
servicePrice = serviceObj.getNumber('price') ?? 0
|
||
}
|
||
|
||
const serviceInfo = new UTSJSONObject()
|
||
serviceInfo.set('service_name', serviceName)
|
||
serviceInfo.set('service_image', '/static/images/default.png')
|
||
serviceInfo.set('appointment_time', this.formatServiceAppointmentText(orderObj.getString('appointment_time') ?? ''))
|
||
serviceInfo.set('address', fullAddress)
|
||
serviceInfo.set('contact_name', orderObj.getString('contact_name') ?? '')
|
||
serviceInfo.set('contact_phone', orderObj.getString('contact_phone') ?? '')
|
||
serviceInfo.set('provider_name', providerName)
|
||
serviceInfo.set('package_name', pricingObj != null ? (pricingObj.getString('package_name') ?? '') : '')
|
||
serviceInfo.set('pricing_data_source', pricingObj != null ? (pricingObj.getString('data_source') ?? '') : '')
|
||
|
||
const unifiedOrder = new UTSJSONObject()
|
||
const rawPaymentStatus = orderObj.getNumber('payment_status')
|
||
let paymentStatus = rawPaymentStatus ?? 0
|
||
if (paymentStatus <= 0) {
|
||
if (normalizedStatus == ORDER_STATUS_PENDING) {
|
||
paymentStatus = PAYMENT_STATUS_UNPAID
|
||
} else if (normalizedStatus == ORDER_STATUS_CANCELLED || normalizedStatus == ORDER_STATUS_TIMEOUT_LEGACY) {
|
||
paymentStatus = PAYMENT_STATUS_TIMEOUT
|
||
} else {
|
||
paymentStatus = 2
|
||
}
|
||
}
|
||
unifiedOrder.set('id', orderObj.getString('id') ?? '')
|
||
unifiedOrder.set('order_no', orderObj.getString('order_no') ?? '')
|
||
unifiedOrder.set('biz_type', 'service')
|
||
unifiedOrder.set('source', 'service')
|
||
unifiedOrder.set('status', normalizedStatus)
|
||
unifiedOrder.set('order_status', normalizedStatus)
|
||
unifiedOrder.set('payment_status', paymentStatus)
|
||
unifiedOrder.set('pay_expire_at', orderObj.getString('pay_expire_at') ?? '')
|
||
unifiedOrder.set('cancel_reason', orderObj.getString('cancel_reason') ?? '')
|
||
unifiedOrder.set('created_at', orderObj.getString('created_at') ?? '')
|
||
unifiedOrder.set('create_time', orderObj.getString('created_at') ?? '')
|
||
unifiedOrder.set('product_amount', servicePrice)
|
||
unifiedOrder.set('shipping_fee', 0)
|
||
unifiedOrder.set('total_amount', servicePrice)
|
||
unifiedOrder.set('paid_amount', servicePrice)
|
||
unifiedOrder.set('pricing_data_source', pricingObj != null ? (pricingObj.getString('data_source') ?? '') : '')
|
||
const dispatchStatus = orderObj.getString('dispatch_status') ?? ''
|
||
const dispatchErrorMessage = orderObj.getString('dispatch_error_message') ?? ''
|
||
unifiedOrder.set('merchant_id', orderObj.getString('current_staff_id') ?? '')
|
||
unifiedOrder.set('shop_name', providerName != '' ? providerName : '康养上门服务')
|
||
unifiedOrder.set('service_info', serviceInfo)
|
||
unifiedOrder.set('ml_order_items', [] as UTSJSONObject[])
|
||
unifiedOrder.set('dispatch_status', dispatchStatus)
|
||
unifiedOrder.set('dispatch_error_code', orderObj.getString('dispatch_error_code') ?? '')
|
||
unifiedOrder.set('dispatch_error_message', dispatchErrorMessage)
|
||
return unifiedOrder
|
||
}
|
||
|
||
private async getServiceOrdersByCursor(params: GetUnifiedOrdersByCursorParams): Promise<GetUnifiedOrdersByCursorResult> {
|
||
const emptyResult: GetUnifiedOrdersByCursorResult = {
|
||
list: [] as UTSJSONObject[],
|
||
nextCursor: '',
|
||
hasMore: false
|
||
}
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return emptyResult
|
||
}
|
||
|
||
let query = supa
|
||
.from('hss_service_orders')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.order('created_at', { ascending: false })
|
||
.limit(200)
|
||
|
||
const cursor = params.cursor.trim()
|
||
if (cursor != '') {
|
||
query = query.lt('created_at', cursor)
|
||
}
|
||
|
||
const response = await query.execute()
|
||
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
|
||
return emptyResult
|
||
}
|
||
|
||
const keyword = params.keyword.trim()
|
||
const filtered = [] as UTSJSONObject[]
|
||
const rawList = response.data as any[]
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const rawItem = rawList[i]
|
||
const rawObj = JSON.parse(JSON.stringify(rawItem)) as UTSJSONObject
|
||
const orderNo = rawObj.getString('order_no') ?? ''
|
||
const serviceName = rawObj.getString('service_name') ?? ''
|
||
const contactName = rawObj.getString('contact_name') ?? ''
|
||
const appointmentTime = rawObj.getString('appointment_time') ?? ''
|
||
const addressSnapshot = rawObj.getString('address_snapshot_json') ?? ''
|
||
const matchedKeyword = keyword == '' || orderNo.indexOf(keyword) >= 0 || serviceName.indexOf(keyword) >= 0 || contactName.indexOf(keyword) >= 0 || appointmentTime.indexOf(keyword) >= 0 || addressSnapshot.indexOf(keyword) >= 0
|
||
if (!matchedKeyword) {
|
||
continue
|
||
}
|
||
if (!this.matchesServiceStatusTab(rawObj.getString('status') ?? '', params.statusTab, rawObj.getString('dispatch_status') ?? '')) {
|
||
continue
|
||
}
|
||
filtered.push(this.buildUnifiedServiceOrder(rawItem))
|
||
}
|
||
|
||
const limit = params.limit <= 0 ? 10 : params.limit
|
||
let hasMore = false
|
||
let list = filtered
|
||
if (filtered.length > limit) {
|
||
hasMore = true
|
||
list = filtered.slice(0, limit)
|
||
}
|
||
|
||
let nextCursor = ''
|
||
if (list.length > 0) {
|
||
const lastOrder = list[list.length - 1]
|
||
nextCursor = lastOrder.getString('created_at') ?? ''
|
||
}
|
||
|
||
return {
|
||
list,
|
||
nextCursor,
|
||
hasMore
|
||
}
|
||
} catch (error) {
|
||
console.error('[getServiceOrdersByCursor] 查询异常:', error)
|
||
return emptyResult
|
||
}
|
||
}
|
||
|
||
async getUnifiedOrdersByCursor(params: GetUnifiedOrdersByCursorParams): Promise<GetUnifiedOrdersByCursorResult> {
|
||
const emptyResult: GetUnifiedOrdersByCursorResult = {
|
||
list: [] as UTSJSONObject[],
|
||
nextCursor: '',
|
||
hasMore: false
|
||
}
|
||
|
||
try {
|
||
if (params.bizType == 'service') {
|
||
return await this.getServiceOrdersByCursor(params)
|
||
}
|
||
|
||
if (params.bizType == 'goods') {
|
||
const goodsResult = await this.getOrdersByCursor({
|
||
cursor: params.cursor,
|
||
limit: params.limit,
|
||
status: params.statusTab == 'pending' ? 1 : params.statusTab == 'shipping' ? 2 : params.statusTab == 'delivering' ? 3 : params.statusTab == 'completed' ? 4 : params.statusTab == 'aftersale' ? 6 : 0,
|
||
keyword: params.keyword
|
||
})
|
||
return {
|
||
list: goodsResult.list,
|
||
nextCursor: goodsResult.nextCursor,
|
||
hasMore: goodsResult.hasMore
|
||
}
|
||
}
|
||
|
||
const goodsStatus = params.statusTab == 'pending' ? 1 : params.statusTab == 'completed' ? 4 : params.statusTab == 'aftersale' ? 6 : 0
|
||
const goodsResult = await this.getOrdersByCursor({
|
||
cursor: '',
|
||
limit: 200,
|
||
status: goodsStatus,
|
||
keyword: params.keyword
|
||
})
|
||
const serviceResult = await this.getServiceOrdersByCursor({
|
||
cursor: '',
|
||
limit: 200,
|
||
bizType: 'service',
|
||
statusTab: params.statusTab,
|
||
keyword: params.keyword
|
||
})
|
||
|
||
let mergedList = [] as UTSJSONObject[]
|
||
for (let i = 0; i < goodsResult.list.length; i++) {
|
||
const goodsItem = goodsResult.list[i]
|
||
const status = goodsItem.getNumber('order_status') ?? 0
|
||
const includeGoods = params.statusTab == 'all' || (params.statusTab == 'pending' && status == 1) || (params.statusTab == 'completed' && status == 4) || (params.statusTab == 'aftersale' && (status == 6 || status == 7)) || (params.statusTab == 'inprogress' && (status == 2 || status == 3))
|
||
if (includeGoods) {
|
||
mergedList.push(goodsItem)
|
||
}
|
||
}
|
||
for (let i = 0; i < serviceResult.list.length; i++) {
|
||
mergedList.push(serviceResult.list[i])
|
||
}
|
||
|
||
mergedList.sort((left: UTSJSONObject, right: UTSJSONObject): number => {
|
||
const leftTime = left.getString('created_at') ?? ''
|
||
const rightTime = right.getString('created_at') ?? ''
|
||
if (leftTime == rightTime) return 0
|
||
return leftTime > rightTime ? -1 : 1
|
||
})
|
||
|
||
const cursor = params.cursor.trim()
|
||
if (cursor != '') {
|
||
const cursorFiltered = [] as UTSJSONObject[]
|
||
for (let i = 0; i < mergedList.length; i++) {
|
||
const createdAt = mergedList[i].getString('created_at') ?? ''
|
||
if (createdAt < cursor) {
|
||
cursorFiltered.push(mergedList[i])
|
||
}
|
||
}
|
||
mergedList = cursorFiltered
|
||
}
|
||
|
||
const limit = params.limit <= 0 ? 10 : params.limit
|
||
let hasMore = false
|
||
let list = mergedList
|
||
if (mergedList.length > limit) {
|
||
hasMore = true
|
||
list = mergedList.slice(0, limit)
|
||
}
|
||
|
||
let nextCursor = ''
|
||
if (list.length > 0) {
|
||
nextCursor = list[list.length - 1].getString('created_at') ?? ''
|
||
}
|
||
|
||
return {
|
||
list,
|
||
nextCursor,
|
||
hasMore
|
||
}
|
||
} catch (error) {
|
||
console.error('[getUnifiedOrdersByCursor] 查询异常:', error)
|
||
return emptyResult
|
||
}
|
||
}
|
||
|
||
async getServiceOrderStatusCounts(): Promise<UTSJSONObject> {
|
||
const counts = new UTSJSONObject()
|
||
counts.set('all', 0)
|
||
counts.set('pending', 0)
|
||
counts.set('accepted', 0)
|
||
counts.set('scheduled', 0)
|
||
counts.set('inservice', 0)
|
||
counts.set('completed', 0)
|
||
counts.set('aftersale', 0)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return counts
|
||
}
|
||
|
||
const response = await supa
|
||
.from('hss_service_orders')
|
||
.select('status,payment_status,dispatch_status')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.limit(500)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
|
||
return counts
|
||
}
|
||
|
||
const rawList = response.data as any[]
|
||
counts.set('all', rawList.length)
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const rawObj = JSON.parse(JSON.stringify(rawList[i])) as UTSJSONObject
|
||
const normalizedStatus = this.normalizeServiceStatus(rawObj.getString('status') ?? '')
|
||
const dispatchStatus = rawObj.getString('dispatch_status') ?? ''
|
||
const paymentStatus = rawObj.getNumber('payment_status') ?? 0
|
||
if (normalizedStatus == 'created' && paymentStatus == 1) {
|
||
counts.set('pending', (counts.getNumber('pending') ?? 0) + 1)
|
||
} else if ((normalizedStatus == 'paid' || normalizedStatus == 'assigned') && dispatchStatus != 'failed') {
|
||
counts.set('accepted', (counts.getNumber('accepted') ?? 0) + 1)
|
||
} else if (normalizedStatus == 'accepted' || normalizedStatus == 'departed') {
|
||
counts.set('scheduled', (counts.getNumber('scheduled') ?? 0) + 1)
|
||
} else if (normalizedStatus == 'arrived' || normalizedStatus == 'in_service') {
|
||
counts.set('inservice', (counts.getNumber('inservice') ?? 0) + 1)
|
||
} else if (normalizedStatus == 'completed' || normalizedStatus == 'pending_acceptance' || normalizedStatus == 'accepted_by_user' || normalizedStatus == 'reviewed' || normalizedStatus == 'settled') {
|
||
counts.set('completed', (counts.getNumber('completed') ?? 0) + 1)
|
||
} else if (dispatchStatus == 'failed') {
|
||
counts.set('accepted', (counts.getNumber('accepted') ?? 0) + 1)
|
||
}
|
||
}
|
||
return counts
|
||
} catch (error) {
|
||
console.error('[getServiceOrderStatusCounts] 统计异常:', error)
|
||
return counts
|
||
}
|
||
}
|
||
|
||
async getOrdersByCursor(params: GetOrdersByCursorParams): Promise<GetOrdersByCursorResult> {
|
||
const emptyResult: GetOrdersByCursorResult = {
|
||
list: [] as UTSJSONObject[],
|
||
nextCursor: '',
|
||
hasMore: false
|
||
}
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return emptyResult
|
||
}
|
||
|
||
const limit = params.limit <= 0 ? 10 : params.limit
|
||
const queryLimit = limit + 1
|
||
|
||
let query = supa
|
||
.from('ml_orders')
|
||
.select('*, ml_order_items(*), ml_shops(shop_name)')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.order('created_at', { ascending: false })
|
||
.limit(queryLimit)
|
||
|
||
if (params.status > 0) {
|
||
if (params.status == 6) {
|
||
query = query.in('order_status', [6, 7])
|
||
} else {
|
||
query = query.eq('order_status', params.status)
|
||
}
|
||
}
|
||
|
||
const keyword = params.keyword.trim()
|
||
if (keyword != '') {
|
||
const matchedOrderIds = await this.getKeywordMatchedOrderIds(userId, keyword)
|
||
if (matchedOrderIds.length == 0) {
|
||
return emptyResult
|
||
}
|
||
query = query.in('id', matchedOrderIds)
|
||
}
|
||
|
||
const cursor = params.cursor.trim()
|
||
if (cursor != '') {
|
||
query = query.lt('created_at', cursor)
|
||
}
|
||
|
||
const response = await query.execute()
|
||
if (response.error != null) {
|
||
console.error('[getOrdersByCursor] 查询失败:', response.error)
|
||
throw response.error
|
||
}
|
||
|
||
const rawData = response.data
|
||
if (rawData == null || !Array.isArray(rawData)) {
|
||
return emptyResult
|
||
}
|
||
|
||
const rawList = rawData as UTSJSONObject[]
|
||
let hasMore = false
|
||
let list = rawList
|
||
if (rawList.length > limit) {
|
||
hasMore = true
|
||
list = rawList.slice(0, limit)
|
||
}
|
||
|
||
const normalizedList = this.normalizeOrderList(list)
|
||
|
||
let nextCursor = ''
|
||
if (normalizedList.length > 0) {
|
||
const lastOrder = normalizedList[normalizedList.length - 1]
|
||
const lastOrderStr = JSON.stringify(lastOrder)
|
||
const lastOrderObj = JSON.parse(lastOrderStr) as UTSJSONObject
|
||
nextCursor = lastOrderObj.getString('created_at') ?? ''
|
||
}
|
||
|
||
return {
|
||
list: normalizedList,
|
||
nextCursor: nextCursor,
|
||
hasMore: hasMore
|
||
}
|
||
} catch (error) {
|
||
console.error('[getOrdersByCursor] 查询异常:', error)
|
||
return emptyResult
|
||
}
|
||
}
|
||
|
||
async getOrderCounts(): Promise<OrderCountsResult> {
|
||
const emptyCounts: OrderCountsResult = {
|
||
all: 0,
|
||
pending: 0,
|
||
shipping: 0,
|
||
delivering: 0,
|
||
completed: 0,
|
||
aftersale: 0,
|
||
cancelled: 0
|
||
}
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return emptyCounts
|
||
}
|
||
|
||
const queryCount = async (statusList: number[]): Promise<number> => {
|
||
let query = supa
|
||
.from('ml_orders')
|
||
.select('*', { count: 'exact' })
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.limit(1)
|
||
|
||
if (statusList.length == 1) {
|
||
query = query.eq('order_status', statusList[0])
|
||
} else if (statusList.length > 1) {
|
||
query = query.in('order_status', statusList)
|
||
}
|
||
|
||
const response = await query.execute()
|
||
if (response.error != null) {
|
||
console.error('[getOrderCounts] 统计失败:', response.error)
|
||
return 0
|
||
}
|
||
|
||
return response.total ?? 0
|
||
}
|
||
|
||
const all = await queryCount([])
|
||
const pending = await queryCount([1])
|
||
const shipping = await queryCount([2])
|
||
const delivering = await queryCount([3])
|
||
const completed = await queryCount([4])
|
||
const aftersale = await queryCount([6, 7])
|
||
const cancelled = await queryCount([5])
|
||
|
||
return {
|
||
all,
|
||
pending,
|
||
shipping,
|
||
delivering,
|
||
completed,
|
||
aftersale,
|
||
cancelled
|
||
}
|
||
} catch (error) {
|
||
console.error('[getOrderCounts] 统计异常:', error)
|
||
return emptyCounts
|
||
}
|
||
}
|
||
|
||
isUuidOrderId(orderId: string): boolean {
|
||
if (orderId == null || orderId == '') {
|
||
return false
|
||
}
|
||
const normalized = orderId.toLowerCase()
|
||
const parts = normalized.split('-')
|
||
if (parts.length != 5) {
|
||
return false
|
||
}
|
||
const expectedLengths = [8, 4, 4, 4, 12]
|
||
for (let i = 0; i < parts.length; i++) {
|
||
const part = parts[i]
|
||
if (part.length != expectedLengths[i]) {
|
||
return false
|
||
}
|
||
for (let j = 0; j < part.length; j++) {
|
||
const ch = part.charAt(j)
|
||
const isDigit = ch >= '0' && ch <= '9'
|
||
const isHexLower = ch >= 'a' && ch <= 'f'
|
||
if (!isDigit && !isHexLower) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
private isServiceOrderSource(orderId: string, source: string): boolean {
|
||
return source == 'service' || orderId.startsWith('so-')
|
||
}
|
||
|
||
private getUnifiedOrderTableName(orderId: string, source: string): string {
|
||
return this.isServiceOrderSource(orderId, source) ? 'hss_service_orders' : 'ml_orders'
|
||
}
|
||
|
||
private logUnifiedOrderRoute(action: string, orderId: string, source: string, tableName: string): void {
|
||
console.log('[' + action + '] source =', source)
|
||
console.log('[' + action + '] orderId =', orderId)
|
||
console.log('[' + action + '] tableName =', tableName)
|
||
}
|
||
|
||
private logPostgrestFailure(action: string, response: any): void {
|
||
console.error('[' + action + '] 请求失败:', response?.error)
|
||
console.error('[' + action + '] response.data:', JSON.stringify(response?.data))
|
||
console.error('[' + action + '] response.status:', response?.status)
|
||
}
|
||
|
||
private buildUnifiedGoodsOrderResult(rawOrder: any): UTSJSONObject {
|
||
const orderObj = JSON.parse(JSON.stringify(rawOrder)) as UTSJSONObject
|
||
const result = new UTSJSONObject()
|
||
result.set('id', orderObj.get('id') ?? '')
|
||
result.set('order_no', orderObj.get('order_no') ?? '')
|
||
result.set('biz_type', 'goods')
|
||
result.set('source', 'goods')
|
||
result.set('status', orderObj.get('order_status') ?? 1)
|
||
result.set('order_status', orderObj.get('order_status') ?? 1)
|
||
result.set('user_id', orderObj.get('user_id') ?? '')
|
||
result.set('merchant_id', orderObj.get('merchant_id') ?? '')
|
||
result.set('product_amount', orderObj.get('product_amount') ?? 0)
|
||
result.set('shipping_fee', orderObj.get('shipping_fee') ?? 0)
|
||
result.set('total_amount', orderObj.get('total_amount') ?? 0)
|
||
result.set('paid_amount', orderObj.get('paid_amount') ?? 0)
|
||
result.set('discount_amount', orderObj.get('discount_amount') ?? 0)
|
||
result.set('payment_method', orderObj.get('payment_method') ?? '')
|
||
result.set('payment_status', orderObj.get('payment_status') ?? 1)
|
||
result.set('cancel_reason', orderObj.get('cancel_reason') ?? '')
|
||
result.set('pay_expire_at', orderObj.get('pay_expire_at') ?? '')
|
||
result.set('consumer_deleted_at', orderObj.get('consumer_deleted_at') ?? '')
|
||
result.set('shipping_status', orderObj.get('shipping_status') ?? 1)
|
||
result.set('created_at', orderObj.get('created_at') ?? '')
|
||
result.set('paid_at', orderObj.get('paid_at') ?? '')
|
||
result.set('shipped_at', orderObj.get('shipped_at') ?? '')
|
||
result.set('completed_at', orderObj.get('completed_at') ?? '')
|
||
result.set('shipping_address', orderObj.get('shipping_address'))
|
||
result.set('ml_order_items', orderObj.get('ml_order_items'))
|
||
result.set('items', orderObj.get('ml_order_items'))
|
||
result.set('ml_shops', orderObj.get('ml_shops'))
|
||
result.set('tracking_no', orderObj.get('tracking_no') ?? '')
|
||
result.set('carrier_name', orderObj.get('carrier_name') ?? '')
|
||
result.set('delivered_at', orderObj.get('delivered_at') ?? '')
|
||
return result
|
||
}
|
||
|
||
private buildUnifiedServiceOrderDetailResult(rawOrder: any): UTSJSONObject {
|
||
const orderObj = JSON.parse(JSON.stringify(rawOrder)) as UTSJSONObject
|
||
const unifiedOrder = this.buildUnifiedServiceOrder(rawOrder)
|
||
const appointmentTime = this.formatServiceAppointmentText(orderObj.getString('appointment_time') ?? '')
|
||
const fullAddress = unifiedOrder.get('service_info')
|
||
let serviceInfo = new UTSJSONObject()
|
||
if (fullAddress != null) {
|
||
serviceInfo = JSON.parse(JSON.stringify(fullAddress)) as UTSJSONObject
|
||
}
|
||
|
||
const contactName = orderObj.getString('contact_name') ?? ''
|
||
const contactPhone = orderObj.getString('contact_phone') ?? ''
|
||
const addressText = serviceInfo.getString('address') ?? ''
|
||
|
||
const shippingAddress = new UTSJSONObject()
|
||
shippingAddress.set('name', contactName)
|
||
shippingAddress.set('phone', contactPhone)
|
||
shippingAddress.set('address', addressText)
|
||
shippingAddress.set('detail', addressText)
|
||
shippingAddress.set('province', '')
|
||
shippingAddress.set('city', '')
|
||
shippingAddress.set('district', '')
|
||
|
||
const item = new UTSJSONObject()
|
||
item.set('id', orderObj.getString('id') ?? '')
|
||
item.set('product_id', orderObj.getString('service_id') ?? '')
|
||
item.set('product_name', serviceInfo.getString('service_name') ?? (orderObj.getString('service_name') ?? '服务订单'))
|
||
item.set('sku_name', appointmentTime)
|
||
item.set('spec', appointmentTime)
|
||
item.set('product_image', serviceInfo.getString('service_image') ?? '/static/images/default.png')
|
||
item.set('image', serviceInfo.getString('service_image') ?? '/static/images/default.png')
|
||
item.set('price', unifiedOrder.getNumber('total_amount') ?? 0)
|
||
item.set('quantity', 1)
|
||
|
||
const items = [] as UTSJSONObject[]
|
||
items.push(item)
|
||
|
||
unifiedOrder.set('status', unifiedOrder.getNumber('order_status') ?? 1)
|
||
unifiedOrder.set('shipping_address', shippingAddress)
|
||
unifiedOrder.set('items', items)
|
||
unifiedOrder.set('service_info', serviceInfo)
|
||
unifiedOrder.set('contact_name', contactName)
|
||
unifiedOrder.set('contact_phone', contactPhone)
|
||
unifiedOrder.set('appointment_time', appointmentTime)
|
||
unifiedOrder.set('address', addressText)
|
||
unifiedOrder.set('payment_method', '')
|
||
unifiedOrder.set('discount_amount', 0)
|
||
unifiedOrder.set('paid_at', '')
|
||
unifiedOrder.set('shipped_at', '')
|
||
unifiedOrder.set('completed_at', orderObj.getString('completed_at') ?? '')
|
||
return unifiedOrder
|
||
}
|
||
|
||
async getUnifiedOrderDetail(orderId: string, source: string): Promise<UTSJSONObject | null> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return null
|
||
}
|
||
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
this.logUnifiedOrderRoute('getUnifiedOrderDetail', orderId, source, tableName)
|
||
|
||
if (tableName == 'hss_service_orders') {
|
||
const response = await supa
|
||
.from('hss_service_orders')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
console.log('[getUnifiedOrderDetail] response.data =', JSON.stringify(response.data))
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('getUnifiedOrderDetail', response)
|
||
return null
|
||
}
|
||
const rawList = response.data as any[]
|
||
if (rawList == null || rawList.length == 0) {
|
||
console.log('[getUnifiedOrderDetail] 未找到订单')
|
||
return null
|
||
}
|
||
return this.buildUnifiedServiceOrderDetailResult(rawList[0])
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select('*, ml_order_items(*), ml_shops(shop_name)')
|
||
.eq('user_id', userId)
|
||
.is('consumer_deleted_at', null)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
console.log('[getUnifiedOrderDetail] response.data =', JSON.stringify(response.data))
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('getUnifiedOrderDetail', response)
|
||
return null
|
||
}
|
||
const rawList = response.data as any[]
|
||
if (rawList == null || rawList.length == 0) {
|
||
console.log('[getUnifiedOrderDetail] 未找到订单')
|
||
return null
|
||
}
|
||
return this.buildUnifiedGoodsOrderResult(rawList[0])
|
||
} catch (e) {
|
||
console.error('[getUnifiedOrderDetail] 获取订单详情异常:', e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async ensureUnifiedPayExpireAt(orderId: string, source: string): Promise<string> {
|
||
try {
|
||
const latestOrder = await this.getUnifiedOrderDetail(orderId, source)
|
||
if (latestOrder == null) {
|
||
return ''
|
||
}
|
||
|
||
const currentExpireAt = latestOrder.getString('pay_expire_at') ?? ''
|
||
if (currentExpireAt != '') {
|
||
return currentExpireAt
|
||
}
|
||
|
||
const createdAt = latestOrder.getString('created_at') ?? ''
|
||
const createdDate = new Date(createdAt)
|
||
if (createdAt == '' || isNaN(createdDate.getTime())) {
|
||
return ''
|
||
}
|
||
|
||
const expireAtIso = new Date(createdDate.getTime() + ORDER_PAY_TIMEOUT_SECONDS * 1000).toISOString()
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return ''
|
||
}
|
||
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
this.logUnifiedOrderRoute('ensureUnifiedPayExpireAt', orderId, source, tableName)
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update({
|
||
pay_expire_at: expireAtIso,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', userId)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.is('pay_expire_at', null)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('ensureUnifiedPayExpireAt', response)
|
||
return ''
|
||
}
|
||
return expireAtIso
|
||
} catch (e) {
|
||
console.error('[ensureUnifiedPayExpireAt] 异常:', e)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
async cancelUnifiedOrder(orderId: string, source: string, reason: string = '用户取消订单'): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return false
|
||
}
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
const isService = tableName == 'hss_service_orders'
|
||
this.logUnifiedOrderRoute('cancelUnifiedOrder', orderId, source, tableName)
|
||
const nowIso = new Date().toISOString()
|
||
|
||
const updatePayload = new UTSJSONObject()
|
||
updatePayload.set('payment_status', PAYMENT_STATUS_TIMEOUT)
|
||
updatePayload.set('cancel_reason', reason)
|
||
updatePayload.set('cancelled_at', nowIso)
|
||
updatePayload.set('updated_at', nowIso)
|
||
if (isService) {
|
||
updatePayload.set('status', 'cancelled')
|
||
} else {
|
||
updatePayload.set('order_status', ORDER_STATUS_CANCELLED)
|
||
}
|
||
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update(updatePayload)
|
||
.eq('user_id', userId)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('cancelUnifiedOrder', response)
|
||
return false
|
||
}
|
||
return response.data != null && (!Array.isArray(response.data) || response.data.length > 0)
|
||
} catch (e) {
|
||
console.error('[cancelUnifiedOrder] 异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async expireUnifiedOrder(orderId: string, source: string): Promise<boolean> {
|
||
try {
|
||
const latestOrder = await this.getUnifiedOrderDetail(orderId, source)
|
||
if (latestOrder == null) {
|
||
return false
|
||
}
|
||
const latestStatus = latestOrder.getNumber('order_status') ?? ORDER_STATUS_PENDING
|
||
const latestPaymentStatus = latestOrder.getNumber('payment_status') ?? PAYMENT_STATUS_UNPAID
|
||
const latestCancelReason = latestOrder.getString('cancel_reason') ?? ''
|
||
const latestExpireAt = latestOrder.getString('pay_expire_at') ?? ''
|
||
if (latestStatus == ORDER_STATUS_CANCELLED || latestStatus == ORDER_STATUS_TIMEOUT_LEGACY || latestPaymentStatus == PAYMENT_STATUS_TIMEOUT || latestCancelReason.indexOf('超时') >= 0) {
|
||
return true
|
||
}
|
||
|
||
let effectiveExpireAt = latestExpireAt
|
||
if (effectiveExpireAt == '') {
|
||
effectiveExpireAt = await this.ensureUnifiedPayExpireAt(orderId, source)
|
||
}
|
||
const expireMs = effectiveExpireAt != '' ? new Date(effectiveExpireAt).getTime() : 0
|
||
if (expireMs <= 0 || expireMs > Date.now()) {
|
||
return false
|
||
}
|
||
|
||
return await this.cancelUnifiedOrder(orderId, source, ORDER_TIMEOUT_CANCEL_REASON)
|
||
} catch (e) {
|
||
console.error('[expireUnifiedOrder] 异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async softDeleteUnifiedOrderForConsumer(orderId: string, source: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return false
|
||
}
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
this.logUnifiedOrderRoute('softDeleteUnifiedOrderForConsumer', orderId, source, tableName)
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update({
|
||
consumer_deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', userId)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('softDeleteUnifiedOrderForConsumer', response)
|
||
return false
|
||
}
|
||
return response.data != null && (!Array.isArray(response.data) || response.data.length > 0)
|
||
} catch (e) {
|
||
console.error('[softDeleteUnifiedOrderForConsumer] 异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async markUnifiedOrderPaymentCancelled(orderId: string, source: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
return false
|
||
}
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
const isService = tableName == 'hss_service_orders'
|
||
this.logUnifiedOrderRoute('markUnifiedOrderPaymentCancelled', orderId, source, tableName)
|
||
|
||
const updatePayload = new UTSJSONObject()
|
||
updatePayload.set('payment_status', PAYMENT_STATUS_UNPAID)
|
||
updatePayload.set('updated_at', new Date().toISOString())
|
||
if (isService) {
|
||
updatePayload.set('status', 'created')
|
||
} else {
|
||
updatePayload.set('order_status', ORDER_STATUS_PENDING)
|
||
}
|
||
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update(updatePayload)
|
||
.eq('user_id', userId)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('markUnifiedOrderPaymentCancelled', response)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('[markUnifiedOrderPaymentCancelled] 异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async payUnifiedOrder(orderId: string, source: string, paymentMethod: string, amount: number): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || userId == '') {
|
||
console.error('[payUnifiedOrder] 用户未登录')
|
||
return false
|
||
}
|
||
|
||
const latestOrder = await this.getUnifiedOrderDetail(orderId, source)
|
||
if (latestOrder == null) {
|
||
console.error('[payUnifiedOrder] 订单不存在,无法支付')
|
||
return false
|
||
}
|
||
|
||
const latestStatus = latestOrder.getNumber('order_status') ?? ORDER_STATUS_PENDING
|
||
const latestPaymentStatus = latestOrder.getNumber('payment_status') ?? PAYMENT_STATUS_UNPAID
|
||
const latestCancelReason = latestOrder.getString('cancel_reason') ?? ''
|
||
const latestPayExpireAt = latestOrder.getString('pay_expire_at') ?? ''
|
||
const expireMs = latestPayExpireAt != '' ? new Date(latestPayExpireAt).getTime() : 0
|
||
const isExpired = latestStatus == ORDER_STATUS_CANCELLED || latestStatus == ORDER_STATUS_TIMEOUT_LEGACY || latestPaymentStatus == PAYMENT_STATUS_TIMEOUT || latestCancelReason.indexOf('超时') >= 0 || (expireMs > 0 && expireMs <= Date.now())
|
||
|
||
if (isExpired) {
|
||
await this.expireUnifiedOrder(orderId, source)
|
||
console.error('[payUnifiedOrder] 订单已超时,拒绝支付')
|
||
return false
|
||
}
|
||
if (latestStatus != ORDER_STATUS_PENDING || latestPaymentStatus != PAYMENT_STATUS_UNPAID) {
|
||
console.error('[payUnifiedOrder] 订单状态已变更,拒绝支付:', latestStatus, latestPaymentStatus)
|
||
return false
|
||
}
|
||
if (source == 'service' && (latestOrder.getString('pricing_data_source') ?? '') == 'dev_seed') {
|
||
console.error('[payUnifiedOrder] 测试套餐订单禁止真实支付')
|
||
return false
|
||
}
|
||
|
||
const tableName = this.getUnifiedOrderTableName(orderId, source)
|
||
const isService = tableName == 'hss_service_orders'
|
||
this.logUnifiedOrderRoute('payUnifiedOrder', orderId, source, tableName)
|
||
const nowIso = new Date().toISOString()
|
||
|
||
const updatePayload = new UTSJSONObject()
|
||
updatePayload.set('payment_status', PAYMENT_STATUS_PAID)
|
||
updatePayload.set('updated_at', nowIso)
|
||
if (isService) {
|
||
updatePayload.set('status', 'paid')
|
||
} else {
|
||
updatePayload.set('order_status', ORDER_STATUS_PAID_OR_SHIPPING)
|
||
updatePayload.set('payment_method', paymentMethod)
|
||
updatePayload.set('paid_at', nowIso)
|
||
updatePayload.set('paid_amount', amount)
|
||
}
|
||
|
||
let query = supa
|
||
.from(tableName)
|
||
.update(updatePayload)
|
||
.eq('user_id', userId)
|
||
.eq('payment_status', PAYMENT_STATUS_UNPAID)
|
||
.or(`id.eq.${orderId},order_no.eq.${orderId}`)
|
||
|
||
if (isService) {
|
||
query = query.eq('status', 'created')
|
||
} else {
|
||
query = query.eq('order_status', ORDER_STATUS_PENDING)
|
||
}
|
||
if (latestPayExpireAt != '') {
|
||
query = query.gt('pay_expire_at', nowIso)
|
||
}
|
||
|
||
const response = await query.execute()
|
||
if (response.error != null) {
|
||
this.logPostgrestFailure('payUnifiedOrder', response)
|
||
return false
|
||
}
|
||
if (response.data == null || (Array.isArray(response.data) && response.data.length === 0)) {
|
||
console.error('[payUnifiedOrder] 没有订单被更新,支付前状态校验未通过')
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('[payUnifiedOrder] 支付异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取订单详情
|
||
async getOrderDetail(orderId: string): Promise<any | null> {
|
||
return await this.getUnifiedOrderDetail(orderId, 'goods')
|
||
}
|
||
|
||
// 支付订单
|
||
async payOrder(orderId: string, paymentMethod: string, amount: number): Promise<boolean> {
|
||
return await this.payUnifiedOrder(orderId, 'goods', paymentMethod, amount)
|
||
}
|
||
|
||
// 根据ID获取订单信息
|
||
async getOrderById(orderId: string): Promise<UTSJSONObject | null> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error('[getOrderById] 用户未登录')
|
||
return null
|
||
}
|
||
|
||
console.log('[getOrderById] 查询订单, orderId:', orderId)
|
||
|
||
const query = supa
|
||
.from('ml_orders')
|
||
try {
|
||
const result = await this.getUnifiedOrderDetail(orderId, 'goods')
|
||
if (result == null) {
|
||
return null
|
||
}
|
||
return result
|
||
} catch (e) {
|
||
console.error('[getOrderById] 查询订单异常:', e)
|
||
return null
|
||
}
|
||
const refundType = d.getNumber('refund_type')
|
||
const refundReason = d.getString('refund_reason')
|
||
const refundAmount = d.getNumber('refund_amount')
|
||
const description = d.getString('description')
|
||
const images = d.getArray('images')
|
||
|
||
console.log('[createRefund] orderId:', orderId)
|
||
console.log('[createRefund] refundType:', refundType)
|
||
console.log('[createRefund] refundReason:', refundReason)
|
||
console.log('[createRefund] refundAmount:', refundAmount)
|
||
|
||
const payload = {
|
||
user_id: userId,
|
||
order_id: orderId,
|
||
refund_no: 'REF' + Date.now() + Math.floor(Math.random() * 1000),
|
||
refund_type: refundType,
|
||
refund_reason: refundReason,
|
||
refund_amount: refundAmount,
|
||
description: description ?? '',
|
||
images: images ?? ([] as any[]),
|
||
status: 1 // Pending
|
||
}
|
||
|
||
console.log('[createRefund] 准备插入 ml_refunds')
|
||
const response = await supa
|
||
.from('ml_refunds')
|
||
.insert(payload)
|
||
.execute()
|
||
|
||
console.log('[createRefund] insert response.error:', response.error)
|
||
|
||
if (response.error != null) {
|
||
console.error('提交售后失败:', response.error)
|
||
return { success: false, message: '提交失败: ' + (response.error.message ?? '未知错误') }
|
||
}
|
||
|
||
console.log('[createRefund] 插入成功,更新订单状态')
|
||
// 更新订单状态为退款中
|
||
const updateResponse = await supa
|
||
.from('ml_orders')
|
||
.update({
|
||
order_status: 6, // 退款中
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', orderId)
|
||
.execute()
|
||
|
||
console.log('[createRefund] update response.error:', updateResponse.error)
|
||
|
||
if (updateResponse.error != null) {
|
||
console.error('更新订单状态失败:', updateResponse.error)
|
||
// 不影响退款申请结果,只记录错误
|
||
}
|
||
|
||
console.log('[createRefund] 完成,返回成功')
|
||
return { success: true, message: '申请提交成功' }
|
||
} catch (e) {
|
||
console.error('提交售后异常:', e)
|
||
return { success: false, message: '系统异常' }
|
||
}
|
||
}
|
||
|
||
// 取消退款申请
|
||
async cancelRefund(orderId: string): Promise<RefundResponse> {
|
||
try {
|
||
console.log('[cancelRefund] 开始取消退款申请, orderId:', orderId)
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return { success: false, message: '请先登录' }
|
||
}
|
||
|
||
// 更新退款记录状态为已取消
|
||
const refundUpdateResponse = await supa
|
||
.from('ml_refunds')
|
||
.update({
|
||
status: 4, // 已取消
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('order_id', orderId)
|
||
.eq('user_id', userId)
|
||
.eq('status', 1) // 只能取消待处理的退款
|
||
.execute()
|
||
|
||
if (refundUpdateResponse.error != null) {
|
||
console.error('取消退款记录失败:', refundUpdateResponse.error)
|
||
return { success: false, message: '取消失败: ' + (refundUpdateResponse.error.message ?? '未知错误') }
|
||
}
|
||
|
||
// 恢复订单状态为已完成(假设之前是已完成状态)
|
||
const orderUpdateResponse = await supa
|
||
.from('ml_orders')
|
||
.update({
|
||
order_status: 4, // 已完成
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', orderId)
|
||
.execute()
|
||
|
||
if (orderUpdateResponse.error != null) {
|
||
console.error('恢复订单状态失败:', orderUpdateResponse.error)
|
||
// 不影响取消退款结果,只记录错误
|
||
}
|
||
|
||
return { success: true, message: '已取消退款申请' }
|
||
} catch (e) {
|
||
console.error('取消退款异常:', e)
|
||
return { success: false, message: '系统异常' }
|
||
}
|
||
}
|
||
|
||
// 再次购买
|
||
async rePurchase(order: any): Promise<boolean> {
|
||
try {
|
||
// 将 order 转换为 UTSJSONObject 以安全访问属性
|
||
const orderObj = JSON.parse(JSON.stringify(order)) as UTSJSONObject
|
||
// 尝试获取 ml_order_items 或 items
|
||
let itemsKey = 'ml_order_items'
|
||
let itemsRaw = orderObj.get(itemsKey)
|
||
|
||
if (itemsRaw == null) {
|
||
itemsKey = 'items'
|
||
itemsRaw = orderObj.get(itemsKey)
|
||
}
|
||
|
||
if (itemsRaw == null) return false
|
||
|
||
// 断言为数组
|
||
const items = itemsRaw as any[]
|
||
if (items.length === 0) return false
|
||
|
||
// 简单的循环添加,实际项目中可以优化为批量插入
|
||
for (let i = 0; i < items.length; i++) {
|
||
// 同样,item 也是 UTSJSONObject 或支持访问的对象
|
||
const item = JSON.parse(JSON.stringify(items[i])) as UTSJSONObject
|
||
const productId = item.getString('product_id')
|
||
const skuId = item.getString('sku_id')
|
||
// 数量可能是数字或字符串
|
||
const quantity = item.getNumber('quantity') ?? 1
|
||
|
||
if (productId != null) {
|
||
await this.addToCart(productId, quantity, skuId ?? '', '')
|
||
}
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('rePurchase error', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 申请售后 (Legacy/Simple update)
|
||
async applyRefund(orderId: string, reason: string): Promise<boolean> {
|
||
try {
|
||
// 更新订单状态为 退款中 (6)
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.update({
|
||
order_status: 6,
|
||
cancel_reason: reason,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', orderId)
|
||
.execute()
|
||
|
||
return response.error === null
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取售后记录列表
|
||
async getRefunds(statusList: number[] = [], page: number = 1, pageSize: number = 10): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
let query = supa
|
||
.from('ml_refunds')
|
||
.select(`
|
||
*,
|
||
order:ml_orders!inner (
|
||
order_no,
|
||
created_at,
|
||
ml_order_items (
|
||
product_id,
|
||
product_name,
|
||
image_url
|
||
)
|
||
)
|
||
`)
|
||
.eq('user_id', userId)
|
||
.order('created_at', { ascending: false })
|
||
|
||
if (statusList.length > 0) {
|
||
// 显式转换为 any[] 以匹配 .in 方法的参数要求
|
||
const anyList = statusList as any[]
|
||
query = query.in('status', anyList)
|
||
}
|
||
|
||
query = query.range((page - 1) * pageSize, page * pageSize - 1)
|
||
|
||
const response = await query.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取售后列表失败:', response.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const data = response.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return data
|
||
} catch (e) {
|
||
console.error('获取售后列表异常:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
async deleteRefund(refundId: string): Promise<boolean> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_refunds')
|
||
.delete()
|
||
.eq('id', refundId)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('删除退款记录失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('删除退款记录异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getUserBalanceNumber(): Promise<number> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
console.log('[Supabase] getUserBalance userId:', userId)
|
||
if (userId == null) return 0
|
||
|
||
// 优先查 ml_user_wallets
|
||
const walletRes = await supa
|
||
.from('ml_user_wallets')
|
||
.select('balance')
|
||
.eq('user_id', userId!)
|
||
.single()
|
||
.execute()
|
||
|
||
if (walletRes.error != null) {
|
||
console.error('[Supabase] getUserBalance error:', walletRes.error)
|
||
} else {
|
||
console.log('[Supabase] getUserBalance data:', walletRes.data)
|
||
}
|
||
|
||
if (walletRes.error == null && walletRes.data != null) {
|
||
let data = walletRes.data
|
||
// 如果是数组,取第一项
|
||
if (Array.isArray(data)) {
|
||
const arr = data as any[]
|
||
if (arr.length == 0) {
|
||
data = null
|
||
} else {
|
||
data = arr[0]
|
||
}
|
||
}
|
||
|
||
if (data == null) {
|
||
console.log('[Supabase] Wallet table returned empty array')
|
||
} else {
|
||
let val:number = 0
|
||
if (data instanceof UTSJSONObject) {
|
||
val = data.getNumber('balance') ?? 0
|
||
// 尝试字符串转换,防止精度丢失导致转为string
|
||
if (val === 0 && data.getString('balance') != null) {
|
||
val = parseFloat(data.getString('balance')!)
|
||
}
|
||
return val
|
||
} else if (data != null) {
|
||
// 对于 Map 或 loose object
|
||
const plainObj = JSON.parse(JSON.stringify(data))
|
||
const rawBalance = plainObj['balance']
|
||
if (typeof rawBalance === 'number') {
|
||
val = rawBalance as number
|
||
} else if (typeof rawBalance === 'string' && rawBalance != '') {
|
||
val = parseFloat(rawBalance as string)
|
||
}
|
||
return val
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('[Supabase] Wallet table empty, checking profile...')
|
||
|
||
// Fallback to profile
|
||
const profile = await this.getUserProfile()
|
||
if (profile != null) {
|
||
if (profile instanceof UTSJSONObject) {
|
||
return profile.getNumber('balance') ?? 0
|
||
} else {
|
||
const pObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||
return pObj.getNumber('balance') ?? 0
|
||
}
|
||
}
|
||
return 0
|
||
} catch(e) {
|
||
console.error('[Supabase] getUserBalance exception:', e)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 获取用户积分
|
||
async getUserPoints(): Promise<number> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return 0
|
||
|
||
// 查 ml_user_points
|
||
const res = await supa
|
||
.from('ml_user_points')
|
||
.select('points')
|
||
.eq('user_id', userId!)
|
||
.single()
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('[Supabase] getUserPoints error:', res.error)
|
||
}
|
||
|
||
if (res.error == null && res.data != null) {
|
||
let data = res.data
|
||
// 如果是数组,取第一项
|
||
if (Array.isArray(data)) {
|
||
const arr = data as any[]
|
||
if (arr.length == 0) {
|
||
data = null
|
||
} else {
|
||
data = arr[0]
|
||
}
|
||
}
|
||
|
||
if (data instanceof UTSJSONObject) {
|
||
return data.getNumber('points') ?? 0
|
||
} else if (data != null) {
|
||
// 尝试转为 UTSJSONObject
|
||
const plainObj = JSON.parse(JSON.stringify(data))
|
||
const rawPoints = plainObj['points']
|
||
if (typeof rawPoints === 'number') {
|
||
return rawPoints as number
|
||
}
|
||
if (typeof rawPoints === 'string' && rawPoints != '') {
|
||
return parseFloat(rawPoints as string)
|
||
}
|
||
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// Fallback check profile if needed
|
||
const profile = await this.getUserProfile()
|
||
if (profile != null) {
|
||
if (profile instanceof UTSJSONObject) {
|
||
return profile.getNumber('points') ?? 0
|
||
} else {
|
||
const plainProfile = JSON.parse(JSON.stringify(profile))
|
||
const rawPoints = plainProfile['points']
|
||
if (typeof rawPoints === 'number') {
|
||
return rawPoints as number
|
||
}
|
||
if (typeof rawPoints === 'string' && rawPoints != '') {
|
||
return parseFloat(rawPoints as string)
|
||
}
|
||
return 0
|
||
}
|
||
}
|
||
|
||
return 0
|
||
} catch (e) {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 获取钱包交易记录
|
||
async getTransactions(page: number = 1, limit: number = 20): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const from = (page - 1) * limit
|
||
const to = from + limit - 1
|
||
|
||
const response = await supa
|
||
.from('ml_wallet_transactions')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.range(from, to)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取交易记录失败:', response.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const data = response.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return data as any[]
|
||
} catch (e) {
|
||
console.error('获取交易记录异常:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取积分记录
|
||
async getPointRecords(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_point_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
const data = res.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
return data as any[]
|
||
} catch (e) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取用户红包
|
||
async getUserRedPackets(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_user_red_packets')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('获取红包失败:', res.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
const data = res.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
return data as any[]
|
||
} catch (e) {
|
||
console.error('获取红包异常:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取用户银行卡
|
||
async getUserBankCards(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_user_bank_cards')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('获取银行卡失败:', res.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
const data = res.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
return data as any[]
|
||
} catch (e) {
|
||
console.error('获取银行卡异常:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 余额充值 (调用 RPC)
|
||
async rechargeBalance(amount: number): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const res = await supa.rpc('recharge_wallet', {
|
||
p_user_id: userId,
|
||
p_amount: amount
|
||
})
|
||
|
||
if (res.error != null) {
|
||
console.error('充值失败RPC:', res.error)
|
||
return false
|
||
}
|
||
|
||
// 简单判断: 如果没有error且data里success为true
|
||
const data = res.data
|
||
if (data instanceof UTSJSONObject) {
|
||
return data.getBoolean('success') ?? false
|
||
}
|
||
// 如果返回不是对象,作为失败处理
|
||
return false
|
||
} catch (e) {
|
||
console.error('充值异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 余额提现 (调用 RPC)
|
||
async withdrawBalance(amount: number): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const res = await supa.rpc('withdraw_wallet', {
|
||
p_user_id: userId,
|
||
p_amount: amount
|
||
})
|
||
|
||
if (res.error != null) {
|
||
console.error('提现失败RPC:', res.error)
|
||
return false
|
||
}
|
||
|
||
const data = res.data
|
||
if (data instanceof UTSJSONObject) {
|
||
return data.getBoolean('success') ?? false
|
||
}
|
||
return false
|
||
} catch (e) {
|
||
console.error('提现异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 添加银行卡
|
||
async addBankCard(card: UTSJSONObject): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
// 补全 user_id
|
||
card.set('user_id', userId)
|
||
|
||
const res = await supa
|
||
.from('ml_user_bank_cards')
|
||
.insert(card)
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('添加银行卡失败:', res.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('添加银行卡异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 删除银行卡
|
||
async deleteBankCard(cardId: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const res = await supa
|
||
.from('ml_user_bank_cards')
|
||
.eq('id', cardId)
|
||
.eq('user_id', userId!)
|
||
.delete()
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('删除银行卡失败:', res.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('删除银行卡异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 收藏相关
|
||
getFavoriteTargetTypeValue(targetType: string): number {
|
||
if (targetType == 'service') {
|
||
return FAVORITE_TARGET_SERVICE
|
||
}
|
||
if (targetType == 'shop') {
|
||
return FAVORITE_TARGET_SHOP
|
||
}
|
||
return FAVORITE_TARGET_PRODUCT
|
||
}
|
||
|
||
getFavoriteTargetTypeKey(targetTypeValue: number): string {
|
||
if (targetTypeValue == FAVORITE_TARGET_SERVICE) {
|
||
return 'service'
|
||
}
|
||
if (targetTypeValue == FAVORITE_TARGET_SHOP) {
|
||
return 'shop'
|
||
}
|
||
return 'product'
|
||
}
|
||
|
||
buildFavoriteDescription(productObj: UTSJSONObject): string {
|
||
const subtitle = productObj.getString('subtitle') ?? ''
|
||
if (subtitle != '') {
|
||
return subtitle
|
||
}
|
||
const description = productObj.getString('description') ?? ''
|
||
return description
|
||
}
|
||
|
||
buildFavoriteImage(mainImageUrl: string, imageUrlsRaw: any): string {
|
||
const fixedMainImage = fixImageUrl(mainImageUrl)
|
||
if (fixedMainImage != '') {
|
||
return fixedMainImage
|
||
}
|
||
const imageUrls = parseStringArrayValue(imageUrlsRaw)
|
||
if (imageUrls.length > 0) {
|
||
return imageUrls[0]
|
||
}
|
||
return DEFAULT_FAVORITE_IMAGE
|
||
}
|
||
|
||
matchesFavoriteKeyword(item: FavoriteDisplayItem, keyword: string): boolean {
|
||
if (keyword == '') {
|
||
return true
|
||
}
|
||
const lowerKeyword = keyword.toLowerCase()
|
||
if (item.title.toLowerCase().indexOf(lowerKeyword) >= 0) {
|
||
return true
|
||
}
|
||
if (item.merchant_name.toLowerCase().indexOf(lowerKeyword) >= 0) {
|
||
return true
|
||
}
|
||
if (item.description_text.toLowerCase().indexOf(lowerKeyword) >= 0) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
appendFavoriteGroup(groupList: FavoriteGroup[], nextGroup: FavoriteGroup): FavoriteGroup[] {
|
||
const updated = groupList.slice() as FavoriteGroup[]
|
||
updated.push(nextGroup)
|
||
return updated
|
||
}
|
||
|
||
async checkFavorite(targetId: string, targetType: string = 'product'): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || targetId == '') {
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_user_favorites')
|
||
.select('id,target_id,target_type,deleted_at')
|
||
.eq('user_id', userId)
|
||
.eq('target_id', targetId)
|
||
.eq('target_type', this.getFavoriteTargetTypeValue(targetType))
|
||
.is('deleted_at', null)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('[checkFavorite] 查询收藏状态失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
const data = response.data
|
||
if (Array.isArray(data)) {
|
||
return (data as any[]).length > 0
|
||
}
|
||
return data != null
|
||
} catch (e) {
|
||
console.error('[checkFavorite] 查询异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async toggleFavorite(targetId: string, targetType: string = 'product'): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || targetId == '') {
|
||
return false
|
||
}
|
||
|
||
const dbTargetType = this.getFavoriteTargetTypeValue(targetType)
|
||
const existedResponse = await supa
|
||
.from('ml_user_favorites')
|
||
.select('id,deleted_at')
|
||
.eq('user_id', userId)
|
||
.eq('target_id', targetId)
|
||
.eq('target_type', dbTargetType)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (existedResponse.error != null) {
|
||
console.error('[toggleFavorite] 查询现有收藏失败:', existedResponse.error)
|
||
return false
|
||
}
|
||
|
||
const currentTime = new Date().toISOString()
|
||
const existedList = Array.isArray(existedResponse.data) ? (existedResponse.data as any[]) : [] as any[]
|
||
if (existedList.length > 0) {
|
||
const existedObj = normalizeUtsObject(existedList[0])
|
||
const favoriteId = existedObj.getString('id') ?? ''
|
||
const deletedAt = existedObj.getString('deleted_at') ?? ''
|
||
if (favoriteId == '') {
|
||
return false
|
||
}
|
||
|
||
const updatePayload = new UTSJSONObject()
|
||
updatePayload.set('updated_at', currentTime)
|
||
if (deletedAt == '') {
|
||
updatePayload.set('deleted_at', currentTime)
|
||
} else {
|
||
updatePayload.set('deleted_at', null)
|
||
}
|
||
|
||
const updateResponse = await supa
|
||
.from('ml_user_favorites')
|
||
.update(updatePayload)
|
||
.eq('id', favoriteId)
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
|
||
if (updateResponse.error != null) {
|
||
console.error('[toggleFavorite] 更新收藏失败:', updateResponse.error)
|
||
return deletedAt == ''
|
||
}
|
||
return deletedAt != ''
|
||
}
|
||
|
||
const insertPayload = new UTSJSONObject()
|
||
insertPayload.set('user_id', userId)
|
||
insertPayload.set('target_id', targetId)
|
||
insertPayload.set('target_type', dbTargetType)
|
||
insertPayload.set('created_at', currentTime)
|
||
insertPayload.set('updated_at', currentTime)
|
||
insertPayload.set('deleted_at', null)
|
||
|
||
const insertResponse = await supa
|
||
.from('ml_user_favorites')
|
||
.insert(insertPayload)
|
||
.execute()
|
||
|
||
if (insertResponse.error != null) {
|
||
console.error('[toggleFavorite] 新增收藏失败:', insertResponse.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('[toggleFavorite] 收藏切换异常:', e)
|
||
return await this.checkFavorite(targetId, targetType)
|
||
}
|
||
}
|
||
|
||
async softDeleteFavorites(favoriteIds: string[]): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || favoriteIds.length === 0) {
|
||
return false
|
||
}
|
||
|
||
const idsAny = [] as any[]
|
||
for (let i = 0; i < favoriteIds.length; i++) {
|
||
if (favoriteIds[i] != '') {
|
||
idsAny.push(favoriteIds[i])
|
||
}
|
||
}
|
||
if (idsAny.length === 0) {
|
||
return false
|
||
}
|
||
|
||
const updatePayload = new UTSJSONObject()
|
||
const currentTime = new Date().toISOString()
|
||
updatePayload.set('deleted_at', currentTime)
|
||
updatePayload.set('updated_at', currentTime)
|
||
|
||
const response = await supa
|
||
.from('ml_user_favorites')
|
||
.update(updatePayload)
|
||
.eq('user_id', userId)
|
||
.is('deleted_at', null)
|
||
.in('id', idsAny)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('[softDeleteFavorites] 软删除收藏失败:', response.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('[softDeleteFavorites] 软删除异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getFavoriteGroups(options: UTSJSONObject | null = null): Promise<FavoriteGroup[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return [] as FavoriteGroup[]
|
||
}
|
||
|
||
let keyword = ''
|
||
let targetTypeFilter = ''
|
||
if (options != null) {
|
||
keyword = options.getString('keyword') ?? ''
|
||
targetTypeFilter = options.getString('targetType') ?? ''
|
||
}
|
||
|
||
const favoriteTypeFilters = [] as any[]
|
||
if (targetTypeFilter == 'product') {
|
||
favoriteTypeFilters.push(FAVORITE_TARGET_PRODUCT)
|
||
} else if (targetTypeFilter == 'service') {
|
||
favoriteTypeFilters.push(FAVORITE_TARGET_SERVICE)
|
||
} else {
|
||
favoriteTypeFilters.push(FAVORITE_TARGET_PRODUCT)
|
||
favoriteTypeFilters.push(FAVORITE_TARGET_SERVICE)
|
||
}
|
||
|
||
const favoriteResponse = await supa
|
||
.from('ml_user_favorites')
|
||
.select('id,target_id,target_type,created_at,updated_at')
|
||
.eq('user_id', userId)
|
||
.is('deleted_at', null)
|
||
.in('target_type', favoriteTypeFilters)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (favoriteResponse.error != null) {
|
||
console.error('[getFavoriteGroups] 查询收藏记录失败:', favoriteResponse.error)
|
||
return [] as FavoriteGroup[]
|
||
}
|
||
|
||
const favoriteRows = Array.isArray(favoriteResponse.data) ? (favoriteResponse.data as any[]) : [] as any[]
|
||
if (favoriteRows.length === 0) {
|
||
return [] as FavoriteGroup[]
|
||
}
|
||
|
||
const productIds = [] as string[]
|
||
const serviceIds = [] as string[]
|
||
for (let i = 0; i < favoriteRows.length; i++) {
|
||
const favoriteObj = normalizeUtsObject(favoriteRows[i])
|
||
const targetId = favoriteObj.getString('target_id') ?? ''
|
||
const targetType = favoriteObj.getNumber('target_type') ?? FAVORITE_TARGET_PRODUCT
|
||
if (targetId == '') {
|
||
continue
|
||
}
|
||
if (targetType == FAVORITE_TARGET_SERVICE) {
|
||
serviceIds.push(targetId)
|
||
} else if (targetType == FAVORITE_TARGET_PRODUCT) {
|
||
productIds.push(targetId)
|
||
}
|
||
}
|
||
|
||
const productMap = new Map<string, UTSJSONObject>()
|
||
const productSkuMap = new Map<string, FavoriteSkuItem[]>()
|
||
const shopMap = new Map<string, UTSJSONObject>()
|
||
const serviceMap = new Map<string, UTSJSONObject>()
|
||
const servicePackageMap = new Map<string, UTSJSONObject>()
|
||
|
||
if (productIds.length > 0) {
|
||
const productIdsAny = [] as any[]
|
||
for (let i = 0; i < productIds.length; i++) {
|
||
productIdsAny.push(productIds[i])
|
||
}
|
||
|
||
const productResponse = await supa
|
||
.from('ml_products')
|
||
.select('id,merchant_id,name,subtitle,description,main_image_url,image_urls,base_price,market_price,available_stock,total_stock,status,tags')
|
||
.in('id', productIdsAny)
|
||
.execute()
|
||
|
||
if (productResponse.error == null && Array.isArray(productResponse.data)) {
|
||
const productRows = productResponse.data as any[]
|
||
const merchantIds = [] as string[]
|
||
for (let i = 0; i < productRows.length; i++) {
|
||
const productObj = normalizeUtsObject(productRows[i])
|
||
const productId = productObj.getString('id') ?? ''
|
||
const merchantId = productObj.getString('merchant_id') ?? ''
|
||
if (productId != '') {
|
||
productMap.set(productId, productObj)
|
||
}
|
||
if (merchantId != '') {
|
||
merchantIds.push(merchantId)
|
||
}
|
||
}
|
||
|
||
if (merchantIds.length > 0) {
|
||
const merchantIdsAny = [] as any[]
|
||
for (let i = 0; i < merchantIds.length; i++) {
|
||
merchantIdsAny.push(merchantIds[i])
|
||
}
|
||
|
||
const shopResponse = await supa
|
||
.from('ml_shops')
|
||
.select('merchant_id,shop_name,shop_logo,status')
|
||
.in('merchant_id', merchantIdsAny)
|
||
.execute()
|
||
|
||
if (shopResponse.error == null && Array.isArray(shopResponse.data)) {
|
||
const shopRows = shopResponse.data as any[]
|
||
for (let i = 0; i < shopRows.length; i++) {
|
||
const shopObj = normalizeUtsObject(shopRows[i])
|
||
const merchantId = shopObj.getString('merchant_id') ?? ''
|
||
if (merchantId != '') {
|
||
shopMap.set(merchantId, shopObj)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const skuResponse = await supa
|
||
.from('ml_product_skus')
|
||
.select('id,product_id,sku_code,specifications,price,stock,image_url,status')
|
||
.in('product_id', productIdsAny)
|
||
.eq('status', 1)
|
||
.execute()
|
||
|
||
if (skuResponse.error == null && Array.isArray(skuResponse.data)) {
|
||
const skuRows = skuResponse.data as any[]
|
||
for (let i = 0; i < skuRows.length; i++) {
|
||
const skuObj = normalizeUtsObject(skuRows[i])
|
||
const productId = skuObj.getString('product_id') ?? ''
|
||
if (productId != '') {
|
||
let skuList = productSkuMap.get(productId)
|
||
if (skuList == null) {
|
||
skuList = [] as FavoriteSkuItem[]
|
||
productSkuMap.set(productId, skuList)
|
||
}
|
||
skuList.push({
|
||
sku_id: skuObj.getString('id') ?? '',
|
||
product_id: productId,
|
||
sku_name: buildFavoriteSkuSpecText(skuObj.getString('specifications') ?? '', skuObj.getString('sku_code') ?? ''),
|
||
spec_text: buildFavoriteSkuSpecText(skuObj.getString('specifications') ?? '', skuObj.getString('sku_code') ?? ''),
|
||
sku_image_url: fixImageUrl(skuObj.getString('image_url') ?? ''),
|
||
sale_price: skuObj.getNumber('price') ?? 0,
|
||
stock_quantity: skuObj.getNumber('stock') ?? 0,
|
||
status: skuObj.getNumber('status') ?? 1
|
||
} as FavoriteSkuItem)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (serviceIds.length > 0) {
|
||
const serviceIdsAny = [] as any[]
|
||
for (let i = 0; i < serviceIds.length; i++) {
|
||
serviceIdsAny.push(serviceIds[i])
|
||
}
|
||
|
||
const serviceResponse = await supa
|
||
.from('hss_service_catalog')
|
||
.select('id,name,category,price,duration_text,summary,tags_json,suitable_for,status,deleted_at')
|
||
.in('id', serviceIdsAny)
|
||
.execute()
|
||
|
||
if (serviceResponse.error == null && Array.isArray(serviceResponse.data)) {
|
||
const serviceRows = serviceResponse.data as any[]
|
||
for (let i = 0; i < serviceRows.length; i++) {
|
||
const serviceObj = normalizeUtsObject(serviceRows[i])
|
||
const serviceId = serviceObj.getString('id') ?? ''
|
||
if (serviceId != '') {
|
||
serviceMap.set(serviceId, serviceObj)
|
||
}
|
||
}
|
||
}
|
||
|
||
const packageResponse = await supa
|
||
.from('hss_service_packages')
|
||
.select('id,service_id,package_name,package_desc,duration_text,price,list_price,is_default,sort_no')
|
||
.in('service_id', serviceIdsAny)
|
||
.eq('status', 1)
|
||
.is('deleted_at', null)
|
||
.order('sort_no', { ascending: true })
|
||
.execute()
|
||
|
||
if (packageResponse.error == null && Array.isArray(packageResponse.data)) {
|
||
const packageRows = packageResponse.data as any[]
|
||
for (let i = 0; i < packageRows.length; i++) {
|
||
const packageObj = normalizeUtsObject(packageRows[i])
|
||
const serviceId = packageObj.getString('service_id') ?? ''
|
||
if (serviceId == '') {
|
||
continue
|
||
}
|
||
const currentPackage = servicePackageMap.get(serviceId)
|
||
if (currentPackage == null) {
|
||
servicePackageMap.set(serviceId, packageObj)
|
||
continue
|
||
}
|
||
const currentDefault = currentPackage.getBoolean('is_default') ?? false
|
||
const nextDefault = packageObj.getBoolean('is_default') ?? false
|
||
if (!currentDefault && nextDefault) {
|
||
servicePackageMap.set(serviceId, packageObj)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let groups = [] as FavoriteGroup[]
|
||
for (let i = 0; i < favoriteRows.length; i++) {
|
||
const favoriteObj = normalizeUtsObject(favoriteRows[i])
|
||
const favoriteId = favoriteObj.getString('id') ?? ''
|
||
const targetId = favoriteObj.getString('target_id') ?? ''
|
||
const targetTypeValue = favoriteObj.getNumber('target_type') ?? FAVORITE_TARGET_PRODUCT
|
||
const targetType = this.getFavoriteTargetTypeKey(targetTypeValue)
|
||
if (favoriteId == '' || targetId == '' || targetType == 'shop') {
|
||
continue
|
||
}
|
||
|
||
let item: FavoritePurchaseItem | null = null
|
||
let groupId = ''
|
||
let groupName = ''
|
||
let groupLogo = ''
|
||
|
||
if (targetType == 'product') {
|
||
const productObj = productMap.get(targetId)
|
||
if (productObj == null) {
|
||
continue
|
||
}
|
||
const merchantId = productObj.getString('merchant_id') ?? ''
|
||
const shopObj = merchantId != '' ? shopMap.get(merchantId) : null
|
||
const title = productObj.getString('name') ?? '商品收藏'
|
||
const currentPrice = productObj.getNumber('base_price') ?? 0
|
||
const originalPrice = productObj.getNumber('market_price') ?? 0
|
||
const availableStock = productObj.getNumber('available_stock') ?? 0
|
||
const totalStock = productObj.getNumber('total_stock') ?? 0
|
||
const productStatus = productObj.getNumber('status') ?? 1
|
||
const skuList = productSkuMap.get(targetId) ?? ([] as FavoriteSkuItem[])
|
||
let defaultSku: FavoriteSkuItem | null = null
|
||
let validSkuCount = 0
|
||
for (let j = 0; j < skuList.length; j++) {
|
||
if (skuList[j].status == 1 && skuList[j].stock_quantity > 0) {
|
||
validSkuCount++
|
||
if (defaultSku == null) {
|
||
defaultSku = skuList[j]
|
||
}
|
||
}
|
||
}
|
||
const hasSkuConfig = skuList.length > 0
|
||
const canPurchase = productStatus == 1 && (hasSkuConfig ? validSkuCount > 0 : (availableStock > 0 || totalStock > 0))
|
||
let availabilityStatus = ''
|
||
if (productStatus != 1) {
|
||
availabilityStatus = '已下架'
|
||
} else if (canPurchase == false) {
|
||
availabilityStatus = '库存不足'
|
||
}
|
||
|
||
item = {
|
||
favorite_id: favoriteId,
|
||
target_id: targetId,
|
||
target_type: targetType,
|
||
merchant_id: merchantId,
|
||
merchant_name: shopObj != null ? (shopObj.getString('shop_name') ?? '商品收藏') : '商品收藏',
|
||
merchant_logo_url: shopObj != null ? fixImageUrl(shopObj.getString('shop_logo') ?? '') : '',
|
||
title: title,
|
||
image_url: this.buildFavoriteImage(productObj.getString('main_image_url') ?? '', productObj.get('image_urls')),
|
||
current_price: currentPrice,
|
||
original_price: originalPrice,
|
||
has_original_price: originalPrice > 0,
|
||
display_tags: parseStringArrayValue(productObj.get('tags')),
|
||
description_text: this.buildFavoriteDescription(productObj),
|
||
availability_status: availabilityStatus,
|
||
has_skus: hasSkuConfig,
|
||
sku_list: skuList,
|
||
selected_sku_id: validSkuCount == 1 && defaultSku != null ? defaultSku.sku_id : '',
|
||
selected_sku_text: validSkuCount == 1 && defaultSku != null ? defaultSku.spec_text : '',
|
||
selected_sku_price: validSkuCount == 1 && defaultSku != null ? defaultSku.sale_price : currentPrice,
|
||
selected_sku_stock: validSkuCount == 1 && defaultSku != null ? defaultSku.stock_quantity : Math.max(availableStock, totalStock),
|
||
quantity: 1,
|
||
purchase_selected: false,
|
||
manage_selected: false,
|
||
can_purchase: canPurchase,
|
||
can_appoint: false
|
||
} as FavoritePurchaseItem
|
||
|
||
groupId = merchantId != '' ? merchantId : 'favorite-group-product'
|
||
groupName = item.merchant_name != '' ? item.merchant_name : '商品收藏'
|
||
groupLogo = item.merchant_logo_url
|
||
} else if (targetType == 'service') {
|
||
const serviceObj = serviceMap.get(targetId)
|
||
if (serviceObj == null) {
|
||
continue
|
||
}
|
||
const packageObj = servicePackageMap.get(targetId)
|
||
const currentPrice = packageObj != null ? (packageObj.getNumber('price') ?? 0) : (serviceObj.getNumber('price') ?? 0)
|
||
const originalPrice = packageObj != null ? (packageObj.getNumber('list_price') ?? 0) : 0
|
||
const serviceStatus = serviceObj.getNumber('status') ?? 1
|
||
const durationText = packageObj != null ? (packageObj.getString('duration_text') ?? '') : (serviceObj.getString('duration_text') ?? '')
|
||
const suitableFor = serviceObj.getString('suitable_for') ?? ''
|
||
const summary = packageObj != null ? (packageObj.getString('package_desc') ?? '') : (serviceObj.getString('summary') ?? '')
|
||
let descriptionText = ''
|
||
if (durationText != '' && suitableFor != '') {
|
||
descriptionText = durationText + ' · ' + suitableFor
|
||
} else if (durationText != '') {
|
||
descriptionText = durationText
|
||
} else if (suitableFor != '') {
|
||
descriptionText = suitableFor
|
||
} else {
|
||
descriptionText = summary
|
||
}
|
||
|
||
item = {
|
||
favorite_id: favoriteId,
|
||
target_id: targetId,
|
||
target_type: targetType,
|
||
merchant_id: 'favorite-group-service',
|
||
merchant_name: '服务收藏',
|
||
merchant_logo_url: '',
|
||
title: serviceObj.getString('name') ?? '服务收藏',
|
||
image_url: DEFAULT_FAVORITE_IMAGE,
|
||
current_price: currentPrice,
|
||
original_price: originalPrice,
|
||
has_original_price: originalPrice > 0,
|
||
display_tags: parseStringArrayValue(serviceObj.get('tags_json')),
|
||
description_text: descriptionText,
|
||
availability_status: serviceStatus == 1 ? '' : '暂停预约',
|
||
has_skus: false,
|
||
sku_list: [] as FavoriteSkuItem[],
|
||
selected_sku_id: '',
|
||
selected_sku_text: '',
|
||
selected_sku_price: currentPrice,
|
||
selected_sku_stock: 0,
|
||
quantity: 1,
|
||
purchase_selected: false,
|
||
manage_selected: false,
|
||
can_purchase: false,
|
||
can_appoint: serviceStatus == 1
|
||
} as FavoritePurchaseItem
|
||
|
||
groupId = 'favorite-group-service'
|
||
groupName = '服务收藏'
|
||
groupLogo = ''
|
||
}
|
||
|
||
if (item == null) {
|
||
continue
|
||
}
|
||
if (!this.matchesFavoriteKeyword(item, keyword)) {
|
||
continue
|
||
}
|
||
|
||
let groupIndex = -1
|
||
for (let j = 0; j < groups.length; j++) {
|
||
if (groups[j].group_id == groupId) {
|
||
groupIndex = j
|
||
break
|
||
}
|
||
}
|
||
|
||
if (groupIndex < 0) {
|
||
groups = this.appendFavoriteGroup(groups, {
|
||
group_id: groupId,
|
||
group_name: groupName,
|
||
group_logo_url: groupLogo,
|
||
group_tags: [] as string[],
|
||
items: [item] as FavoritePurchaseItem[]
|
||
} as FavoriteGroup)
|
||
} else {
|
||
groups[groupIndex].items.push(item)
|
||
}
|
||
}
|
||
|
||
return groups
|
||
} catch (e) {
|
||
console.error('[getFavoriteGroups] 获取收藏分组异常:', e)
|
||
return [] as FavoriteGroup[]
|
||
}
|
||
}
|
||
|
||
async getFavorites(): Promise<any[]> {
|
||
const groups = await this.getFavoriteGroups()
|
||
const result = [] as any[]
|
||
for (let i = 0; i < groups.length; i++) {
|
||
for (let j = 0; j < groups[i].items.length; j++) {
|
||
result.push(groups[i].items[j])
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// 获取足迹列表
|
||
async getFootprints(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.log('[getFootprints] 用户未登录')
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
console.log('[getFootprints] 查询足迹, userId:', userId)
|
||
|
||
// 1. 获取足迹记录,兼容新旧表名
|
||
let footprints: any[] = []
|
||
for (let i = 0; i < FOOTPRINT_TABLE_CANDIDATES.length; i++) {
|
||
const tableName = FOOTPRINT_TABLE_CANDIDATES[i]
|
||
let response = await supa
|
||
.from(tableName)
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.is('deleted_at', null)
|
||
.order('updated_at', { ascending: false })
|
||
.limit(200)
|
||
.execute()
|
||
|
||
if (response.error != null && isMissingDeletedAtError(response.error)) {
|
||
response = await supa
|
||
.from(tableName)
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('updated_at', { ascending: false })
|
||
.limit(200)
|
||
.execute()
|
||
}
|
||
|
||
console.log('[getFootprints] 足迹查询 table:', tableName)
|
||
console.log('[getFootprints] 足迹查询 error:', response.error)
|
||
|
||
if (response.error == null) {
|
||
footprints = response.data as any[]
|
||
break
|
||
}
|
||
}
|
||
|
||
if (footprints == null || footprints.length === 0) {
|
||
console.log('[getFootprints] 没有足迹记录')
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
console.log('[getFootprints] 足迹记录数量:', footprints.length)
|
||
|
||
// 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 {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
pid = itemObj.getString('product_id') ?? ''
|
||
}
|
||
if (pid !== '' && !productIds.includes(pid)) productIds.push(pid)
|
||
}
|
||
|
||
if (productIds.length === 0) return []
|
||
|
||
const productIdsAny: any[] = []
|
||
for(let i=0; i<productIds.length; i++) {
|
||
productIdsAny.push(productIds[i])
|
||
}
|
||
|
||
// 3. 批量查询商品详情
|
||
const productRes = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('id, name, main_image_url, base_price, market_price, sale_count, merchant_id, shop_name, status')
|
||
.in('id', productIdsAny)
|
||
.execute()
|
||
|
||
// 如果视图失败,回退查基础表
|
||
let products: any[] = []
|
||
if (productRes.error == null && productRes.data != null) {
|
||
products = productRes.data as any[]
|
||
} else {
|
||
console.warn('View查询失败,尝试查询基础表')
|
||
const baseRes = await supa
|
||
.from('ml_products')
|
||
.select('id, name, main_image_url, base_price, market_price, sale_count, merchant_id, status')
|
||
.in('id', productIdsAny)
|
||
.execute()
|
||
if (baseRes.error == null && baseRes.data != null) {
|
||
products = baseRes.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 {
|
||
const pObj = JSON.parse(JSON.stringify(p)) as UTSJSONObject
|
||
pid = pObj.getString('id') ?? ''
|
||
}
|
||
if (pid !== '') productMap.set(pid, p)
|
||
}
|
||
|
||
// 4. 组合结果
|
||
let hasSaleStatusField = false
|
||
let matchedProductCount = 0
|
||
let missingProductCount = 0
|
||
const result: any[] = []
|
||
for (let i = 0; i < footprints.length; i++) {
|
||
let fp = footprints[i]
|
||
let footprintId = ''
|
||
let pid = ''
|
||
let viewTime = 0
|
||
|
||
if (fp instanceof UTSJSONObject) {
|
||
footprintId = fp.getString('id') ?? ''
|
||
pid = fp.getString('product_id') ?? ''
|
||
const dateStr = fp.getString('updated_at')
|
||
if (dateStr != null) viewTime = new Date(dateStr).getTime()
|
||
} else {
|
||
const fpObj = JSON.parse(JSON.stringify(fp)) as UTSJSONObject
|
||
footprintId = fpObj.getString('id') ?? ''
|
||
pid = fpObj.getString('product_id') ?? ''
|
||
const dateStr = fpObj.getString('updated_at')
|
||
if (dateStr != null) viewTime = new Date(dateStr).getTime()
|
||
}
|
||
|
||
const product = productMap.get(pid)
|
||
if (product != null) {
|
||
matchedProductCount++
|
||
let pName = ''
|
||
let pImage = ''
|
||
let pPrice = 0
|
||
let pOriginalPrice = 0
|
||
let pSales = 0
|
||
let pShopId = ''
|
||
let pShopName = ''
|
||
let pStatus = 0
|
||
let hasRealStatus = false
|
||
|
||
if (product instanceof UTSJSONObject) {
|
||
pName = product.getString('name') ?? ''
|
||
pImage = product.getString('main_image_url') ?? ''
|
||
pPrice = product.getNumber('base_price') ?? 0
|
||
pOriginalPrice = product.getNumber('market_price') ?? 0
|
||
pSales = product.getNumber('sale_count') ?? 0
|
||
pShopId = product.getString('merchant_id') ?? ''
|
||
pShopName = product.getString('shop_name') ?? ''
|
||
const statusValue = product.getNumber('status')
|
||
if (statusValue != null) {
|
||
pStatus = statusValue
|
||
hasRealStatus = true
|
||
hasSaleStatusField = true
|
||
}
|
||
} else {
|
||
const pObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
|
||
pName = pObj.getString('name') ?? ''
|
||
pImage = pObj.getString('main_image_url') ?? ''
|
||
pPrice = pObj.getNumber('base_price') ?? 0
|
||
pOriginalPrice = pObj.getNumber('market_price') ?? 0
|
||
pSales = pObj.getNumber('sale_count') ?? 0
|
||
pShopId = pObj.getString('merchant_id') ?? ''
|
||
pShopName = pObj.getString('shop_name') ?? ''
|
||
const statusValue = pObj.getNumber('status')
|
||
if (statusValue != null) {
|
||
pStatus = statusValue
|
||
hasRealStatus = true
|
||
hasSaleStatusField = true
|
||
}
|
||
}
|
||
|
||
const fpObj = new UTSJSONObject()
|
||
fpObj.set('id', footprintId)
|
||
fpObj.set('footprintId', footprintId)
|
||
fpObj.set('productId', pid)
|
||
fpObj.set('name', pName)
|
||
fpObj.set('price', pPrice)
|
||
fpObj.set('original_price', pOriginalPrice)
|
||
fpObj.set('image', pImage)
|
||
fpObj.set('sales', pSales)
|
||
fpObj.set('shopId', pShopId)
|
||
fpObj.set('shopName', pShopName)
|
||
fpObj.set('merchant_id', pShopId)
|
||
fpObj.set('merchantId', pShopId)
|
||
fpObj.set('viewTime', viewTime)
|
||
fpObj.set('saleStatus', pStatus)
|
||
fpObj.set('hasRealStatus', hasRealStatus)
|
||
fpObj.set('isOffShelf', hasRealStatus && pStatus != 1)
|
||
result.push(fpObj)
|
||
} else {
|
||
missingProductCount++
|
||
}
|
||
}
|
||
console.log('[getFootprints] footprints:', footprints.length)
|
||
console.log('[getFootprints] matched products:', matchedProductCount)
|
||
console.log('[getFootprints] missing products:', missingProductCount)
|
||
console.log('[getFootprints] has sale status field:', hasSaleStatusField)
|
||
|
||
return result
|
||
} catch (error) {
|
||
console.error('获取足迹异常:', error)
|
||
return []
|
||
}
|
||
}
|
||
|
||
// 添加/更新足迹
|
||
async addFootprint(productId: string): Promise<boolean> {
|
||
try {
|
||
const compatUserId = this.getCurrentUserId()
|
||
if (compatUserId != null) {
|
||
for (let i = 0; i < FOOTPRINT_TABLE_CANDIDATES.length; i++) {
|
||
const tableName = FOOTPRINT_TABLE_CANDIDATES[i]
|
||
let checkRes = await supa
|
||
.from(tableName)
|
||
.select('id, deleted_at')
|
||
.eq('user_id', compatUserId)
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
|
||
let supportsDeletedAt = true
|
||
if (checkRes.error != null && isMissingDeletedAtError(checkRes.error)) {
|
||
supportsDeletedAt = false
|
||
checkRes = await supa
|
||
.from(tableName)
|
||
.select('id')
|
||
.eq('user_id', compatUserId)
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
}
|
||
|
||
if (checkRes.error != null) {
|
||
continue
|
||
}
|
||
|
||
const checkData = checkRes.data as any[]
|
||
const exists = checkData != null && Array.isArray(checkData) && checkData.length > 0
|
||
if (exists) {
|
||
const updatePayload = new UTSJSONObject()
|
||
updatePayload.set('updated_at', new Date().toISOString())
|
||
if (supportsDeletedAt) {
|
||
updatePayload.set('deleted_at', null)
|
||
}
|
||
const updateRes = await supa
|
||
.from(tableName)
|
||
.update(updatePayload)
|
||
.eq('user_id', compatUserId)
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
return updateRes.error == null
|
||
}
|
||
|
||
const insertPayload = new UTSJSONObject()
|
||
insertPayload.set('user_id', compatUserId)
|
||
insertPayload.set('product_id', productId)
|
||
insertPayload.set('created_at', new Date().toISOString())
|
||
insertPayload.set('updated_at', new Date().toISOString())
|
||
if (supportsDeletedAt) {
|
||
insertPayload.set('deleted_at', null)
|
||
}
|
||
const insertRes = await supa
|
||
.from(tableName)
|
||
.insert(insertPayload)
|
||
.execute()
|
||
return insertRes.error == null
|
||
}
|
||
}
|
||
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.log('[addFootprint] 用户未登录')
|
||
return false
|
||
}
|
||
|
||
console.log('[addFootprint] 添加足迹, userId:', userId, 'productId:', productId)
|
||
|
||
// 检查是否已存在
|
||
const checkRes = await supa
|
||
.from('ml_user_footprints')
|
||
.select('id, deleted_at')
|
||
.eq('user_id', userId!)
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
|
||
console.log('[addFootprint] 检查结果 error:', checkRes.error)
|
||
console.log('[addFootprint] 检查结果 data:', JSON.stringify(checkRes.data))
|
||
|
||
const checkData = checkRes.data as any[]
|
||
const exists = checkData != null && Array.isArray(checkData) && checkData.length > 0
|
||
|
||
if (checkRes.error == null && exists) {
|
||
console.log('[addFootprint] 足迹已存在,更新时间')
|
||
// 更新时间
|
||
const updateRes = await supa
|
||
.from('ml_user_footprints')
|
||
.update({ updated_at: new Date().toISOString(), deleted_at: null })
|
||
.eq('user_id', userId!)
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
console.log('[addFootprint] 更新结果 error:', updateRes.error)
|
||
} else {
|
||
console.log('[addFootprint] 足迹不存在,插入新记录')
|
||
// 插入新记录
|
||
const insertPayload = new UTSJSONObject()
|
||
insertPayload.set('user_id', userId!)
|
||
insertPayload.set('product_id', productId)
|
||
insertPayload.set('created_at', new Date().toISOString())
|
||
insertPayload.set('updated_at', new Date().toISOString())
|
||
insertPayload.set('deleted_at', null)
|
||
|
||
const insertRes = await supa
|
||
.from('ml_user_footprints')
|
||
.insert(insertPayload)
|
||
.execute()
|
||
console.log('[addFootprint] 插入结果 error:', insertRes.error)
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('[addFootprint] 添加足迹异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 删除单个足迹
|
||
async deleteFootprint(productId: string): Promise<boolean> {
|
||
try {
|
||
const compatUserId = this.getCurrentUserId()
|
||
if (compatUserId != null) {
|
||
for (let i = 0; i < FOOTPRINT_TABLE_CANDIDATES.length; i++) {
|
||
const tableName = FOOTPRINT_TABLE_CANDIDATES[i]
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', compatUserId)
|
||
.eq('product_id', productId)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
if (response.error == null) {
|
||
return true
|
||
}
|
||
if (!isMissingDeletedAtError(response.error)) {
|
||
continue
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.log('[deleteFootprint] 用户未登录')
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_user_footprints')
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', userId)
|
||
.eq('product_id', productId)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('[deleteFootprint] 删除足迹失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
console.log('[deleteFootprint] 删除足迹成功')
|
||
return true
|
||
} catch (e) {
|
||
console.error('[deleteFootprint] 删除足迹异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 批量删除足迹
|
||
async deleteFootprints(footprintIds: string[]): Promise<boolean> {
|
||
try {
|
||
const compatUserId = this.getCurrentUserId()
|
||
if (compatUserId != null) {
|
||
const idsAnyCompat: any[] = []
|
||
for (let i = 0; i < footprintIds.length; i++) {
|
||
idsAnyCompat.push(footprintIds[i])
|
||
}
|
||
for (let i = 0; i < FOOTPRINT_TABLE_CANDIDATES.length; i++) {
|
||
const tableName = FOOTPRINT_TABLE_CANDIDATES[i]
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', compatUserId)
|
||
.in('id', idsAnyCompat)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
if (response.error == null) {
|
||
return true
|
||
}
|
||
if (!isMissingDeletedAtError(response.error)) {
|
||
continue
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.log('[deleteFootprints] 用户未登录')
|
||
return false
|
||
}
|
||
|
||
const idsAny: any[] = []
|
||
for (let i = 0; i < footprintIds.length; i++) {
|
||
idsAny.push(footprintIds[i])
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_user_footprints')
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', userId)
|
||
.in('id', idsAny)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('[deleteFootprints] 批量删除足迹失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
console.log('[deleteFootprints] 批量删除足迹成功')
|
||
return true
|
||
} catch (e) {
|
||
console.error('[deleteFootprints] 批量删除足迹异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 清空所有足迹
|
||
async clearFootprints(): Promise<boolean> {
|
||
try {
|
||
const compatUserId = this.getCurrentUserId()
|
||
if (compatUserId != null) {
|
||
for (let i = 0; i < FOOTPRINT_TABLE_CANDIDATES.length; i++) {
|
||
const tableName = FOOTPRINT_TABLE_CANDIDATES[i]
|
||
const response = await supa
|
||
.from(tableName)
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', compatUserId)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
if (response.error == null) {
|
||
return true
|
||
}
|
||
if (!isMissingDeletedAtError(response.error)) {
|
||
continue
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.log('[clearFootprints] 用户未登录')
|
||
return false
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_user_footprints')
|
||
.update({
|
||
deleted_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('user_id', userId)
|
||
.is('deleted_at', null)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('[clearFootprints] 清空足迹失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
console.log('[clearFootprints] 清空足迹成功')
|
||
return true
|
||
} catch (e) {
|
||
console.error('[clearFootprints] 清空足迹异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
async getAddressList(): Promise<UserAddress[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: UserAddress[] = []
|
||
return empty
|
||
}
|
||
|
||
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 != null) {
|
||
console.error('获取地址列表失败:', response.error)
|
||
const empty: UserAddress[] = []
|
||
return empty
|
||
}
|
||
return response.data as UserAddress[]
|
||
} catch (e) {
|
||
console.error('获取地址列表异常:', e)
|
||
const empty: UserAddress[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 设置默认地址
|
||
async setDefaultAddress(addressId: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
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 != null) {
|
||
console.error('设置默认地址失败:', response.error)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('设置默认地址异常:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取用户优惠券列表
|
||
async getUserCoupons(status?: number): Promise<UserCoupon[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: UserCoupon[] = []
|
||
return empty
|
||
}
|
||
|
||
let query = supa
|
||
.from('ml_user_coupons')
|
||
.select('*, template:ml_coupon_templates(name, discount_value, min_order_amount, coupon_type, discount_type, merchant_id, applicable_products)')
|
||
.eq('user_id', userId!)
|
||
|
||
if (status != null) {
|
||
query = query.eq('status', status.toString())
|
||
}
|
||
|
||
const response = await query.order('expire_at', { ascending: true }).execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取优惠券失败:', response.error)
|
||
throw new Error(response.error.message != null ? response.error.message as string : '获取优惠券失败')
|
||
}
|
||
|
||
const rawData: any[] = []
|
||
const respData = response.data
|
||
if (respData != null) {
|
||
if (Array.isArray(respData)) {
|
||
const arr = respData as any[]
|
||
for (let i = 0; i < arr.length; i++) {
|
||
rawData.push(arr[i])
|
||
}
|
||
} else if (respData instanceof UTSJSONObject) {
|
||
rawData.push(respData)
|
||
} else {
|
||
try {
|
||
const parsed = JSON.parse(JSON.stringify(respData))
|
||
if (Array.isArray(parsed)) {
|
||
for (let i = 0; i < parsed.length; i++) {
|
||
rawData.push(parsed[i])
|
||
}
|
||
}
|
||
} catch (parseErr) {
|
||
console.error('解析优惠券数据异常:', parseErr)
|
||
}
|
||
}
|
||
}
|
||
|
||
const coupons: UserCoupon[] = []
|
||
const merchantIds: string[] = []
|
||
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i]
|
||
let template: any | null = null
|
||
let itemId = ''
|
||
let itemTemplateId = ''
|
||
let itemStatus = 1
|
||
let itemUsedAt = ''
|
||
let itemReceivedAt = ''
|
||
let itemExpire = ''
|
||
|
||
if (item instanceof UTSJSONObject) {
|
||
template = item.get('template') as any | null
|
||
itemId = item.getString('id') ?? ''
|
||
itemTemplateId = item.getString('template_id') ?? ''
|
||
itemStatus = item.getNumber('status') ?? 1
|
||
itemUsedAt = item.getString('used_at') ?? ''
|
||
itemReceivedAt = item.getString('received_at') ?? ''
|
||
itemExpire = item.getString('expire_at') ?? ''
|
||
} else {
|
||
const iObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
template = iObj.get('template') as any | null
|
||
itemId = iObj.getString('id') ?? ''
|
||
itemTemplateId = iObj.getString('template_id') ?? ''
|
||
itemStatus = iObj.getNumber('status') ?? 1
|
||
itemUsedAt = iObj.getString('used_at') ?? ''
|
||
itemReceivedAt = iObj.getString('received_at') ?? ''
|
||
itemExpire = iObj.getString('expire_at') ?? ''
|
||
}
|
||
|
||
if (template == null) template = new UTSJSONObject()
|
||
|
||
let tName = ''
|
||
let tAmount = 0
|
||
let tMin = 0
|
||
let tDiscountType = 1
|
||
let tCouponType = 1
|
||
let tMerchantId = ''
|
||
let tScopeType = 'store'
|
||
|
||
if (template instanceof UTSJSONObject) {
|
||
tName = template.getString('name') ?? '优惠券'
|
||
tAmount = template.getNumber('discount_value') ?? 0
|
||
tMin = template.getNumber('min_order_amount') ?? 0
|
||
tDiscountType = template.getNumber('discount_type') ?? 1
|
||
tCouponType = template.getNumber('coupon_type') ?? 1
|
||
tMerchantId = template.getString('merchant_id') ?? ''
|
||
|
||
const applicableProducts = template.get('applicable_products')
|
||
if (applicableProducts != null) {
|
||
let hasProducts = false
|
||
if (Array.isArray(applicableProducts)) {
|
||
const arr = applicableProducts as any[]
|
||
if (arr.length > 0) hasProducts = true
|
||
} else if (typeof applicableProducts === 'string') {
|
||
const s = applicableProducts as string
|
||
if (s != '' && s != '[]') {
|
||
try {
|
||
const parsed = JSON.parse(s)
|
||
if (Array.isArray(parsed) && (parsed as any[]).length > 0) {
|
||
hasProducts = true
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
if (hasProducts) {
|
||
tScopeType = 'product'
|
||
}
|
||
}
|
||
} else {
|
||
const tObj = JSON.parse(JSON.stringify(template)) as UTSJSONObject
|
||
tName = tObj.getString('name') ?? '优惠券'
|
||
tAmount = tObj.getNumber('discount_value') ?? 0
|
||
tMin = tObj.getNumber('min_order_amount') ?? 0
|
||
tDiscountType = tObj.getNumber('discount_type') ?? 1
|
||
tCouponType = tObj.getNumber('coupon_type') ?? 1
|
||
tMerchantId = tObj.getString('merchant_id') ?? ''
|
||
|
||
const applicableProducts = tObj.get('applicable_products')
|
||
if (applicableProducts != null) {
|
||
let hasProducts = false
|
||
if (Array.isArray(applicableProducts)) {
|
||
const arr = applicableProducts as any[]
|
||
if (arr.length > 0) hasProducts = true
|
||
} else if (typeof applicableProducts === 'string') {
|
||
const s = applicableProducts as string
|
||
if (s != '' && s != '[]') {
|
||
try {
|
||
const parsed = JSON.parse(s)
|
||
if (Array.isArray(parsed) && (parsed as any[]).length > 0) {
|
||
hasProducts = true
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
if (hasProducts) {
|
||
tScopeType = 'product'
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tMerchantId != '' && merchantIds.indexOf(tMerchantId) == -1) {
|
||
merchantIds.push(tMerchantId)
|
||
}
|
||
|
||
const couponItem: UserCoupon = {
|
||
id: itemId,
|
||
template_id: itemTemplateId,
|
||
template_name: tName,
|
||
amount: tAmount,
|
||
min_spend: tMin,
|
||
expire_at: itemExpire,
|
||
status: itemStatus,
|
||
discount_type: tDiscountType,
|
||
coupon_type: tCouponType,
|
||
used_at: itemUsedAt,
|
||
received_at: itemReceivedAt,
|
||
merchant_id: tMerchantId,
|
||
shop_name: '',
|
||
shop_logo: '',
|
||
scope_type: tScopeType
|
||
}
|
||
|
||
coupons.push(couponItem)
|
||
}
|
||
|
||
// 批量查询店铺信息
|
||
if (merchantIds.length > 0) {
|
||
try {
|
||
const shopRes = await supa
|
||
.from('ml_shops')
|
||
.select('merchant_id, shop_name, shop_logo')
|
||
.in('merchant_id', merchantIds as any[])
|
||
.execute()
|
||
|
||
if (shopRes.error == null && shopRes.data != null) {
|
||
const shopRows = shopRes.data as any[]
|
||
for (let i = 0; i < shopRows.length; i++) {
|
||
const shopObj = normalizeUtsObject(shopRows[i])
|
||
const mid = shopObj.getString('merchant_id') ?? ''
|
||
const sname = shopObj.getString('shop_name') ?? ''
|
||
const slogo = shopObj.getString('shop_logo') ?? ''
|
||
if (mid != '') {
|
||
for (let j = 0; j < coupons.length; j++) {
|
||
if (coupons[j].merchant_id == mid) {
|
||
coupons[j].shop_name = sname
|
||
coupons[j].shop_logo = slogo
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (shopErr) {
|
||
console.error('查询店铺信息失败:', shopErr)
|
||
}
|
||
}
|
||
|
||
return coupons
|
||
} catch (e) {
|
||
console.error('获取优惠券异常:', e)
|
||
throw e
|
||
}
|
||
}
|
||
|
||
// 软删除用户优惠券(消费者端自行管理)
|
||
async softDeleteUserCoupons(ids: string[]): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null || ids.length == 0) {
|
||
return false
|
||
}
|
||
|
||
const nowIso = new Date().toISOString()
|
||
|
||
const response = await supa
|
||
.from('ml_user_coupons')
|
||
.update({
|
||
consumer_deleted_at: nowIso,
|
||
consumer_deleted_by: userId,
|
||
consumer_delete_reason: 'user_delete'
|
||
} as any)
|
||
.in('id', ids as any[])
|
||
.eq('user_id', userId!)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('软删除优惠券失败:', response.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('软删除优惠券异常:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取可用优惠券数量
|
||
async getUserCouponCount(): Promise<number> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return 0
|
||
|
||
const response = await supa
|
||
.from('ml_user_coupons')
|
||
.select('id', { count: 'exact' })
|
||
.eq('user_id', userId!)
|
||
.eq('status', '1')
|
||
.gt('expire_at', new Date().toISOString())
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
return 0
|
||
}
|
||
return response.total ?? 0
|
||
} catch (e) {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 获取店铺/商品可用优惠券
|
||
async getAvailableCoupons(merchantId: string): Promise<any[]> {
|
||
return this.fetchShopCoupons(merchantId)
|
||
}
|
||
|
||
// ALIAS for Cache busting: 获取店铺优惠券
|
||
async fetchShopCoupons(merchantId: string): Promise<any[]> {
|
||
try {
|
||
console.log('[fetchShopCoupons] 开始获取优惠券,merchantId:', merchantId)
|
||
// 查询该商家的优惠券 + 平台通用券 (merchant_id is null)
|
||
// 注意:这里简化逻辑,实际可能需要联合查询用户是否已领取
|
||
const response = await supa
|
||
.from('ml_coupon_templates')
|
||
.select('*')
|
||
.or(`merchant_id.eq.${merchantId},merchant_id.is.null`)
|
||
.eq('status', '1') // 使用字符串 '1'
|
||
.gt('end_time', new Date().toISOString())
|
||
.order('discount_value', { ascending: false })
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('Fetch coupons failed:', response.error)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const data = response.data
|
||
if (data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
console.log('[fetchShopCoupons] 获取到优惠券数量:', (data as any[]).length)
|
||
return data as any[]
|
||
} catch (e) {
|
||
console.error('Fetch coupons error:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 领取优惠券
|
||
async claimCoupon(templateId: string, userId: string): Promise<boolean> {
|
||
return this.claimShopCoupon(templateId, userId)
|
||
}
|
||
|
||
// ALIAS for Cache busting
|
||
async claimShopCoupon(templateId: string, userId: string): Promise<boolean> {
|
||
try {
|
||
console.log('Claiming coupon templateId:', templateId, 'userId:', userId)
|
||
|
||
// 1. Fetch template details to get merchant_id and validity
|
||
const tmplRes = await supa
|
||
.from('ml_coupon_templates')
|
||
.select('*')
|
||
.eq('id', templateId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (tmplRes.error != null) {
|
||
console.error('Claim Coupon: Template query error', tmplRes.error)
|
||
return false
|
||
}
|
||
|
||
// Null check for data
|
||
if (tmplRes.data == null) {
|
||
console.error('Claim Coupon: Template data response is null')
|
||
return false
|
||
}
|
||
|
||
const dataList = tmplRes.data as any[]
|
||
if (dataList.length === 0) {
|
||
console.error('Claim Coupon: Template not found (empty list)')
|
||
return false
|
||
}
|
||
|
||
const template = dataList[0]
|
||
|
||
// Safe property access
|
||
let validDays = 0
|
||
let endTimeStr: string | null = null
|
||
let merchantId: string | null = null
|
||
|
||
if (template instanceof UTSJSONObject) {
|
||
validDays = template.getNumber('valid_days') ?? 0
|
||
endTimeStr = template.getString('end_time')
|
||
merchantId = template.getString('merchant_id')
|
||
} else {
|
||
const tJson = JSON.parse(JSON.stringify(template)) as UTSJSONObject
|
||
validDays = tJson.getNumber('valid_days') ?? 0
|
||
endTimeStr = tJson.getString('end_time')
|
||
merchantId = tJson.getString('merchant_id')
|
||
}
|
||
|
||
// Calculate expire_at
|
||
let expireAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
||
if (validDays > 0) {
|
||
expireAt = new Date(Date.now() + (validDays * 24 * 60 * 60 * 1000)).toISOString()
|
||
} else if (endTimeStr != null && endTimeStr !== '') {
|
||
expireAt = endTimeStr
|
||
}
|
||
|
||
// Handle UUID fields: Empty string is not valid UUID, must be null
|
||
if (merchantId != null && merchantId.length === 0) {
|
||
merchantId = null
|
||
}
|
||
|
||
// 2. Insert into user coupons with merchant_id
|
||
const insertData = {
|
||
user_id: userId,
|
||
template_id: templateId,
|
||
merchant_id: merchantId, // Important for shop filtering: null for platform coupons
|
||
coupon_code: 'C' + Date.now() + Math.floor(Math.random() * 1000),
|
||
status: 1,
|
||
expire_at: expireAt,
|
||
received_at: new Date().toISOString()
|
||
}
|
||
|
||
console.log('Claim Coupon Insert Payload:', JSON.stringify(insertData))
|
||
|
||
const response = await supa
|
||
.from('ml_user_coupons')
|
||
.insert(insertData)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('Claim Coupon: Insert failed:', JSON.stringify(response.error))
|
||
// 尝试降级:如果 merchant_id 报错,尝试不带 merchant_id (仅调试用,或兼容旧表结构)
|
||
if (JSON.stringify(response.error).includes('merchant_id')) {
|
||
console.log('Retrying without merchant_id...')
|
||
const fallbackData = {
|
||
user_id: userId,
|
||
template_id: templateId,
|
||
coupon_code: 'C' + Date.now() + Math.random().toString().substring(2,6),
|
||
status: 1,
|
||
expire_at: expireAt,
|
||
received_at: new Date().toISOString()
|
||
}
|
||
const res2 = await supa.from('ml_user_coupons').insert(fallbackData).execute()
|
||
if (res2.error == null) return true
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
} catch(e) {
|
||
console.error('Claim coupon error:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// 聊天相关方法
|
||
// ==========================================
|
||
|
||
// 发送消息
|
||
async sendMessage(merchantId: string, content: string, msgType: string = 'text'): Promise<boolean> {
|
||
// 确保 session 有效
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error("sendMessage failed: user not logged in or session lost")
|
||
return false
|
||
}
|
||
|
||
try {
|
||
// Debug check
|
||
// const session = supa.getSession()
|
||
// console.log("Sending check: UserID", userId, "AuthID:", session.user?.getString('id'))
|
||
|
||
const msg = {
|
||
sender_id: userId!,
|
||
receiver_id: merchantId,
|
||
content: content,
|
||
msg_type: msgType,
|
||
is_read: false,
|
||
is_from_user: true
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_chat_messages')
|
||
.insert(msg)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('sendMessage error:', response.error)
|
||
return false
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('sendMessage exception:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 上传聊天图片
|
||
async uploadChatImage(filePath: string): Promise<string> {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
console.error("uploadChatImage failed: user not logged in")
|
||
return ''
|
||
}
|
||
|
||
try {
|
||
// 生成唯一文件名
|
||
const timestamp = Date.now()
|
||
const randomStr = Math.random().toString(36).substring(2, 8)
|
||
const fileName = `chat_${userId}_${timestamp}_${randomStr}.jpg`
|
||
const storagePath = `chat-images/${fileName}`
|
||
|
||
console.log('[uploadChatImage] 开始上传:', filePath, '->', storagePath)
|
||
|
||
const response = await supa.storage
|
||
.from('chat')
|
||
.upload(storagePath, filePath, {})
|
||
|
||
if (response.error != null) {
|
||
console.error('[uploadChatImage] 上传失败:', response.error)
|
||
return ''
|
||
}
|
||
|
||
// 构建公开访问URL
|
||
const publicUrl = `${supa.baseUrl}/storage/v1/object/public/chat/${storagePath}`
|
||
console.log('[uploadChatImage] 上传成功:', publicUrl)
|
||
return publicUrl
|
||
} catch (e) {
|
||
console.error('[uploadChatImage] 上传异常:', e)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// 标记会话已读
|
||
async markRead(merchantId: string): Promise<boolean> {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
try {
|
||
const response = await supa
|
||
.from('ml_chat_messages')
|
||
.update({ is_read: true })
|
||
.eq('sender_id', merchantId)
|
||
.eq('receiver_id', userId)
|
||
.eq('is_read', false)
|
||
.execute()
|
||
|
||
if (response.error != null) return false
|
||
} catch (e) { return false }
|
||
return true
|
||
}
|
||
|
||
// 提交商品评价
|
||
async submitProductReviews(reviews: Array<UTSJSONObject>): Promise<boolean> {
|
||
try {
|
||
for (let i: number = 0; i < reviews.length; i++) {
|
||
const review = reviews[i]
|
||
const response = await supa
|
||
.from('ml_product_reviews')
|
||
.insert(review)
|
||
.execute()
|
||
if (response.error != null) {
|
||
console.error('提交商品评价失败:', response.error)
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
} catch (e) {
|
||
console.error('提交商品评价失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 提交店铺评价
|
||
async submitShopReview(review: UTSJSONObject): Promise<boolean> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_shop_reviews')
|
||
.insert(review)
|
||
.execute()
|
||
return response.error == null
|
||
} catch (e) {
|
||
console.error('提交店铺评价失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 更新订单状态
|
||
async updateOrderStatus(orderId: string, status: number): Promise<boolean> {
|
||
try {
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('order_status', status)
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.update(updateData)
|
||
.eq('id', orderId)
|
||
.execute()
|
||
return response.error == null
|
||
} catch (e) {
|
||
console.error('更新订单状态失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ==================== 智能推荐相关API ====================
|
||
|
||
// 获取热搜词(全站搜索频率最高的关键词)
|
||
async getHotKeywords(limit: number = 10): Promise<string[]> {
|
||
try {
|
||
if (!logConsumerQueryStart('getHotKeywords', 'ml_search_history', 'keyword')) {
|
||
return [] as string[]
|
||
}
|
||
const response = await supa
|
||
.from('ml_search_history')
|
||
.select('keyword')
|
||
.order('created_at', { ascending: false })
|
||
.limit(100)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
logConsumerQueryFailure('getHotKeywords', 'ml_search_history', response.error)
|
||
return [] as string[]
|
||
}
|
||
|
||
// 统计关键词频率
|
||
const keywordCount = new Map<string, number>()
|
||
const rawList = response.data as any[]
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const keyword = safeGetString(itemObj, 'keyword').toLowerCase().trim()
|
||
if (keyword.length > 0) {
|
||
const count = keywordCount.get(keyword) ?? 0
|
||
keywordCount.set(keyword, count + 1)
|
||
}
|
||
}
|
||
|
||
// 按频率排序并返回前N个 - UTS兼容方式
|
||
// 将Map转换为数组进行排序
|
||
type KeywordEntry = {
|
||
keyword: string
|
||
count: number
|
||
}
|
||
const entryArray: KeywordEntry[] = []
|
||
|
||
// 使用forEach遍历Map(UTS支持)
|
||
keywordCount.forEach((value: number, key: string) => {
|
||
entryArray.push({
|
||
keyword: key,
|
||
count: value
|
||
})
|
||
})
|
||
|
||
// 按count降序排序
|
||
entryArray.sort((a: KeywordEntry, b: KeywordEntry): number => {
|
||
return b.count - a.count
|
||
})
|
||
|
||
// 取前limit个并提取关键词
|
||
const sortedKeywords: string[] = []
|
||
const maxCount = Math.min(entryArray.length, limit)
|
||
for (let i = 0; i < maxCount; i++) {
|
||
sortedKeywords.push(entryArray[i].keyword)
|
||
}
|
||
|
||
logConsumerQuerySuccess('getHotKeywords', 'ml_search_history', sortedKeywords.length)
|
||
return sortedKeywords
|
||
} catch (e) {
|
||
logConsumerQueryFailure('getHotKeywords', 'ml_search_history', e)
|
||
console.error('获取热搜词失败:', e)
|
||
return [] as string[]
|
||
}
|
||
}
|
||
|
||
// 获取用户搜索历史
|
||
async getUserSearchHistory(limit: number = 10): Promise<string[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return [] as string[]
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_search_history')
|
||
.select('keyword')
|
||
.order('created_at', { ascending: false })
|
||
.limit(limit * 2)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return [] as string[]
|
||
}
|
||
|
||
const keywords: string[] = []
|
||
const rawList = response.data as any[]
|
||
const seen = new Set<string>()
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const rawUserId = itemObj.get('user_id')
|
||
const itemUserId = (typeof rawUserId == 'string') ? (rawUserId as string) : ''
|
||
|
||
// 只获取当前用户的搜索历史
|
||
if (itemUserId !== userId) continue
|
||
|
||
const keyword = safeGetString(itemObj, 'keyword').trim()
|
||
if (keyword.length > 0 && !seen.has(keyword)) {
|
||
keywords.push(keyword)
|
||
seen.add(keyword)
|
||
if (keywords.length >= limit) break
|
||
}
|
||
}
|
||
|
||
return keywords
|
||
} catch (e) {
|
||
console.error('获取用户搜索历史失败:', e)
|
||
return [] as string[]
|
||
}
|
||
}
|
||
|
||
// 获取用户浏览历史中的商品分类
|
||
async getUserBrowseCategories(limit: number = 5): Promise<string[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return [] as string[]
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_browse_history')
|
||
.select('product_id')
|
||
.order('created_at', { ascending: false })
|
||
.limit(20)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return [] as string[]
|
||
}
|
||
|
||
// 获取浏览过的商品ID
|
||
const productIds: string[] = []
|
||
const rawList = response.data as any[]
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
// 手动过滤 user_id
|
||
const rawUserId = itemObj.get('user_id')
|
||
const itemUserId = (typeof rawUserId == 'string') ? (rawUserId as string) : ''
|
||
if (itemUserId !== userId) continue
|
||
|
||
const productId = safeGetString(itemObj, 'product_id')
|
||
if (productId.length > 0) {
|
||
productIds.push(productId)
|
||
}
|
||
}
|
||
|
||
if (productIds.length === 0) {
|
||
return [] as string[]
|
||
}
|
||
|
||
// 查询这些商品的分类
|
||
const prodResponse = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('id, category_id')
|
||
.limit(50)
|
||
.execute()
|
||
|
||
if (prodResponse.error != null || prodResponse.data == null) {
|
||
return [] as string[]
|
||
}
|
||
|
||
const categoryIds: string[] = []
|
||
const prodList = prodResponse.data as any[]
|
||
for (let i = 0; i < prodList.length; i++) {
|
||
const prodItem = prodList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(prodItem)) as UTSJSONObject
|
||
const prodId = safeGetString(prodObj, 'id')
|
||
|
||
// 只统计浏览过的商品
|
||
let found = false
|
||
for (let j = 0; j < productIds.length; j++) {
|
||
if (productIds[j] == prodId) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if (!found) continue
|
||
|
||
const catId = safeGetString(prodObj, 'category_id')
|
||
if (catId.length > 0 && categoryIds.indexOf(catId) < 0) {
|
||
categoryIds.push(catId)
|
||
if (categoryIds.length >= limit) break
|
||
}
|
||
}
|
||
|
||
return categoryIds
|
||
} catch (e) {
|
||
console.error('获取用户浏览分类失败:', e)
|
||
return [] as string[]
|
||
}
|
||
}
|
||
|
||
// 智能推荐:综合用户搜索历史、浏览历史、热销商品
|
||
async getSmartRecommendations(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Product>> {
|
||
try {
|
||
if (!logConsumerQueryStart('getSmartRecommendations', 'ml_products_detail_view', 'composed: search_history + browse_history + products_detail_view')) {
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
console.log('[getSmartRecommendations] 开始获取智能推荐...')
|
||
|
||
const products: Product[] = []
|
||
const addedIds = new Set<string>()
|
||
const requiredCount = page * limit + 1
|
||
|
||
// 1. 根据用户搜索历史推荐商品(权重最高)
|
||
const searchHistory = await this.getUserSearchHistory(5)
|
||
console.log('[getSmartRecommendations] 用户搜索历史:', searchHistory)
|
||
|
||
if (searchHistory.length > 0) {
|
||
// 根据搜索关键词查找商品
|
||
const keywordProducts = await this.searchProductsByKeywords(searchHistory, requiredCount)
|
||
for (let i = 0; i < keywordProducts.length; i++) {
|
||
const prod = keywordProducts[i]
|
||
if (!addedIds.has(prod.id)) {
|
||
products.push(prod)
|
||
addedIds.add(prod.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 根据用户浏览历史推荐相似分类商品
|
||
if (products.length < requiredCount) {
|
||
const browseCategories = await this.getUserBrowseCategories(3)
|
||
console.log('[getSmartRecommendations] 用户浏览分类:', browseCategories)
|
||
|
||
if (browseCategories.length > 0) {
|
||
const categoryProducts = await this.getProductsByCategories(browseCategories, requiredCount - products.length)
|
||
for (let i = 0; i < categoryProducts.length; i++) {
|
||
const prod = categoryProducts[i]
|
||
if (!addedIds.has(prod.id)) {
|
||
products.push(prod)
|
||
addedIds.add(prod.id)
|
||
if (products.length >= requiredCount) break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 补充热销商品
|
||
if (products.length < requiredCount) {
|
||
const hotProducts = await this.getHotProducts(requiredCount - products.length + 5)
|
||
for (let i = 0; i < hotProducts.length; i++) {
|
||
const prod = hotProducts[i]
|
||
if (!addedIds.has(prod.id)) {
|
||
products.push(prod)
|
||
addedIds.add(prod.id)
|
||
if (products.length >= requiredCount) break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. 如果还不够,用普通商品补充
|
||
if (products.length < requiredCount) {
|
||
const moreProducts = await this.getProductsByPrice(1, requiredCount - products.length + 5, false)
|
||
for (let i = 0; i < moreProducts.data.length; i++) {
|
||
const prod = moreProducts.data[i]
|
||
if (!addedIds.has(prod.id)) {
|
||
products.push(prod)
|
||
addedIds.add(prod.id)
|
||
if (products.length >= requiredCount) break
|
||
}
|
||
}
|
||
}
|
||
|
||
const startIndex = (page - 1) * limit
|
||
const endIndex = startIndex + limit
|
||
const pageData = products.slice(startIndex, endIndex)
|
||
console.log('[getSmartRecommendations] 返回商品数量:', pageData.length)
|
||
logConsumerQuerySuccess('getSmartRecommendations', 'ml_products_detail_view', pageData.length)
|
||
return {
|
||
data: pageData,
|
||
total: products.length,
|
||
page,
|
||
limit,
|
||
hasmore: products.length > endIndex
|
||
}
|
||
} catch (e) {
|
||
logConsumerQueryFailure('getSmartRecommendations', 'ml_products_detail_view', e)
|
||
console.error('获取智能推荐失败:', e)
|
||
return emptyProductPage(page, limit)
|
||
}
|
||
}
|
||
|
||
// 根据关键词列表搜索商品
|
||
async searchProductsByKeywords(keywords: string[], limit: number): Promise<Product[]> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||
.order('sale_count', { ascending: false })
|
||
.limit(limit * 2)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return [] as Product[]
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = response.data as any[]
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
// 手动过滤 status
|
||
const rawStatus = prodObj.get('status')
|
||
let statusNum: number = 0
|
||
if (typeof rawStatus == 'number') {
|
||
statusNum = rawStatus as number
|
||
}
|
||
if (statusNum !== 1) continue
|
||
|
||
// 检查是否匹配任何关键词
|
||
const name = safeGetString(prodObj, 'name').toLowerCase()
|
||
const desc = safeGetString(prodObj, 'description').toLowerCase()
|
||
|
||
let matched = false
|
||
for (let j = 0; j < keywords.length; j++) {
|
||
const keyword = keywords[j].toLowerCase()
|
||
if (name.indexOf(keyword) >= 0 || desc.indexOf(keyword) >= 0) {
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!matched) continue
|
||
|
||
products.push(parseProductFromRaw(item))
|
||
if (products.length >= limit) break
|
||
}
|
||
|
||
return products
|
||
} catch (e) {
|
||
console.error('根据关键词搜索商品失败:', e)
|
||
return [] as Product[]
|
||
}
|
||
}
|
||
|
||
// 根据分类列表获取商品
|
||
async getProductsByCategories(categoryIds: string[], limit: number): Promise<Product[]> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_products_detail_view')
|
||
.select('id, name, description, base_price, market_price, main_image_url, image_urls, category_id, brand_id, merchant_id, total_stock, sale_count, status, is_featured, is_new, is_hot')
|
||
.order('sale_count', { ascending: false })
|
||
.limit(limit * 2)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return [] as Product[]
|
||
}
|
||
|
||
const products: Product[] = []
|
||
const rawList = response.data as any[]
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
// 手动过滤 status
|
||
const rawStatus = prodObj.get('status')
|
||
let statusNum: number = 0
|
||
if (typeof rawStatus == 'number') {
|
||
statusNum = rawStatus as number
|
||
}
|
||
if (statusNum !== 1) continue
|
||
|
||
// 手动过滤 category_id
|
||
const rawCatId = prodObj.get('category_id')
|
||
const itemCatId = (typeof rawCatId == 'string') ? (rawCatId as string) : ''
|
||
|
||
let matched = false
|
||
for (let j = 0; j < categoryIds.length; j++) {
|
||
if (itemCatId == categoryIds[j]) {
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!matched) continue
|
||
|
||
products.push(parseProductFromRaw(item))
|
||
if (products.length >= limit) break
|
||
}
|
||
|
||
return products
|
||
} catch (e) {
|
||
console.error('根据分类获取商品失败:', e)
|
||
return [] as Product[]
|
||
}
|
||
}
|
||
|
||
// 记录用户搜索行为
|
||
async recordSearch(keyword: string, resultCount: number): Promise<void> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
const searchRecord = new UTSJSONObject()
|
||
searchRecord.set('keyword', keyword)
|
||
searchRecord.set('result_count', resultCount)
|
||
if (userId != null) {
|
||
searchRecord.set('user_id', userId)
|
||
}
|
||
|
||
await supa
|
||
.from('ml_search_history')
|
||
.insert(searchRecord)
|
||
.execute()
|
||
} catch (e) {
|
||
console.error('记录搜索失败:', e)
|
||
}
|
||
}
|
||
|
||
// 记录用户浏览行为
|
||
async recordBrowse(productId: string, duration: number = 0): Promise<void> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return
|
||
|
||
const browseRecord = new UTSJSONObject()
|
||
browseRecord.set('user_id', userId)
|
||
browseRecord.set('product_id', productId)
|
||
browseRecord.set('browse_duration', duration)
|
||
browseRecord.set('created_at', new Date().toISOString())
|
||
|
||
// UTS Android不支持upsert,使用insert
|
||
await supa
|
||
.from('ml_browse_history')
|
||
.insert(browseRecord)
|
||
.execute()
|
||
} catch (e) {
|
||
console.error('记录浏览失败:', e)
|
||
}
|
||
}
|
||
|
||
// ==================== 签到相关API ====================
|
||
|
||
// 用户签到
|
||
async signin(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('success', false)
|
||
result.set('points', 0)
|
||
result.set('continuous_days', 0)
|
||
result.set('bonus_points', 0)
|
||
result.set('total_points', 0)
|
||
result.set('message', '')
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
result.set('message', '请先登录')
|
||
return result
|
||
}
|
||
|
||
const today = new Date()
|
||
const todayStr = today.toISOString().split('T')[0]
|
||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
|
||
const yesterdayStr = yesterday.toISOString().split('T')[0]
|
||
|
||
// 检查今天是否已签到
|
||
const checkRes = await supa
|
||
.from('ml_signin_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.eq('signin_date', todayStr)
|
||
.execute()
|
||
|
||
if (checkRes.error != null) {
|
||
result.set('message', '查询签到状态失败')
|
||
return result
|
||
}
|
||
|
||
const checkData = checkRes.data as any[]
|
||
if (checkData != null && checkData.length > 0) {
|
||
result.set('message', '今天已签到')
|
||
return result
|
||
}
|
||
|
||
// 查询昨天是否签到,计算连续天数
|
||
const yesterdayRes = await supa
|
||
.from('ml_signin_records')
|
||
.select('continuous_days')
|
||
.eq('user_id', userId!)
|
||
.eq('signin_date', yesterdayStr)
|
||
.execute()
|
||
|
||
let continuousDays = 1
|
||
if (yesterdayRes.error == null && yesterdayRes.data != null) {
|
||
const yData = yesterdayRes.data as any[]
|
||
if (yData.length > 0) {
|
||
const yItem = yData[0]
|
||
let yDays = 0
|
||
if (yItem instanceof UTSJSONObject) {
|
||
yDays = yItem.getNumber('continuous_days') ?? 0
|
||
} else {
|
||
const yObj = JSON.parse(JSON.stringify(yItem)) as UTSJSONObject
|
||
yDays = yObj.getNumber('continuous_days') ?? 0
|
||
}
|
||
continuousDays = yDays + 1
|
||
}
|
||
}
|
||
|
||
// 计算积分
|
||
let pointsEarned = 5 // 每日签到基础积分
|
||
let bonusPoints = 0
|
||
|
||
if (continuousDays >= 30) {
|
||
bonusPoints = 100
|
||
} else if (continuousDays >= 7) {
|
||
bonusPoints = 20
|
||
}
|
||
|
||
const totalPointsEarned = pointsEarned + bonusPoints
|
||
|
||
// 插入签到记录
|
||
const signinRecord = new UTSJSONObject()
|
||
signinRecord.set('user_id', userId!)
|
||
signinRecord.set('signin_date', todayStr)
|
||
signinRecord.set('points_earned', pointsEarned)
|
||
signinRecord.set('bonus_points', bonusPoints)
|
||
signinRecord.set('continuous_days', continuousDays)
|
||
|
||
const insertRes = await supa
|
||
.from('ml_signin_records')
|
||
.insert(signinRecord)
|
||
.execute()
|
||
|
||
if (insertRes.error != null) {
|
||
result.set('message', '签到失败')
|
||
return result
|
||
}
|
||
|
||
// 更新用户积分
|
||
await this.addPoints(userId!, totalPointsEarned, 'signin', '每日签到')
|
||
|
||
// 获取最新积分
|
||
const newPoints = await this.getUserPoints()
|
||
|
||
result.set('success', true)
|
||
result.set('points', pointsEarned)
|
||
result.set('continuous_days', continuousDays)
|
||
result.set('bonus_points', bonusPoints)
|
||
result.set('total_points', newPoints)
|
||
result.set('message', '签到成功')
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('签到异常:', e)
|
||
result.set('message', '签到异常')
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取签到记录(当月)
|
||
async getSigninRecords(year: number, month: number): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const startDate = `${year}-${month.toString().padStart(2, '0')}-01`
|
||
const endDate = month === 12
|
||
? `${year + 1}-01-01`
|
||
: `${year}-${(month + 1).toString().padStart(2, '0')}-01`
|
||
|
||
const response = await supa
|
||
.from('ml_signin_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.gte('signin_date', startDate)
|
||
.lt('signin_date', endDate)
|
||
.order('signin_date', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return response.data as any[]
|
||
} catch (e) {
|
||
console.error('获取签到记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取今日签到状态
|
||
async getTodaySigninStatus(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('signed', false)
|
||
result.set('continuous_days', 0)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return result
|
||
|
||
const today = new Date().toISOString().split('T')[0]
|
||
|
||
// 检查今天是否签到
|
||
const todayRes = await supa
|
||
.from('ml_signin_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.eq('signin_date', today)
|
||
.execute()
|
||
|
||
if (todayRes.error == null && todayRes.data != null) {
|
||
const tData = todayRes.data as any[]
|
||
if (tData.length > 0) {
|
||
const tItem = tData[0]
|
||
let cDays = 0
|
||
if (tItem instanceof UTSJSONObject) {
|
||
cDays = tItem.getNumber('continuous_days') ?? 0
|
||
} else {
|
||
const tObj = JSON.parse(JSON.stringify(tItem)) as UTSJSONObject
|
||
cDays = tObj.getNumber('continuous_days') ?? 0
|
||
}
|
||
result.set('signed', true)
|
||
result.set('continuous_days', cDays)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 今天未签到,获取最近的连续签到天数
|
||
const lastRes = await supa
|
||
.from('ml_signin_records')
|
||
.select('continuous_days, signin_date')
|
||
.eq('user_id', userId!)
|
||
.order('signin_date', { ascending: false })
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (lastRes.error == null && lastRes.data != null) {
|
||
const lData = lastRes.data as any[]
|
||
if (lData.length > 0) {
|
||
const lItem = lData[0]
|
||
let lastDate = ''
|
||
let lastDays = 0
|
||
if (lItem instanceof UTSJSONObject) {
|
||
lastDate = lItem.getString('signin_date') ?? ''
|
||
lastDays = lItem.getNumber('continuous_days') ?? 0
|
||
} else {
|
||
const lObj = JSON.parse(JSON.stringify(lItem)) as UTSJSONObject
|
||
lastDate = lObj.getString('signin_date') ?? ''
|
||
lastDays = lObj.getNumber('continuous_days') ?? 0
|
||
}
|
||
|
||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||
if (lastDate === yesterday) {
|
||
result.set('continuous_days', lastDays)
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取签到状态失败:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// ==================== 积分兑换相关API ====================
|
||
|
||
// 获取积分兑换商品列表
|
||
async getPointProducts(): Promise<any[]> {
|
||
try {
|
||
const response = await supa
|
||
.from('ml_point_products')
|
||
.select('*')
|
||
.eq('status', 1)
|
||
.gt('stock', 0)
|
||
.order('sort_order', { ascending: true })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return response.data as any[]
|
||
} catch (e) {
|
||
console.error('获取积分商品失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 积分兑换
|
||
async exchangeProduct(productId: string, quantity: number, addressSnapshot: UTSJSONObject | null): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('success', false)
|
||
result.set('message', '')
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
result.set('message', '请先登录')
|
||
return result
|
||
}
|
||
|
||
// 获取商品信息
|
||
const productRes = await supa
|
||
.from('ml_point_products')
|
||
.select('*')
|
||
.eq('id', productId)
|
||
.single()
|
||
.execute()
|
||
|
||
if (productRes.error != null || productRes.data == null) {
|
||
result.set('message', '商品不存在')
|
||
return result
|
||
}
|
||
|
||
const productRaw = productRes.data
|
||
let pointsRequired = 0
|
||
let stock = 0
|
||
let productType = ''
|
||
|
||
// 检查是否是数组,如果是则取第一个元素
|
||
let productObj: UTSJSONObject | null = null
|
||
if (Array.isArray(productRaw)) {
|
||
const arr = productRaw as any[]
|
||
if (arr.length > 0) {
|
||
const firstItem = arr[0]
|
||
if (firstItem instanceof UTSJSONObject) {
|
||
productObj = firstItem
|
||
} else {
|
||
productObj = JSON.parse(JSON.stringify(firstItem)) as UTSJSONObject
|
||
}
|
||
}
|
||
} else {
|
||
if (productRaw instanceof UTSJSONObject) {
|
||
productObj = productRaw
|
||
} else {
|
||
productObj = JSON.parse(JSON.stringify(productRaw)) as UTSJSONObject
|
||
}
|
||
}
|
||
|
||
// 使用 UTSJSONObject 方法访问属性
|
||
if (productObj != null) {
|
||
pointsRequired = productObj.getNumber('points_required') ?? 0
|
||
stock = productObj.getNumber('stock') ?? 0
|
||
productType = productObj.getString('product_type') ?? ''
|
||
}
|
||
|
||
const totalPoints = pointsRequired * quantity
|
||
|
||
// 检查库存
|
||
if (stock < quantity) {
|
||
result.set('message', '库存不足')
|
||
return result
|
||
}
|
||
|
||
// 检查积分
|
||
const userPoints = await this.getUserPoints()
|
||
if (userPoints < totalPoints) {
|
||
result.set('message', '积分不足')
|
||
return result
|
||
}
|
||
|
||
// 创建兑换记录
|
||
const exchangeRecord = new UTSJSONObject()
|
||
exchangeRecord.set('user_id', userId!)
|
||
exchangeRecord.set('product_id', productId)
|
||
exchangeRecord.set('quantity', quantity)
|
||
exchangeRecord.set('points_used', totalPoints)
|
||
exchangeRecord.set('status', 0)
|
||
if (addressSnapshot != null && productType === 'physical') {
|
||
exchangeRecord.set('address_snapshot', JSON.stringify(addressSnapshot))
|
||
}
|
||
|
||
const insertRes = await supa
|
||
.from('ml_point_exchanges')
|
||
.insert(exchangeRecord)
|
||
.execute()
|
||
|
||
if (insertRes.error != null) {
|
||
console.error('[exchangeProduct] 创建兑换记录失败:', insertRes.error)
|
||
result.set('message', '兑换失败')
|
||
return result
|
||
}
|
||
|
||
console.log('[exchangeProduct] 兑换记录创建成功')
|
||
|
||
// 扣减库存
|
||
console.log('[exchangeProduct] 准备扣减库存')
|
||
console.log('[exchangeProduct] productId 类型:', typeof productId)
|
||
console.log('[exchangeProduct] productId 值:', productId)
|
||
console.log('[exchangeProduct] 当前库存:', stock, ', 扣减数量:', quantity)
|
||
|
||
// 使用 UTSJSONObject 替代 Record<string, any>
|
||
const stockUpdateData = new UTSJSONObject()
|
||
stockUpdateData.set('stock', stock - quantity)
|
||
|
||
console.log('[exchangeProduct] stockUpdateData:', stockUpdateData)
|
||
console.log('[exchangeProduct] stockUpdateData 类型:', typeof stockUpdateData)
|
||
|
||
// 先查询确认商品存在
|
||
const checkProduct = await supa
|
||
.from('ml_point_products')
|
||
.select('id, stock')
|
||
.eq('id', productId)
|
||
.execute()
|
||
console.log('[exchangeProduct] 查询商品结果:', checkProduct.data, 'error:', checkProduct.error)
|
||
|
||
const stockUpdateRes = await supa
|
||
.from('ml_point_products')
|
||
.update(stockUpdateData)
|
||
.eq('id', productId)
|
||
.execute()
|
||
|
||
console.log('[exchangeProduct] 库存更新结果 error:', stockUpdateRes.error)
|
||
console.log('[exchangeProduct] 库存更新结果 data:', stockUpdateRes.data)
|
||
|
||
if (stockUpdateRes.error != null) {
|
||
console.error('[exchangeProduct] 扣减库存失败:', stockUpdateRes.error)
|
||
}
|
||
|
||
// 扣减积分
|
||
console.log('[exchangeProduct] 准备扣减积分, userId:', userId, ', 积分:', totalPoints)
|
||
const deductResult = await this.deductPoints(userId!, totalPoints, 'redeem', '积分兑换商品')
|
||
console.log('[exchangeProduct] 积分扣减结果:', deductResult)
|
||
|
||
if (!deductResult) {
|
||
console.error('[exchangeProduct] 扣减积分失败')
|
||
}
|
||
|
||
console.log('[exchangeProduct] 兑换流程完成')
|
||
result.set('success', true)
|
||
result.set('message', '兑换成功')
|
||
return result
|
||
} catch (e) {
|
||
console.error('积分兑换异常:', e)
|
||
result.set('message', '兑换异常')
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取兑换记录
|
||
async getExchangeRecords(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_point_exchanges')
|
||
.select('*, product:ml_point_products(name, image_url, product_type)')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return response.data as any[]
|
||
} catch (e) {
|
||
console.error('获取兑换记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// ==================== 评价相关API ====================
|
||
|
||
// 获取商品评价列表
|
||
async getProductReviews(productId: string, page: number = 1, limit: number = 10, rating: number = 0, hasImage: boolean = false): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('total', 0)
|
||
result.set('page', page)
|
||
result.set('limit', limit)
|
||
result.set('data', [] as any[])
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
|
||
let query = supa
|
||
.from('ml_product_reviews')
|
||
.select('*', { count: 'exact' })
|
||
.eq('product_id', productId)
|
||
|
||
if (rating > 0) {
|
||
query = query.eq('rating', rating)
|
||
}
|
||
|
||
if (hasImage) {
|
||
query = query.neq('images', '[]')
|
||
}
|
||
|
||
const offset = (page - 1) * limit
|
||
const response = await query
|
||
.order('created_at', { ascending: false })
|
||
.range(offset, offset + limit - 1)
|
||
.execute()
|
||
|
||
if (response.error != null) {
|
||
console.error('获取评价列表失败:', response.error)
|
||
return result
|
||
}
|
||
|
||
const total = response.total ?? 0
|
||
const reviews = response.data as any[]
|
||
|
||
const reviewUserIds = [] as any[]
|
||
for (let i = 0; i < reviews.length; i++) {
|
||
const reviewObj = normalizeUtsObject(reviews[i])
|
||
const reviewUserId = reviewObj.getString('user_id') ?? ''
|
||
if (reviewUserId == '') {
|
||
continue
|
||
}
|
||
let exists = false
|
||
for (let j = 0; j < reviewUserIds.length; j++) {
|
||
if ((reviewUserIds[j] as string) == reviewUserId) {
|
||
exists = true
|
||
break
|
||
}
|
||
}
|
||
if (!exists) {
|
||
reviewUserIds.push(reviewUserId)
|
||
}
|
||
}
|
||
|
||
const reviewerMap = new Map<string, UTSJSONObject>()
|
||
if (reviewUserIds.length > 0) {
|
||
const profileResponse = await supa
|
||
.from('ak_user_public_profiles')
|
||
.select('id,username,avatar_url')
|
||
.in('id', reviewUserIds)
|
||
.execute()
|
||
|
||
if (profileResponse.error != null) {
|
||
console.error('获取评价用户公开资料失败:', profileResponse.error)
|
||
} else if (Array.isArray(profileResponse.data)) {
|
||
const profileRows = profileResponse.data as any[]
|
||
for (let i = 0; i < profileRows.length; i++) {
|
||
const profileObj = normalizeUtsObject(profileRows[i])
|
||
const profileId = profileObj.getString('id') ?? ''
|
||
if (profileId != '') {
|
||
reviewerMap.set(profileId, profileObj)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理评价数据
|
||
const processedReviews: any[] = []
|
||
for (let i = 0; i < reviews.length; i++) {
|
||
const review = reviews[i]
|
||
const processed = JSON.parse(JSON.stringify(review)) as UTSJSONObject
|
||
|
||
// 处理用户信息
|
||
const reviewerId = processed.getString('user_id') ?? ''
|
||
let userName = '匿名用户'
|
||
let userAvatar = ''
|
||
|
||
if (reviewerId != '') {
|
||
const reviewerProfile = reviewerMap.get(reviewerId)
|
||
if (reviewerProfile != null) {
|
||
userName = reviewerProfile.getString('username') ?? '匿名用户'
|
||
userAvatar = reviewerProfile.getString('avatar_url') ?? ''
|
||
}
|
||
}
|
||
|
||
// 检查是否匿名
|
||
const isAnonymous = processed.getBoolean('is_anonymous') ?? false
|
||
if (isAnonymous) {
|
||
userName = '匿名用户'
|
||
userAvatar = ''
|
||
}
|
||
|
||
processed.set('user_name', userName)
|
||
processed.set('user_avatar', userAvatar)
|
||
|
||
// 检查当前用户是否点赞
|
||
let isLiked = false
|
||
if (userId != null) {
|
||
const likeRes = await supa
|
||
.from('ml_review_likes')
|
||
.select('id')
|
||
.eq('review_id', processed.getString('id') ?? '')
|
||
.eq('user_id', userId!)
|
||
.limit(1)
|
||
.execute()
|
||
if (likeRes.error == null && likeRes.data != null) {
|
||
const likeData = likeRes.data as any[]
|
||
isLiked = likeData.length > 0
|
||
}
|
||
}
|
||
processed.set('is_liked', isLiked)
|
||
|
||
processedReviews.push(processed)
|
||
}
|
||
|
||
result.set('total', total)
|
||
result.set('data', processedReviews)
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取评价列表异常:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取商品评价统计
|
||
async getReviewStats(productId: string): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('total_count', 0)
|
||
result.set('avg_rating', 0)
|
||
result.set('good_rate', 0)
|
||
result.set('rating_distribution', new UTSJSONObject())
|
||
result.set('tags', [] as any[])
|
||
|
||
try {
|
||
const response = await supa
|
||
.from('ml_product_reviews')
|
||
.select('rating')
|
||
.eq('product_id', productId)
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
return result
|
||
}
|
||
|
||
const reviews = response.data as any[]
|
||
const totalCount = reviews.length
|
||
|
||
if (totalCount === 0) return result
|
||
|
||
let totalRating = 0
|
||
let goodCount = 0
|
||
const distribution: Map<number, number> = new Map()
|
||
distribution.set(1, 0)
|
||
distribution.set(2, 0)
|
||
distribution.set(3, 0)
|
||
distribution.set(4, 0)
|
||
distribution.set(5, 0)
|
||
|
||
for (let i = 0; i < reviews.length; i++) {
|
||
const review = reviews[i]
|
||
let rating = 0
|
||
if (review instanceof UTSJSONObject) {
|
||
rating = review.getNumber('rating') ?? 0
|
||
} else {
|
||
const rObj = JSON.parse(JSON.stringify(review)) as UTSJSONObject
|
||
rating = rObj.getNumber('rating') ?? 0
|
||
}
|
||
|
||
totalRating += rating
|
||
if (rating >= 4) goodCount++
|
||
|
||
const currentCount = distribution.get(rating) ?? 0
|
||
distribution.set(rating, currentCount + 1)
|
||
}
|
||
|
||
const avgRating = Math.round((totalRating / totalCount) * 10) / 10
|
||
const goodRate = Math.round((goodCount / totalCount) * 100)
|
||
|
||
const distObj = new UTSJSONObject()
|
||
distribution.forEach((value: number, key: number) => {
|
||
distObj.set(key.toString(), value)
|
||
})
|
||
|
||
result.set('total_count', totalCount)
|
||
result.set('avg_rating', avgRating)
|
||
result.set('good_rate', goodRate)
|
||
result.set('rating_distribution', distObj)
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取评价统计异常:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 评价点赞
|
||
async toggleReviewLike(reviewId: string): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('success', false)
|
||
result.set('is_liked', false)
|
||
result.set('like_count', 0)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
return result
|
||
}
|
||
|
||
// 检查是否已点赞
|
||
const checkRes = await supa
|
||
.from('ml_review_likes')
|
||
.select('id')
|
||
.eq('review_id', reviewId)
|
||
.eq('user_id', userId!)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
let isLiked = false
|
||
if (checkRes.error == null && checkRes.data != null) {
|
||
const checkData = checkRes.data as any[]
|
||
isLiked = checkData.length > 0
|
||
}
|
||
|
||
if (isLiked) {
|
||
// 取消点赞
|
||
await supa
|
||
.from('ml_review_likes')
|
||
.eq('review_id', reviewId)
|
||
.eq('user_id', userId!)
|
||
.delete()
|
||
.execute()
|
||
|
||
// 更新点赞数 - 直接查询并更新
|
||
const currentCountRes = await supa
|
||
.from('ml_product_reviews')
|
||
.select('like_count')
|
||
.eq('id', reviewId)
|
||
.single()
|
||
.execute()
|
||
|
||
if (currentCountRes.error == null && currentCountRes.data != null) {
|
||
let currentCount = 0
|
||
if (currentCountRes.data instanceof UTSJSONObject) {
|
||
currentCount = currentCountRes.data.getNumber('like_count') ?? 0
|
||
} else {
|
||
const countObj = JSON.parse(JSON.stringify(currentCountRes.data)) as UTSJSONObject
|
||
currentCount = countObj.getNumber('like_count') ?? 0
|
||
}
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('like_count', Math.max(0, currentCount - 1))
|
||
await supa
|
||
.from('ml_product_reviews')
|
||
.update(updateData)
|
||
.eq('id', reviewId)
|
||
.execute()
|
||
}
|
||
|
||
result.set('is_liked', false)
|
||
} else {
|
||
// 添加点赞
|
||
const likeRecord = new UTSJSONObject()
|
||
likeRecord.set('review_id', reviewId)
|
||
likeRecord.set('user_id', userId!)
|
||
|
||
await supa
|
||
.from('ml_review_likes')
|
||
.insert(likeRecord)
|
||
.execute()
|
||
|
||
// 更新点赞数 - 直接查询并更新
|
||
const currentCountRes = await supa
|
||
.from('ml_product_reviews')
|
||
.select('like_count')
|
||
.eq('id', reviewId)
|
||
.single()
|
||
.execute()
|
||
|
||
if (currentCountRes.error == null && currentCountRes.data != null) {
|
||
let currentCount = 0
|
||
if (currentCountRes.data instanceof UTSJSONObject) {
|
||
currentCount = currentCountRes.data.getNumber('like_count') ?? 0
|
||
} else {
|
||
const countObj = JSON.parse(JSON.stringify(currentCountRes.data)) as UTSJSONObject
|
||
currentCount = countObj.getNumber('like_count') ?? 0
|
||
}
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('like_count', currentCount + 1)
|
||
await supa
|
||
.from('ml_product_reviews')
|
||
.update(updateData)
|
||
.eq('id', reviewId)
|
||
.execute()
|
||
}
|
||
|
||
result.set('is_liked', true)
|
||
}
|
||
|
||
// 获取最新点赞数
|
||
const reviewRes = await supa
|
||
.from('ml_product_reviews')
|
||
.select('like_count')
|
||
.eq('id', reviewId)
|
||
.single()
|
||
.execute()
|
||
|
||
if (reviewRes.error == null && reviewRes.data != null) {
|
||
let likeCount = 0
|
||
if (reviewRes.data instanceof UTSJSONObject) {
|
||
likeCount = reviewRes.data.getNumber('like_count') ?? 0
|
||
} else {
|
||
const rObj = JSON.parse(JSON.stringify(reviewRes.data)) as UTSJSONObject
|
||
likeCount = rObj.getNumber('like_count') ?? 0
|
||
}
|
||
result.set('like_count', likeCount)
|
||
}
|
||
|
||
result.set('success', true)
|
||
return result
|
||
} catch (e) {
|
||
console.error('评价点赞异常:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取我的评价列表
|
||
async getMyReviews(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const response = await supa
|
||
.from('ml_product_reviews')
|
||
.select(`
|
||
*,
|
||
product:ml_products!ml_product_reviews_product_id_fkey(name, main_image_url)
|
||
`)
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (response.error != null || response.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const reviews = response.data as any[]
|
||
const result: any[] = []
|
||
|
||
for (let i = 0; i < reviews.length; i++) {
|
||
const review = reviews[i]
|
||
const processed = JSON.parse(JSON.stringify(review)) as UTSJSONObject
|
||
|
||
// 处理商品信息
|
||
const productRaw = processed.get('product')
|
||
let productName = ''
|
||
let productImage = ''
|
||
if (productRaw != null) {
|
||
let productObj: UTSJSONObject
|
||
if (productRaw instanceof UTSJSONObject) {
|
||
productObj = productRaw as UTSJSONObject
|
||
} else {
|
||
productObj = JSON.parse(JSON.stringify(productRaw)) as UTSJSONObject
|
||
}
|
||
productName = productObj.getString('name') ?? ''
|
||
productImage = productObj.getString('main_image_url') ?? ''
|
||
}
|
||
processed.set('product_name', productName)
|
||
processed.set('product_image', productImage)
|
||
|
||
// 计算是否可追加评价(7天内)
|
||
const createdAt = processed.getString('created_at') ?? ''
|
||
const createdTime = new Date(createdAt).getTime()
|
||
const now = Date.now()
|
||
const sevenDays = 7 * 24 * 60 * 60 * 1000
|
||
const canAppend = (now - createdTime) < sevenDays && (processed.getString('append_content') ?? '') === ''
|
||
processed.set('can_append', canAppend)
|
||
|
||
// 计算是否可编辑(24小时内)
|
||
const oneDay = 24 * 60 * 60 * 1000
|
||
const canEdit = (now - createdTime) < oneDay
|
||
processed.set('can_edit', canEdit)
|
||
|
||
result.push(processed)
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取我的评价失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 追加评价
|
||
async appendReview(reviewId: string, content: string, images: string[]): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('append_content', content)
|
||
updateData.set('append_images', JSON.stringify(images))
|
||
updateData.set('append_at', new Date().toISOString())
|
||
|
||
const response = await supa
|
||
.from('ml_product_reviews')
|
||
.update(updateData)
|
||
.eq('id', reviewId)
|
||
.eq('user_id', userId!)
|
||
.execute()
|
||
|
||
return response.error == null
|
||
} catch (e) {
|
||
console.error('追加评价失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 删除评价
|
||
async deleteReview(reviewId: string): Promise<boolean> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return false
|
||
|
||
const response = await supa
|
||
.from('ml_product_reviews')
|
||
.delete()
|
||
.eq('id', reviewId)
|
||
.eq('user_id', userId!)
|
||
.execute()
|
||
|
||
return response.error == null
|
||
} catch (e) {
|
||
console.error('删除评价失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ==================== 积分辅助方法 ====================
|
||
|
||
// 增加积分
|
||
private async addPoints(userId: string, points: number, type: string, description: string): Promise<boolean> {
|
||
try {
|
||
// 获取当前积分
|
||
const currentPoints = await this.getUserPoints()
|
||
const newPoints = currentPoints + points
|
||
const totalEarned = await this.getTotalEarned()
|
||
|
||
// 检查用户积分记录是否存在
|
||
const checkRes = await supa
|
||
.from('ml_user_points')
|
||
.select('user_id')
|
||
.eq('user_id', userId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
const exists = checkRes.error == null && checkRes.data != null && (checkRes.data as any[]).length > 0
|
||
|
||
if (exists) {
|
||
// 更新现有记录
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('points', newPoints)
|
||
updateData.set('total_earned', totalEarned + points)
|
||
updateData.set('updated_at', new Date().toISOString())
|
||
|
||
await supa
|
||
.from('ml_user_points')
|
||
.update(updateData)
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
} else {
|
||
// 插入新记录
|
||
const insertData = new UTSJSONObject()
|
||
insertData.set('user_id', userId)
|
||
insertData.set('points', newPoints)
|
||
insertData.set('total_earned', points)
|
||
insertData.set('updated_at', new Date().toISOString())
|
||
|
||
await supa
|
||
.from('ml_user_points')
|
||
.insert(insertData)
|
||
.execute()
|
||
}
|
||
|
||
// 记录积分变动
|
||
const record = new UTSJSONObject()
|
||
record.set('user_id', userId)
|
||
record.set('points', points)
|
||
record.set('type', type)
|
||
record.set('description', description)
|
||
|
||
await supa
|
||
.from('ml_point_records')
|
||
.insert(record)
|
||
.execute()
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('增加积分失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 扣减积分
|
||
private async deductPoints(userId: string, points: number, type: string, description: string): Promise<boolean> {
|
||
try {
|
||
const currentPoints = await this.getUserPoints()
|
||
const newPoints = currentPoints - points
|
||
|
||
if (newPoints < 0) return false
|
||
|
||
const updateData = new UTSJSONObject()
|
||
updateData.set('points', newPoints)
|
||
updateData.set('updated_at', new Date().toISOString())
|
||
|
||
await supa
|
||
.from('ml_user_points')
|
||
.update(updateData)
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
|
||
const record = new UTSJSONObject()
|
||
record.set('user_id', userId)
|
||
record.set('points', -points)
|
||
record.set('type', type)
|
||
record.set('description', description)
|
||
|
||
await supa
|
||
.from('ml_point_records')
|
||
.insert(record)
|
||
.execute()
|
||
|
||
return true
|
||
} catch (e) {
|
||
console.error('扣减积分失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 获取历史累计积分
|
||
private async getTotalEarned(): Promise<number> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return 0
|
||
|
||
const res = await supa
|
||
.from('ml_user_points')
|
||
.select('total_earned')
|
||
.eq('user_id', userId!)
|
||
.single()
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null) {
|
||
if (res.data instanceof UTSJSONObject) {
|
||
return res.data.getNumber('total_earned') ?? 0
|
||
} else {
|
||
const obj = JSON.parse(JSON.stringify(res.data)) as UTSJSONObject
|
||
return obj.getNumber('total_earned') ?? 0
|
||
}
|
||
}
|
||
return 0
|
||
} catch (e) {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// ==================== 积分过期相关API ====================
|
||
|
||
// 获取即将过期积分
|
||
async getExpiringPoints(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('expiring_points', 0)
|
||
result.set('expiring_date', null)
|
||
result.set('details', [] as any[])
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return result
|
||
|
||
// 查询30天内即将过期的积分记录
|
||
const now = new Date()
|
||
const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
|
||
const nowStr = now.toISOString()
|
||
const laterStr = thirtyDaysLater.toISOString()
|
||
|
||
const res = await supa
|
||
.from('ml_point_records')
|
||
.select('points, description, expires_at, created_at')
|
||
.eq('user_id', userId!)
|
||
.gt('points', 0)
|
||
.eq('is_expired', false)
|
||
.not('expires_at', 'is', null)
|
||
.gte('expires_at', nowStr)
|
||
.lte('expires_at', laterStr)
|
||
.order('expires_at', { ascending: true })
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('获取即将过期积分失败:', res.error)
|
||
return result
|
||
}
|
||
|
||
if (res.data != null && Array.isArray(res.data)) {
|
||
const records = res.data as any[]
|
||
let totalExpiring = 0
|
||
let earliestDate: string | null = null
|
||
const details: any[] = []
|
||
|
||
for (let i = 0; i < records.length; i++) {
|
||
const record = records[i]
|
||
let recordObj: UTSJSONObject
|
||
if (record instanceof UTSJSONObject) {
|
||
recordObj = record
|
||
} else {
|
||
recordObj = JSON.parse(JSON.stringify(record)) as UTSJSONObject
|
||
}
|
||
|
||
const points = recordObj.getNumber('points') ?? 0
|
||
const expiresAt = recordObj.getString('expires_at') ?? ''
|
||
|
||
totalExpiring += points
|
||
|
||
if (earliestDate == null || expiresAt < earliestDate) {
|
||
earliestDate = expiresAt
|
||
}
|
||
|
||
details.push({
|
||
points: points,
|
||
description: recordObj.getString('description'),
|
||
expires_at: expiresAt,
|
||
created_at: recordObj.getString('created_at') ?? ''
|
||
})
|
||
}
|
||
|
||
result.set('expiring_points', totalExpiring)
|
||
result.set('expiring_date', earliestDate != null ? earliestDate.split('T')[0] : null)
|
||
result.set('details', details)
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取即将过期积分异常:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取积分概览(包含即将过期积分)
|
||
async getPointsOverview(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('current_points', 0)
|
||
result.set('total_earned', 0)
|
||
result.set('expiring_points', 0)
|
||
result.set('expiring_date', null)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return result
|
||
|
||
const res = await supa
|
||
.from('ml_user_points')
|
||
.select('points, total_earned, expiring_points, expiring_date')
|
||
.eq('user_id', userId!)
|
||
.single()
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null) {
|
||
let data: UTSJSONObject
|
||
if (res.data instanceof UTSJSONObject) {
|
||
data = res.data as UTSJSONObject
|
||
} else {
|
||
data = JSON.parse(JSON.stringify(res.data)) as UTSJSONObject
|
||
}
|
||
|
||
result.set('current_points', data.getNumber('points') ?? 0)
|
||
result.set('total_earned', data.getNumber('total_earned') ?? 0)
|
||
result.set('expiring_points', data.getNumber('expiring_points') ?? 0)
|
||
result.set('expiring_date', data.getString('expiring_date'))
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取积分概览异常:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取过期提醒通知列表
|
||
async getExpiryNotifications(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_point_expiry_notifications')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.eq('is_sent', false)
|
||
.order('expiry_date', { ascending: true })
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取过期提醒失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 标记过期提醒为已读
|
||
async markNotificationRead(notificationId: string): Promise<boolean> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_point_expiry_notifications')
|
||
.update({ is_sent: true, sent_at: new Date().toISOString() })
|
||
.eq('id', notificationId)
|
||
.execute()
|
||
|
||
return res.error == null
|
||
} catch (e) {
|
||
console.error('标记通知失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 手动触发积分维护任务(管理员功能)
|
||
// 注意:UTS不支持rpc,此功能需要在Supabase后台手动执行或通过其他方式触发
|
||
async triggerPointsMaintenance(): Promise<boolean> {
|
||
console.warn('triggerPointsMaintenance: UTS不支持rpc调用,请在Supabase后台手动执行 daily_points_maintenance()')
|
||
return false
|
||
}
|
||
|
||
// ==================== 推销模式 - 商家配置API ====================
|
||
|
||
// 获取商家推销配置
|
||
async getMerchantPromotionConfig(merchantId: string): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('promotion_enabled', false)
|
||
result.set('share_free_enabled', false)
|
||
result.set('distribution_enabled', false)
|
||
result.set('required_count', 4)
|
||
result.set('reward_type', 'product_price')
|
||
result.set('fixed_reward_amount', 0)
|
||
|
||
try {
|
||
const res = await supa
|
||
.from('ml_merchant_promotion_config')
|
||
.select('*')
|
||
.eq('merchant_id', merchantId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null && Array.isArray(res.data)) {
|
||
const arr = res.data as any[]
|
||
if (arr.length > 0) {
|
||
const item = arr[0]
|
||
const itemAny = item as any
|
||
|
||
if (itemAny instanceof UTSJSONObject) {
|
||
result.set('promotion_enabled', itemAny.getBoolean('promotion_enabled') ?? false)
|
||
result.set('share_free_enabled', itemAny.getBoolean('share_free_enabled') ?? false)
|
||
result.set('distribution_enabled', itemAny.getBoolean('distribution_enabled') ?? false)
|
||
result.set('required_count', itemAny.getNumber('required_count') ?? 4)
|
||
result.set('reward_type', itemAny.getString('reward_type') ?? 'product_price')
|
||
result.set('fixed_reward_amount', itemAny.getNumber('fixed_reward_amount') ?? 0)
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('获取商家推销配置失败:', e)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 检查商家是否开启分享免单
|
||
async isShareFreeEnabled(merchantId: string): Promise<boolean> {
|
||
try {
|
||
const config = await this.getMerchantPromotionConfig(merchantId)
|
||
const promotionEnabled = config.get('promotion_enabled')
|
||
const shareFreeEnabled = config.get('share_free_enabled')
|
||
return (promotionEnabled === true || promotionEnabled === 'true') &&
|
||
(shareFreeEnabled === true || shareFreeEnabled === 'true')
|
||
} catch (e) {
|
||
console.error('检查分享免单状态失败:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ==================== 推销模式 - 余额相关API ====================
|
||
|
||
// 获取用户余额
|
||
async getUserBalance(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('balance', 0)
|
||
result.set('frozen_balance', 0)
|
||
result.set('total_earned', 0)
|
||
result.set('total_withdrawn', 0)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return result
|
||
|
||
const res = await supa
|
||
.from('ml_user_balance')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null && Array.isArray(res.data)) {
|
||
const arr = res.data as any[]
|
||
if (arr.length > 0) {
|
||
const item = arr[0]
|
||
const itemAny = item as any
|
||
if (itemAny instanceof UTSJSONObject) {
|
||
result.set('balance', itemAny.getNumber('balance') ?? 0)
|
||
result.set('frozen_balance', itemAny.getNumber('frozen_balance') ?? 0)
|
||
result.set('total_earned', itemAny.getNumber('total_earned') ?? 0)
|
||
result.set('total_withdrawn', itemAny.getNumber('total_withdrawn') ?? 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取用户余额失败:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取余额变动记录
|
||
async getBalanceRecords(page: number = 1, limit: number = 20): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const offset = (page - 1) * limit
|
||
const res = await supa
|
||
.from('ml_balance_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.range(offset, offset + limit - 1)
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取余额记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// ==================== 推销模式 - 分享免单相关API ====================
|
||
|
||
// 创建分享记录
|
||
async createShareRecord(productId: string, orderId: string, orderItemId: string | null, productName: string, productImage: string | null, productPrice: number): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('success', false)
|
||
result.set('share_code', '')
|
||
result.set('message', '')
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
result.set('message', '请先登录')
|
||
return result
|
||
}
|
||
|
||
// 生成分享码
|
||
const shareCode = this.generateShareCode()
|
||
|
||
const insertData = new UTSJSONObject()
|
||
insertData.set('user_id', userId)
|
||
insertData.set('product_id', productId)
|
||
insertData.set('order_id', orderId)
|
||
insertData.set('order_item_id', orderItemId)
|
||
insertData.set('share_code', shareCode)
|
||
insertData.set('product_name', productName)
|
||
insertData.set('product_image', productImage)
|
||
insertData.set('product_price', productPrice)
|
||
insertData.set('required_count', 4)
|
||
insertData.set('current_count', 0)
|
||
insertData.set('status', 0)
|
||
|
||
const res = await supa
|
||
.from('ml_share_records')
|
||
.insert(insertData)
|
||
.execute()
|
||
|
||
if (res.error != null) {
|
||
console.error('[createShareRecord] 插入失败:', res.error)
|
||
console.error('[createShareRecord] 插入数据:', JSON.stringify(insertData))
|
||
result.set('message', '创建分享记录失败: ' + (res.error.message ?? '未知错误'))
|
||
return result
|
||
}
|
||
|
||
// 获取插入记录的id
|
||
let insertedId = ''
|
||
if (res.data != null && Array.isArray(res.data) && res.data.length > 0) {
|
||
const inserted = res.data[0]
|
||
let insertedObj: UTSJSONObject | null = null
|
||
if (inserted instanceof UTSJSONObject) {
|
||
insertedObj = inserted
|
||
} else {
|
||
insertedObj = JSON.parse(JSON.stringify(inserted)) as UTSJSONObject
|
||
}
|
||
insertedId = insertedObj.getString('id') ?? ''
|
||
}
|
||
|
||
result.set('success', true)
|
||
result.set('id', insertedId)
|
||
result.set('share_code', shareCode)
|
||
result.set('message', '分享创建成功')
|
||
return result
|
||
} catch (e) {
|
||
console.error('创建分享记录失败:', e)
|
||
result.set('message', '创建分享记录异常')
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 生成分享码
|
||
private generateShareCode(): string {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||
let result = ''
|
||
for (let i = 0; i < 8; i++) {
|
||
const randomIndex = Math.floor(Math.random() * chars.length)
|
||
result += chars.charAt(randomIndex)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// 验证分享码
|
||
async validateShareCode(shareCode: string): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('valid', false)
|
||
result.set('share_record', null)
|
||
|
||
try {
|
||
const res = await supa
|
||
.from('ml_share_records')
|
||
.select('*')
|
||
.eq('share_code', shareCode)
|
||
.eq('status', 0)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null && Array.isArray(res.data)) {
|
||
const arr = res.data as any[]
|
||
if (arr.length > 0) {
|
||
result.set('valid', true)
|
||
result.set('share_record', arr[0])
|
||
}
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('验证分享码失败:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取我的分享记录
|
||
async getMyShareRecords(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_share_records')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取分享记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取分享详情
|
||
async getShareDetail(shareId: string): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('share_record', null)
|
||
result.set('secondary_purchases', [] as any[])
|
||
|
||
try {
|
||
const res = await supa
|
||
.from('ml_share_records')
|
||
.select('*')
|
||
.eq('id', shareId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
if (res.error == null && res.data != null && Array.isArray(res.data)) {
|
||
const arr = res.data as any[]
|
||
if (arr.length > 0) {
|
||
result.set('share_record', arr[0])
|
||
}
|
||
}
|
||
|
||
// 获取二级购买记录
|
||
const purchasesRes = await supa
|
||
.from('ml_secondary_purchases')
|
||
.select('*')
|
||
.eq('share_record_id', shareId)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (purchasesRes.error == null && purchasesRes.data != null) {
|
||
result.set('secondary_purchases', purchasesRes.data)
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取分享详情失败:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取免单奖励记录
|
||
async getFreeOrderRewards(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_free_order_rewards')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取免单奖励记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// ==================== 推销模式 - 会员等级相关API ====================
|
||
|
||
// 获取会员等级列表
|
||
async getMemberLevels(): Promise<any[]> {
|
||
try {
|
||
const res = await supa
|
||
.from('ml_member_levels')
|
||
.select('*')
|
||
.eq('is_active', true)
|
||
.order('level_rank', { ascending: true } as OrderOptions)
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取会员等级列表失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
|
||
// 获取用户会员信息
|
||
async getUserMemberInfo(): Promise<UTSJSONObject> {
|
||
const result = new UTSJSONObject()
|
||
result.set('member_level', 0)
|
||
result.set('level_name', '普通会员')
|
||
result.set('discount', 1.0)
|
||
result.set('total_spent', 0)
|
||
result.set('next_level', null)
|
||
result.set('progress_percent', 0)
|
||
result.set('manual_level', false)
|
||
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) return result
|
||
|
||
// 获取用户信息(包括 tier_id)
|
||
const userRes = await supa
|
||
.from('ml_user_profiles')
|
||
.select('tier_id, total_spent, manual_level')
|
||
.eq('user_id', userId!)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
let tierId: string = ''
|
||
let totalSpent = 0
|
||
let manualLevel = false
|
||
|
||
if (userRes.error == null && userRes.data != null && Array.isArray(userRes.data)) {
|
||
const arr = userRes.data as any[]
|
||
if (arr.length > 0) {
|
||
const item = arr[0]
|
||
const itemAny = item as any
|
||
if (itemAny instanceof UTSJSONObject) {
|
||
tierId = itemAny.getString('tier_id') ?? ''
|
||
totalSpent = itemAny.getNumber('total_spent') ?? 0
|
||
manualLevel = itemAny.getBoolean('manual_level') ?? false
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取等级信息
|
||
const levels = await this.getMemberLevels()
|
||
let levelName = '普通会员'
|
||
let discount = 1.0
|
||
let nextLevel: UTSJSONObject | null = null
|
||
let progressPercent = 0
|
||
let currentLevelRank = 0
|
||
|
||
// 通过 tier_id 匹配等级
|
||
for (let i = 0; i < levels.length; i++) {
|
||
const level = levels[i]
|
||
const levelAny = level as any
|
||
let levelId = ''
|
||
let levelNameStr = ''
|
||
let levelRank = 0
|
||
let levelDiscount = 1.0
|
||
|
||
if (levelAny instanceof UTSJSONObject) {
|
||
levelId = levelAny.getString('id') ?? ''
|
||
levelNameStr = levelAny.getString('name') ?? ''
|
||
levelRank = levelAny.getNumber('level_rank') ?? 0
|
||
levelDiscount = levelAny.getNumber('discount_rate') ?? 1.0
|
||
}
|
||
|
||
// 通过 tier_id 匹配当前等级
|
||
if (levelId == tierId) {
|
||
levelName = levelNameStr
|
||
discount = levelDiscount
|
||
currentLevelRank = levelRank
|
||
}
|
||
}
|
||
|
||
// 找下一等级(level_rank 更大的第一个等级)
|
||
for (let i = 0; i < levels.length; i++) {
|
||
const level = levels[i]
|
||
const levelAny = level as any
|
||
let levelRank = 0
|
||
let levelNameStr = ''
|
||
let levelMinAmount = 0
|
||
|
||
if (levelAny instanceof UTSJSONObject) {
|
||
levelRank = levelAny.getNumber('level_rank') ?? 0
|
||
levelNameStr = levelAny.getString('name') ?? ''
|
||
levelMinAmount = levelAny.getNumber('min_amount') ?? 0
|
||
}
|
||
|
||
if (levelRank > currentLevelRank && nextLevel == null) {
|
||
const nextLevelObj = new UTSJSONObject()
|
||
const levelObj = level as UTSJSONObject
|
||
nextLevelObj.set('id', levelObj.getString('id') ?? '')
|
||
nextLevelObj.set('name', levelNameStr)
|
||
nextLevelObj.set('min_amount', levelMinAmount)
|
||
nextLevel = nextLevelObj
|
||
}
|
||
}
|
||
|
||
result.set('member_level', currentLevelRank)
|
||
result.set('level_name', levelName)
|
||
result.set('discount', discount)
|
||
result.set('total_spent', totalSpent)
|
||
result.set('next_level', nextLevel)
|
||
result.set('progress_percent', progressPercent)
|
||
result.set('manual_level', manualLevel)
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('获取用户会员信息失败:', e)
|
||
return result
|
||
}
|
||
}
|
||
|
||
// 获取当前等级的最低消费金额
|
||
private getCurrentLevelMinAmount(levels: any[], currentLevel: number): number {
|
||
for (let i = 0; i < levels.length; i++) {
|
||
const level = levels[i]
|
||
const levelAny = level as any
|
||
if (levelAny instanceof UTSJSONObject) {
|
||
const levelId = levelAny.getNumber('id') ?? 0
|
||
if (levelId === currentLevel) {
|
||
return levelAny.getNumber('min_amount') ?? 0
|
||
}
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// 获取会员等级变更记录
|
||
async getMemberLevelLogs(): Promise<any[]> {
|
||
try {
|
||
const userId = this.getCurrentUserId()
|
||
if (userId == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
const res = await supa
|
||
.from('ml_member_level_logs')
|
||
.select('*')
|
||
.eq('user_id', userId!)
|
||
.order('created_at', { ascending: false })
|
||
.execute()
|
||
|
||
if (res.error != null || res.data == null) {
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
|
||
return res.data as any[]
|
||
} catch (e) {
|
||
console.error('获取会员等级变更记录失败:', e)
|
||
const empty: any[] = []
|
||
return empty
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
export const supabaseService = new SupabaseService()
|
||
|
||
// 默认导出
|
||
export default supabaseService
|