补充数据库数据,修改分类栏内容

This commit is contained in:
2026-05-21 11:50:32 +08:00
parent b8b0b453e0
commit 7ba3d313be
32 changed files with 6657 additions and 1684 deletions

View File

@@ -175,6 +175,8 @@ function parseProductFromRaw(item: any): Product {
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'),
@@ -193,6 +195,12 @@ function parseProductFromRaw(item: any): Product {
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'),
@@ -256,6 +264,12 @@ export type Category = {
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
}
@@ -290,6 +304,13 @@ export type Product = {
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
@@ -491,6 +512,192 @@ function emptyProductPage(page: number, limit: number): PaginatedResponse<Produc
}
}
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
}
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[]
@@ -946,6 +1153,373 @@ class SupabaseService {
}
}
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 {