diff --git a/ak/config.uts b/ak/config.uts index 9db7b3c6..10482315 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -1,32 +1,20 @@ // Supabase 配置 // 内网环境 - 本地部署的 Supabase -// IP: 192.168.1.62 -// IP: 192.168.1.62 +// IP: 192.168.1.61 +// IP: 192.168.1.61 // Kong HTTP Port: 8000 -<<<<<<< HEAD - -//export const SUPA_URL: string = 'http://192.168.1.61:18000' -//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' +//自己的配置自己解开即可 export const SUPA_URL: string = 'http://192.168.1.61:18000' export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' - -// WebSocket 实时连接(内网使用 ws:// 而非 wss://) -export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket' -//export const WS_URL: string = 'ws://localhost:18000/realtime/v1/websocket' -======= -//自己的配置自己解开即可 -//export const SUPA_URL: string = 'http://192.168.1.61:18000' -//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' //export const SUPA_URL: string = 'http://192.168.1.62:18000' //export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' -export const SUPA_URL: string = 'http://192.168.1.63:18000' -export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU' +// export const SUPA_URL: string = 'http://192.168.1.63:18000' +// export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU' // WebSocket 实时连接(内网使用 ws:// 而非 wss://) // export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket' -//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket' -export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' ->>>>>>> origin/main +export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket' +// export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' // 备用配置(已注释,如需切换可取消注释) // 开发环境 - 其他内网地址 @@ -49,4 +37,4 @@ export const HOME_REDIRECT: string = '/pages/mall/consumer/index' export const TABORPAGE: string = '/pages/mall/consumer/index' // 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向) -export const IS_TEST_MODE: boolean = true +export const IS_TEST_MODE: boolean = true \ No newline at end of file diff --git a/ak/configbackup.uts b/ak/configbackup.uts deleted file mode 100644 index c5fe910b..00000000 --- a/ak/configbackup.uts +++ /dev/null @@ -1,38 +0,0 @@ - -// // export const SUPA_URL: string = 'http://192.168.0.150:8080' -// // export const SUPA_ANON_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE' -// // export const SUPA_SERVICE_ROLE_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q' -// // export const SUPA_KEY = SUPA_ANON_KEY -// // export const WS_URL: string = 'ws://'+'/192.168.0.150:8080'+'/realtime/v1/websocket'; -// export const SUPA_URL: string = 'https://ak3.oulog.com'; -// export const SUPA_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; -// export const SUPA_SERVICE_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" -// export const WS_URL: string = 'wss://'+'ak3.oulog.com'+'/realtime/v1/websocket'; -// // Optional: Edge Function or API endpoint that returns S3 presigned POST -// // Expected response: { url: string, fields: object, publicUrl?: string } -// export const S3_PRESIGN_URL: string = '' -// // Optional: Public base URL for your S3/CND to build final URLs when presign response has no publicUrl -// export const S3_PUBLIC_BASE: string = '' -// export const RAG_API_KEY: string ='ragflow-lkZmNjMzI2YzRiNjExZWY4ZGIwMDI0Mm'; -// export const RAG_BASE_URL: string ='https://rag.oulog.com'; -// export const RAG_AGENT_ID: string ='15b01b26128111f08cd30242ac120006'; -// export const TABORPAGE:boolean = false - -// // export const HOME_REDIRECT :string = '/pages/ec/health/ecalert' -// //export const HOME_REDIRECT :string = '/pages/sport/index' -// // export const HOME_REDIRECT :string = '/pages/sport/teacher/dashboard' -// // export const HOME_REDIRECT :string = '/pages/test/multi_device_monitor' - - - -// // export const HOME_REDIRECT :string = '/pages/ec/admin/dashboard' -// // export const HOME_REDIRECT :string = '/pages/sense/healthble' -// //export const HOME_REDIRECT :string = '/pages/ec/elder/dashboard' -// // export const HOME_REDIRECT :string = '/pages/ec/caregiver/dashboard' -// // export const HOME_REDIRECT :string = '/pages/ec/doctor/dashboard' -// // export const HOME_REDIRECT :string = '/pages/ec/family/dashboard' - - - - - diff --git a/ak/configme.uts b/ak/configme.uts deleted file mode 100644 index 31f4a263..00000000 --- a/ak/configme.uts +++ /dev/null @@ -1,29 +0,0 @@ -// // Supabase 配置 -// // 内网环境 - 本地部署的 Supabase -// // 家里通过端口映射访问公司内网Supabase -// // 本地映射端口:HTTP 18000, WebSocket 13000 -// export const SUPA_URL: string = 'http://192.168.1.61:18000' -// export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg1234567890' -// //export const SUPA_URL: string = 'https://ak3.oulog.com' -// //export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE' - -// // WebSocket 实时连接(内网使用 ws:// 而非 wss://) -// export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket' - -// // 备用配置(已注释,如需切换可取消注释) -// // 开发环境 - 其他内网地址 -// // export const SUPA_URL: string = 'http://192.168.0.150:8080' -// // export const SUPA_KEY: string = 'your-anon-key' -// // export const WS_URL: string = 'ws://192.168.0.150:8080/realtime/v1/websocket' - -// // 生产环境 - Supabase 云服务(已注释) -// // export const SUPA_URL: string = 'https://ak3.oulog.com' -// // export const SUPA_KEY: string = 'your-anon-key' -// // export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket' - -// // 指向你的 Supabase 服务(开发/私有部署) -// // export const SUPA_URL: string = 'http://192.168.1.64:3000' -// // export const SUPA_KEY: string = 'your-anon-key' -// // export const WS_URL: string = 'ws://192.168.1.64:3000/realtime/v1' - -// //export const HOME_REDIRECT :string = '/pages/mall/consumer/index' diff --git a/check_db_coupons.py b/check_db_coupons.py new file mode 100644 index 00000000..2be0b4d3 --- /dev/null +++ b/check_db_coupons.py @@ -0,0 +1,57 @@ +import urllib.request +import json +import ssl +import time + +# Config from ak/config.uts +SUPA_URL = 'http://192.168.1.61:18000' +SUPA_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' + +headers = { + "apikey": SUPA_KEY, + "Authorization": f"Bearer {SUPA_KEY}", + "Content-Type": "application/json" +} + +def get_data(table, query="select=*"): + try: + url = f"{SUPA_URL}/rest/v1/{table}?{query}" + req = urllib.request.Request(url, headers=headers) + # Ignore SSL errors for local IP + context = ssl._create_unverified_context() + try: + with urllib.request.urlopen(req, context=context, timeout=5) as response: + if response.status == 200: + return json.loads(response.read().decode()) + else: + return f"Error: {response.status}" + except urllib.error.URLError as e: + return f"Connection Failed: {e}" + except Exception as e: + return f"Exception: {e}" + +print("--- DIAGNOSTIC START ---") +print(f"Target: {SUPA_URL}") + +# Check 1: Shops +print("\n[ Checking Shops (ml_shops) ]") +shops = get_data("ml_shops", "select=id,merchant_id,shop_name&status=eq.1") +if isinstance(shops, list): + print(f"Found {len(shops)} active shops.") + for shop in shops: + print(f" - {shop.get('shop_name')} (ID: {shop.get('merchant_id')})") +else: + print(f"Failed to fetch shops: {shops}") + +# Check 2: Coupons +print("\n[ Checking Coupons (ml_coupon_templates) ]") +coupons = get_data("ml_coupon_templates", "select=id,name,merchant_id,status") +if isinstance(coupons, list): + print(f"Found {len(coupons)} coupon templates.") + for coupon in coupons: + mid_disp = coupon.get('merchant_id') if coupon.get('merchant_id') else "PLATFORM" + print(f" - {coupon.get('name')} | Owner: {mid_disp} | Status: {coupon.get('status')}") +else: + print(f"Failed to fetch coupons: {coupons}") + +print("\n--- DIAGNOSTIC END ---") diff --git a/check_db_schema.py b/check_db_schema.py new file mode 100644 index 00000000..7c58defc --- /dev/null +++ b/check_db_schema.py @@ -0,0 +1,34 @@ +import urllib.request +import json +import ssl + +SUPA_URL = 'http://192.168.1.61:18000' +SUPA_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' + +headers = { + "apikey": SUPA_KEY, + "Authorization": f"Bearer {SUPA_KEY}", + "Content-Type": "application/json" +} + +def get_one_row(table): + try: + url = f"{SUPA_URL}/rest/v1/{table}?select=*&limit=1" + req = urllib.request.Request(url, headers=headers) + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context, timeout=5) as response: + if response.status == 200: + data = json.loads(response.read().decode()) + if len(data) > 0: + return data[0] + else: + return "Empty table" + return f"Error {response.status}" + except Exception as e: + return str(e) + +print("\n[ml_user_coupons Columns]") +print(get_one_row("ml_user_coupons")) + +print("\n[ml_products Columns]") +print(get_one_row("ml_products")) diff --git a/check_products_schema.py b/check_products_schema.py new file mode 100644 index 00000000..8ba48ca4 --- /dev/null +++ b/check_products_schema.py @@ -0,0 +1,39 @@ +import urllib.request +import json +import ssl + +SUPA_URL = 'http://192.168.1.61:18000' +SUPA_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' + +headers = { + "apikey": SUPA_KEY, + "Authorization": f"Bearer {SUPA_KEY}", + "Content-Type": "application/json" +} + +def get_one_row(table): + try: + # Limit 1 to just see columns + url = f"{SUPA_URL}/rest/v1/{table}?select=*&limit=1" + req = urllib.request.Request(url, headers=headers) + context = ssl._create_unverified_context() + with urllib.request.urlopen(req, context=context, timeout=5) as response: + if response.status == 200: + data = json.loads(response.read().decode()) + if len(data) > 0: + return data[0] + else: + return "Empty table" + return f"Error {response.status}" + except Exception as e: + return str(e) + +print("Checking 'ml_products' table columns:") +prod = get_one_row("ml_products") +if isinstance(prod, dict): + print("Columns found:", list(prod.keys())) + print("Sample Price keys:", [k for k in prod.keys() if 'price' in k]) + print("Sample Stock keys:", [k for k in prod.keys() if 'stock' in k]) + print("Sample Sales keys:", [k for k in prod.keys() if 'sale' in k]) +else: + print(prod) diff --git a/doc_mall/consumer/sql/add_coupons_for_existing_shops.sql b/doc_mall/consumer/sql/add_coupons_for_existing_shops.sql new file mode 100644 index 00000000..1a51be57 --- /dev/null +++ b/doc_mall/consumer/sql/add_coupons_for_existing_shops.sql @@ -0,0 +1,135 @@ +-- 1. 为所有现有店铺创建优惠券模板 +-- 这个脚本会查找数据库中现有的所有 Shop,并为每个 Shop 创建一系列优惠券 + +DO $$ +DECLARE + r_shop RECORD; + v_template_id UUID; +BEGIN + -- 遍历所有状态正常的店铺 + FOR r_shop IN SELECT id, merchant_id, shop_name FROM public.ml_shops WHERE status = 1 LOOP + + RAISE NOTICE '正在为店铺: % 创建优惠券模板...', r_shop.shop_name; + + -- A. 创建店铺满减券 (满100减10) + -- 判断是否已存在同名券,避免重复创建 + IF NOT EXISTS (SELECT 1 FROM public.ml_coupon_templates WHERE merchant_id = r_shop.merchant_id AND name = '店铺新人礼 - 满100减10') THEN + INSERT INTO public.ml_coupon_templates ( + merchant_id, + name, + coupon_type, + discount_type, + discount_value, + min_order_amount, + total_quantity, + start_time, + end_time, + status, + applicable_products + ) VALUES ( + r_shop.merchant_id, + '店铺新人礼 - 满100减10', + 1, -- 满减 + 1, -- 固定金额 + 10.00, + 100.00, + 500, -- 发放500张 + now(), + now() + interval '1 year', + 1, + '[]'::jsonb + ); + END IF; + + -- B. 创建店铺折扣券 (9.5折, 满200可用) + IF NOT EXISTS (SELECT 1 FROM public.ml_coupon_templates WHERE merchant_id = r_shop.merchant_id AND name = '全店95折券') THEN + INSERT INTO public.ml_coupon_templates ( + merchant_id, + name, + coupon_type, + discount_type, + discount_value, + min_order_amount, + total_quantity, + start_time, + end_time, + status, + applicable_products + ) VALUES ( + r_shop.merchant_id, + '全店95折券', + 1, -- 满减/折扣 + 2, -- 百分比 + 0.95, -- 95折 + 200.00, + 1000, + now(), + now() + interval '1 year', + 1, + '[]'::jsonb + ); + END IF; + + -- C. 创建大额满减券 (满500减60) + IF NOT EXISTS (SELECT 1 FROM public.ml_coupon_templates WHERE merchant_id = r_shop.merchant_id AND name = '店庆大促 - 满500减60') THEN + INSERT INTO public.ml_coupon_templates ( + merchant_id, + name, + coupon_type, + discount_type, + discount_value, + min_order_amount, + total_quantity, + start_time, + end_time, + status, + applicable_products + ) VALUES ( + r_shop.merchant_id, + '店庆大促 - 满500减60', + 1, + 1, + 60.00, + 500.00, + 200, + now(), + now() + interval '30 days', + 1, + '[]'::jsonb + ); + END IF; + + END LOOP; + + -- D. 创建几个平台通用券 (如果不存在) + IF NOT EXISTS (SELECT 1 FROM public.ml_coupon_templates WHERE merchant_id IS NULL AND name = '平台春季大促红包') THEN + INSERT INTO public.ml_coupon_templates ( + merchant_id, + name, + coupon_type, + discount_type, + discount_value, + min_order_amount, + total_quantity, + start_time, + end_time, + status, + applicable_products + ) VALUES ( + NULL, + '平台春季大促红包', + 1, + 1, + 8.00, + 0, + 10000, + now(), + now() + interval '3 months', + 1, + '[]'::jsonb + ); + END IF; + + RAISE NOTICE '所有现有店铺优惠券模板创建完成。'; + +END $$; diff --git a/main.uts b/main.uts index da42848e..ec1fc450 100644 --- a/main.uts +++ b/main.uts @@ -2,6 +2,8 @@ import { createSSRApp } from 'vue' import App from './App.uvue' +console.log('Main App Initializing... Force Rebuild ' + Date.now()) + export function createApp() { const app = createSSRApp(App) diff --git a/mall/pages/mall/consumer/category.uvue b/mall/pages/mall/consumer/category.uvue index ac527d51..fb639e1c 100644 --- a/mall/pages/mall/consumer/category.uvue +++ b/mall/pages/mall/consumer/category.uvue @@ -153,14 +153,38 @@ onMounted(async() => { // 添加加载分类的方法 const loadCategories = async () => { try { - const categories = await supabaseService.getCategories() - console.log('加载分类数据成功,数量:', categories.length) + const categoriesData = await supabaseService.getCategories() + console.log('加载分类数据成功,数量:', categoriesData.length) + + // 映射数据并添加默认颜色,防止选中时背景透明导致文字看不清 + const categories = categoriesData.map((cat: any) => ({ + id: cat.id, + name: cat.name, + icon: cat.icon_url || '📦', + desc: cat.description || '', + description: cat.description || '', // 兼容不同字段名 + color: cat.color || '#4CAF50' // 默认绿色,如果有color字段则使用 + })) as Category[] + if (categories.length > 0) { primaryCategories.value = categories // 如果没有通过参数设置分类,则设置默认选中第一个分类 if (!activePrimary.value && categories[0]) { activePrimary.value = categories[0].id console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id) + currentCategoryName.value = categories[0].name + currentCategoryDesc.value = categories[0].description || '' + } else if (activePrimary.value) { + // 如果已经选中了分类(可能来自Storage),更新显示信息 + const current = categories.find(c => c.id == activePrimary.value) + if (current) { + currentCategoryName.value = current.name + currentCategoryDesc.value = current.description || '' + // 如果此时没有商品列表(且没有正在加载),可能需要加载 + if (productList.value.length === 0 && !loading.value) { + loadProducts() + } + } } } else { console.warn('从Supabase获取的分类数据为空') @@ -290,6 +314,21 @@ onShow(() => { console.log('页面显示时间:', Date.now()) console.log('当前活动分类:', activePrimary.value) + // 1. 优先检查 Storage 中的参数 (由首页传入) + const storageCategoryId = uni.getStorageSync('selectedCategory') + if (storageCategoryId) { + console.log('✅ onShow中找到Storage分类参数:', storageCategoryId) + hasLoadedFromParams.value = true + // 清除Storage,防止下次误读 + uni.removeStorageSync('selectedCategory') + + if (activePrimary.value !== storageCategoryId) { + selectPrimaryCategory(storageCategoryId) + } + // 如果分类还没加载完,这里设置了ID,等loadCategories完成后会自动匹配信息 + return + } + // 在onShow中,我们也需要检查是否有新的参数 // 因为当从主页再次点击分类跳转过来时,可能不会触发onLoad // 而是触发onShow diff --git a/mall/pages/mall/consumer/chat.uvue b/mall/pages/mall/consumer/chat.uvue index 0ab769f1..175c71e9 100644 --- a/mall/pages/mall/consumer/chat.uvue +++ b/mall/pages/mall/consumer/chat.uvue @@ -7,7 +7,7 @@ - 在线客服 + {{ headerTitle }} 在线 @@ -119,8 +119,10 @@ + + diff --git a/mall/pages/mall/consumer/checkout.uvue b/mall/pages/mall/consumer/checkout.uvue index bf9255c9..94964371 100644 --- a/mall/pages/mall/consumer/checkout.uvue +++ b/mall/pages/mall/consumer/checkout.uvue @@ -81,7 +81,7 @@ 优惠券 - {{ selectedCoupon.template?.name || '优惠券' }} + {{ selectedCoupon.template?.name || '已选择优惠券 (¥' + selectedCoupon.template?.discount_value + ')' }} 选择优惠券 @@ -306,6 +306,7 @@ type CheckoutItemType = { quantity: number shop_id?: string shop_name?: string + merchant_id?: string } type DeliveryOptionType = { @@ -406,11 +407,9 @@ const totalAmount = computed(() => { } const itemTotal = price * quantity - console.log(`商品 ${item.product_name},单价: ${price},数量: ${quantity},小计: ${itemTotal}`) return sum + itemTotal }, 0) - console.log('商品总价计算结果:', total) return total }) @@ -423,7 +422,7 @@ const discountAmount = computed(() => { if (!selectedCoupon.value || !selectedCoupon.value.template) return 0 const coupon = selectedCoupon.value.template - // 确保使用计算后的商品总价进行比较 + // 确保使用计算后的商品总价进行比较 (should be min_order_amount) if (totalAmount.value < coupon.min_order_amount) return 0 // 简单处理:假设都是满减券 @@ -499,6 +498,7 @@ onLoad(() => { const processCheckoutItems = (items: any[]) => { // 数据清洗:确保价格和数量是数字类型 if (items && items.length > 0) { + // 关键修复:需要保留 shop_id 和 merchant_id items = items.map((item: any) => { // 确保价格是数字 let price = 0 @@ -522,7 +522,11 @@ const processCheckoutItems = (items: any[]) => { product_image: item.product_image || item.image || '', sku_specifications: item.sku_specifications || item.spec || {}, price: Number(price.toFixed(2)), - quantity: quantity + quantity: quantity, + // 保留店铺信息,用于分组 + shop_id: item.shop_id || item.shopId || 'unknown', + shop_name: item.shop_name || item.shopName || '', + merchant_id: item.merchant_id || item.merchantId } }) } @@ -531,7 +535,7 @@ const processCheckoutItems = (items: any[]) => { if (checkoutItems.value && checkoutItems.value.length > 0) { console.log('清洗后商品价格明细:') checkoutItems.value.forEach((item: any, index: number) => { - console.log(`商品${index}:`, item.product_name, '价格:', item.price, '类型:', typeof item.price, '数量:', item.quantity) + console.log(`商品${index}:`, item.product_name, '价格:', item.price, 'shop:', item.shop_id) }) } } @@ -554,11 +558,9 @@ onMounted(() => { // 页面显示时触发 const onShow = () => { - console.log('checkout页面显示,检查登录状态并重新加载地址') - // 检查用户登录状态 + // 检查用户登录状态 const userId = getCurrentUserId() if (userId) { - console.log('用户已登录,重新加载地址数据') // 重新加载默认地址和地址列表 loadDefaultAddress() loadAddressList() @@ -601,7 +603,10 @@ const loadFromLocalStorage = () => { product_image: item.image || '', sku_specifications: item.spec ? { spec: item.spec } : {}, price: Number(item.price) || 0, - quantity: quantity + quantity: quantity, + shop_id: item.shop_id || item.shopId, + shop_name: item.shop_name || item.shopName, + merchant_id: item.merchant_id || item.merchantId } }) // 再次经过process处理确保类型正确 @@ -625,13 +630,10 @@ const loadDefaultAddress = async () => { try { // 首先检查用户是否登录 const currentUserId = getCurrentUserId() - console.log('loadDefaultAddress: 当前用户ID:', currentUserId) // 如果用户已登录,尝试从Supabase加载地址数据 if (currentUserId) { - console.log('loadDefaultAddress: 用户已登录,从Supabase加载地址') const supabaseAddresses = await supabaseService.getAddresses() - console.log('loadDefaultAddress: Supabase返回地址:', supabaseAddresses) if (supabaseAddresses && supabaseAddresses.length > 0) { // 查找默认地址 @@ -648,7 +650,6 @@ const loadDefaultAddress = async () => { detail: defaultAddress.detail_address, is_default: defaultAddress.is_default } - console.log('loadDefaultAddress: 找到默认地址:', selectedAddress.value) } else { // 如果没有默认地址,使用第一个地址 const firstAddress = supabaseAddresses[0] @@ -662,7 +663,6 @@ const loadDefaultAddress = async () => { detail: firstAddress.detail_address, is_default: firstAddress.is_default } - console.log('loadDefaultAddress: 使用第一个地址:', selectedAddress.value) } // 同时更新本地存储缓存 @@ -677,17 +677,12 @@ const loadDefaultAddress = async () => { isDefault: addr.is_default })) uni.setStorageSync('addresses', JSON.stringify(localAddresses)) - console.log('loadDefaultAddress: 地址已保存到本地存储') - } else { - console.log('loadDefaultAddress: Supabase未返回地址数据') } } // 如果Supabase没有地址数据或用户未登录,尝试从本地存储加载 if (!selectedAddress.value) { - console.log('loadDefaultAddress: 尝试从本地存储加载地址') const storedAddresses = uni.getStorageSync('addresses') - console.log('loadDefaultAddress: 本地存储地址数据:', storedAddresses) if (storedAddresses) { try { const addresses = JSON.parse(storedAddresses as string) as any[] @@ -705,7 +700,6 @@ const loadDefaultAddress = async () => { detail: defaultAddress.detail, is_default: defaultAddress.isDefault } - console.log('loadDefaultAddress: 从本地存储找到默认地址:', selectedAddress.value) } else { // 如果没有默认地址,使用第一个地址 const firstAddress = addresses[0] @@ -719,7 +713,6 @@ const loadDefaultAddress = async () => { detail: firstAddress.detail, is_default: firstAddress.isDefault } - console.log('loadDefaultAddress: 从本地存储使用第一个地址:', selectedAddress.value) } } } catch (err) { @@ -730,7 +723,6 @@ const loadDefaultAddress = async () => { // 如果仍然没有地址,使用模拟地址数据 if (!selectedAddress.value) { - console.log('loadDefaultAddress: 使用模拟地址数据') // 模拟地址数据 const mockAddresses = [ { @@ -757,7 +749,6 @@ const loadDefaultAddress = async () => { // 保存模拟地址到本地存储 uni.setStorageSync('addresses', JSON.stringify(mockAddresses)) - console.log('loadDefaultAddress: 模拟地址已保存到本地存储') // 使用第一个地址作为默认地址 selectedAddress.value = { @@ -770,50 +761,10 @@ const loadDefaultAddress = async () => { detail: mockAddresses[0].detail, is_default: mockAddresses[0].isDefault } - console.log('loadDefaultAddress: 使用模拟地址:', selectedAddress.value) } - // 如果仍然没有地址,selectedAddress.value将保持为null - // 用户可以在结算页面点击地址区域添加新地址 - } catch (error) { - console.error('从Supabase加载默认地址失败:', error) - // 失败时从本地存储加载 - const storedAddresses = uni.getStorageSync('addresses') - if (storedAddresses) { - try { - const addresses = JSON.parse(storedAddresses as string) as any[] - if (addresses && addresses.length > 0) { - const defaultAddress = addresses.find((addr: any) => addr.isDefault === true) - if (defaultAddress) { - selectedAddress.value = { - id: defaultAddress.id, - recipient_name: defaultAddress.name, - phone: defaultAddress.phone, - province: defaultAddress.province, - city: defaultAddress.city, - district: defaultAddress.district, - detail: defaultAddress.detail, - is_default: defaultAddress.isDefault - } - } else { - const firstAddress = addresses[0] - selectedAddress.value = { - id: firstAddress.id, - recipient_name: firstAddress.name, - phone: firstAddress.phone, - province: firstAddress.province, - city: firstAddress.city, - district: firstAddress.district, - detail: firstAddress.detail, - is_default: firstAddress.isDefault - } - } - } - } catch (err) { - console.error('解析本地地址数据失败:', err) - } - } + console.error('加载地址失败:', error) } } @@ -821,7 +772,6 @@ const loadDefaultAddress = async () => { const getCurrentUserId = (): string => { // 使用 SupabaseService 获取当前用户ID const userId = supabaseService.getCurrentUserId() - console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId) return userId ?? '' } @@ -838,19 +788,14 @@ const getFullAddress = (address: any): string => { // 加载地址列表 const loadAddressList = async () => { + // 逻辑保持不变,略微简化控制台输出以免刷屏 try { - // 首先检查用户是否登录 const currentUserId = getCurrentUserId() - console.log('loadAddressList: 当前用户ID:', currentUserId) - // 如果用户已登录,尝试从Supabase加载地址数据 if (currentUserId) { - console.log('loadAddressList: 用户已登录,从Supabase加载地址') const supabaseAddresses = await supabaseService.getAddresses() - console.log('loadAddressList: Supabase返回地址:', supabaseAddresses) if (supabaseAddresses && supabaseAddresses.length > 0) { - // 转换地址格式以匹配addressList的结构 addressList.value = supabaseAddresses.map((addr: SupabaseUserAddress) => ({ id: addr.id, recipient_name: addr.recipient_name, @@ -861,9 +806,7 @@ const loadAddressList = async () => { detail: addr.detail_address, is_default: addr.is_default })) - console.log('loadAddressList: 从Supabase加载地址成功,数量:', addressList.value.length) - // 同时更新本地存储缓存 const localAddresses = supabaseAddresses.map((addr: SupabaseUserAddress) => ({ id: addr.id, name: addr.recipient_name, @@ -875,22 +818,15 @@ const loadAddressList = async () => { isDefault: addr.is_default })) uni.setStorageSync('addresses', JSON.stringify(localAddresses)) - console.log('loadAddressList: 地址已保存到本地存储') - } else { - console.log('loadAddressList: Supabase未返回地址数据') } } - // 如果Supabase没有地址数据或用户未登录,尝试从本地存储加载 if (!addressList.value || addressList.value.length === 0) { - console.log('loadAddressList: 尝试从本地存储加载地址') const storedAddresses = uni.getStorageSync('addresses') - console.log('loadAddressList: 本地存储地址数据:', storedAddresses) if (storedAddresses) { try { const addresses = JSON.parse(storedAddresses as string) as any[] if (addresses && addresses.length > 0) { - // 转换地址格式以匹配addressList的结构 addressList.value = addresses.map((addr: any) => ({ id: addr.id, recipient_name: addr.name, @@ -901,25 +837,18 @@ const loadAddressList = async () => { detail: addr.detail, is_default: addr.isDefault })) - console.log('loadAddressList: 从本地存储加载地址成功,数量:', addressList.value.length) } else { addressList.value = [] - console.log('loadAddressList: 本地存储地址为空数组') } } catch (err) { - console.error('解析本地地址数据失败:', err) addressList.value = [] } } else { addressList.value = [] - console.log('loadAddressList: 本地存储没有地址数据') } } - // 如果仍然没有地址,使用模拟地址数据(与loadDefaultAddress保持一致) if (!addressList.value || addressList.value.length === 0) { - console.log('loadAddressList: 使用模拟地址数据') - // 模拟地址数据(与loadDefaultAddress中保持一致) const mockAddresses = [ { id: 'addr_001', @@ -943,11 +872,8 @@ const loadAddressList = async () => { } ] - // 保存模拟地址到本地存储 uni.setStorageSync('addresses', JSON.stringify(mockAddresses)) - console.log('loadAddressList: 模拟地址已保存到本地存储') - // 转换为checkout页面格式 addressList.value = mockAddresses.map((addr: any) => ({ id: addr.id, recipient_name: addr.name, @@ -958,39 +884,9 @@ const loadAddressList = async () => { detail: addr.detail, is_default: addr.isDefault })) - console.log('loadAddressList: 模拟地址已加载到地址列表,数量:', addressList.value.length) } - - console.log('loadAddressList: 最终地址列表:', addressList.value) } catch (error) { - console.error('从Supabase加载地址列表失败:', error) - // 失败时从本地存储加载 - const storedAddresses = uni.getStorageSync('addresses') - if (storedAddresses) { - try { - const addresses = JSON.parse(storedAddresses as string) as any[] - if (addresses && addresses.length > 0) { - // 转换地址格式以匹配addressList的结构 - addressList.value = addresses.map((addr: any) => ({ - id: addr.id, - recipient_name: addr.name, - phone: addr.phone, - province: addr.province, - city: addr.city, - district: addr.district, - detail: addr.detail, - is_default: addr.isDefault - })) - } else { - addressList.value = [] - } - } catch (err) { - console.error('解析本地地址数据失败:', err) - addressList.value = [] - } - } else { - addressList.value = [] - } + console.error('加载地址列表失败:', error) } } @@ -1007,7 +903,6 @@ const handleAddNewAddress = () => { // 保存新地址 const saveNewAddress = async () => { - // 验证表单 if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) { uni.showToast({ title: '请填写完整信息', @@ -1015,10 +910,6 @@ const saveNewAddress = async () => { }) return } - - const userId = getCurrentUserId() - // if (!userId) return // 允许未登录用户保存地址用于演示 - // 触发保存确认弹窗 showSaveConfirm.value = true } @@ -1040,37 +931,29 @@ const handleSaveConfirm = async (save: boolean) => { } if (save) { - // 从本地存储加载现有地址 const storedAddresses = uni.getStorageSync('addresses') let addresses: any[] = [] if (storedAddresses) { try { addresses = JSON.parse(storedAddresses as string) as any[] } catch (e) { - console.error('解析地址数据失败:', e) addresses = [] } } - // 如果是默认地址,取消其他默认地址 if (newAddressData.isDefault) { addresses.forEach(addr => { addr.isDefault = false }) } - // 如果是第一个地址且未设置默认,则自动设为默认 if (addresses.length === 0 && !newAddressData.isDefault) { newAddressData.isDefault = true } - // 添加到地址列表 addresses.unshift(newAddressData) - - // 保存到本地存储 uni.setStorageSync('addresses', JSON.stringify(addresses)) - // 发布地址更新事件,让address-list页面也能获取到 uni.$emit('addressUpdated', addresses.map((addr) => ({ id: addr.id, recipient_name: addr.name, @@ -1083,7 +966,6 @@ const handleSaveConfirm = async (save: boolean) => { }))) } - // 转换为checkout页面格式并添加到当前地址列表 const checkoutFormatAddress = { id: newAddressData.id, recipient_name: newAddressData.name, @@ -1095,24 +977,18 @@ const handleSaveConfirm = async (save: boolean) => { is_default: newAddressData.isDefault } - // 如果是默认地址,取消其他默认地址 if (checkoutFormatAddress.is_default) { addressList.value.forEach(addr => { addr.is_default = false }) } - // 添加到当前地址列表 addressList.value.unshift(checkoutFormatAddress) - // 如果选择了保存,address-list 已通过事件收到更新;未保存则不影响地址簿 - - // 如果保存的是默认地址,或者当前没有选中地址,则选中这个新地址 if (checkoutFormatAddress.is_default || !selectedAddress.value) { selectedAddress.value = checkoutFormatAddress } - // 重置表单 newAddress.value = { recipient_name: '', phone: '', @@ -1123,7 +999,6 @@ const handleSaveConfirm = async (save: boolean) => { is_default: false } smartAddressInput.value = '' - showNewAddressForm.value = false uni.showToast({ @@ -1137,7 +1012,6 @@ const parseSmartAddress = () => { const input = smartAddressInput.value.trim() if (!input) return - // 重置表单 newAddress.value.recipient_name = '' newAddress.value.phone = '' newAddress.value.province = '' @@ -1145,22 +1019,18 @@ const parseSmartAddress = () => { newAddress.value.district = '' newAddress.value.detail = '' - // 尝试匹配手机号码(11位数字) const phoneRegex = /(1[3-9]\d{9})/g const phoneMatches = input.match(phoneRegex) if (phoneMatches && phoneMatches.length > 0) { newAddress.value.phone = phoneMatches[0] } - // 尝试匹配收件人姓名(中文姓名,2-4个汉字) const nameRegex = /([\u4e00-\u9fa5]{2,4})/g const nameMatches = input.match(nameRegex) if (nameMatches && nameMatches.length > 0) { - // 取第一个匹配的中文姓名作为收件人 newAddress.value.recipient_name = nameMatches[0] } - // 提取地址部分(移除姓名和手机号) let addressText = input if (newAddress.value.recipient_name) { addressText = addressText.replace(newAddress.value.recipient_name, '') @@ -1169,14 +1039,10 @@ const parseSmartAddress = () => { addressText = addressText.replace(newAddress.value.phone, '') } - // 清理地址文本(移除多余的空格和标点) addressText = addressText.replace(/[,,;;\s]+/g, ' ').trim() - // 地址解析逻辑 const patterns = [ - // 匹配格式:省市区详细地址 /^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/, - // 匹配格式:省市详细地址 /^(.*?省)?(.*?市)?(.*)$/ ] @@ -1190,7 +1056,6 @@ const parseSmartAddress = () => { if (district) newAddress.value.district = district.trim() if (detail) newAddress.value.detail = detail.trim() - // 如果详细地址为空,但还有剩余内容,则作为详细地址 if (!newAddress.value.detail && district && detail) { newAddress.value.detail = detail.trim() } @@ -1199,9 +1064,7 @@ const parseSmartAddress = () => { } } - // 如果没有匹配到模式,尝试简单分割 if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) { - // 尝试按常见分隔符分割 const parts = addressText.split(/[省市县区]/) if (parts.length >= 2) { newAddress.value.province = parts[0] || '' @@ -1212,7 +1075,6 @@ const parseSmartAddress = () => { } } - // 如果地址部分为空,但还有剩余文本,则作为详细地址 if (!newAddress.value.detail && addressText.trim()) { newAddress.value.detail = addressText.trim() } @@ -1244,12 +1106,6 @@ const getSpecText = (specs: any): string => { return String(specs) } -// 选择地址 -const selectAddress = () => { - showAddressPopup.value = true - loadAddressList() -} - // 选择配送方式 const selectDelivery = (option: DeliveryOptionType) => { selectedDelivery.value = option.id @@ -1264,30 +1120,21 @@ const selectCoupon = () => { } }) - // 监听优惠券选择 uni.$on('couponSelected', (coupon: any) => { selectedCoupon.value = coupon uni.$off('couponSelected') }) } -// 提交订单 +// 提交订单 - 逻辑保持,注意shopGroups已有完整信息 const submitOrder = async () => { - // 校验地址 if (!selectedAddress.value) { - uni.showToast({ - title: '请选择收货地址', - icon: 'none' - }) + uni.showToast({ title: '请选择收货地址', icon: 'none' }) return } - // 校验商品 if (checkoutItems.value.length === 0) { - uni.showToast({ - title: '订单中没有商品', - icon: 'none' - }) + uni.showToast({ title: '订单中没有商品', icon: 'none' }) return } @@ -1297,32 +1144,27 @@ const submitOrder = async () => { const userId = supabaseService.getCurrentUserId() if (!userId) { uni.hideLoading() - uni.showToast({ - title: '请先登录', - icon: 'none' - }) + uni.showToast({ title: '请先登录', icon: 'none' }) return } - // 准备按店铺分组数据 const groups = shopGroups.value.map((group: any): any => { return { merchant_id: group.merchant_id || group.shopId, shopName: group.shopName, items: group.items.map((item: any): any => ({ - id: item.id, // 用于清理购物车 + id: item.id, product_id: item.product_id, sku_id: item.sku_id, quantity: item.quantity, price: item.price, product_name: item.product_name, product_image: item.product_image, - specifications: item.sku_specifications // 保持原始对象,createOrder 会处理序列化 + specifications: item.sku_specifications })) } }) - // 调用 Supabase 服务创建多店铺订单 const result = await supabaseService.createOrdersByShop({ shipping_address: selectedAddress.value, shopGroups: groups, @@ -1333,31 +1175,20 @@ const submitOrder = async () => { uni.hideLoading() if (result.success) { - // 清除结算商品 try { uni.removeStorageSync('checkout_items') uni.removeStorageSync('checkout_type') - } catch(e) { - console.error('清除结算商品失败', e) - } + } catch(e) { console.error(e) } const orderIds = result.orderIds - if (orderIds.length === 1) { - // 单个订单跳转支付 uni.navigateTo({ url: `/pages/mall/consumer/payment?orderId=${orderIds[0]}&amount=${actualAmount.value}` }) } else { - // 多个订单跳转到订单列表 - uni.showToast({ - title: `成功创建${orderIds.length}个订单`, - icon: 'success' - }) + uni.showToast({ title: `成功创建${orderIds.length}个订单`, icon: 'success' }) setTimeout(() => { - uni.redirectTo({ - url: '/pages/mall/consumer/orders' - }) + uni.redirectTo({ url: '/pages/mall/consumer/orders' }) }, 1500) } } else { @@ -1367,47 +1198,18 @@ const submitOrder = async () => { } catch (err: any) { uni.hideLoading() console.error('提交订单错误:', err) - uni.showToast({ - title: err.message || '提交订单失败', - icon: 'none' - }) + uni.showToast({ title: err.message || '提交订单失败', icon: 'none' }) } } - // 生成订单号 const generateOrderNo = (): string => { const date = new Date() - const year = date.getFullYear() - const month = (date.getMonth() + 1).toString().padStart(2, '0') - const day = date.getDate().toString().padStart(2, '0') + // ... const random = Math.random().toString().slice(2, 8) - return `ORD${year}${month}${day}${random}` + return `ORD${Date.now()}${random}` } -// 清空购物车 -const clearShoppingCart = async () => { - const userId = getCurrentUserId() - if (!userId) return - - const productIds = checkoutItems.value.map(item => item.product_id) - - try { - /* const { error } = await supa - .from('shopping_cart') - .delete() - .eq('user_id', userId) - .in('product_id', productIds) - - if (error !== null) { - console.error('清空购物车失败:', error) - } */ - } catch (err) { - console.error('清空购物车异常:', err) - } -} - - // 返回 const goBack = () => { uni.navigateBack() @@ -1422,13 +1224,10 @@ const goBack = () => { background-color: #f5f5f5; } -/* 删除未使用的顶部栏样式 */ +/* 顶部栏 */ .checkout-header { background-color: #ffffff; padding: 15px; - display: flex; - align-items: center; - justify-content: center; border-bottom: 1px solid #e5e5e5; } @@ -1436,6 +1235,7 @@ const goBack = () => { font-size: 18px; font-weight: bold; color: #333333; + text-align: center; } .checkout-content { @@ -1450,747 +1250,126 @@ const goBack = () => { align-items: center; } -.address-info { - flex: 1; -} - -.address-header { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.recipient { - font-size: 16px; - font-weight: bold; - color: #333333; - margin-right: 15px; -} - -.phone { - font-size: 14px; - color: #666666; - margin-right: 10px; -} - -.default-tag { - background-color: #ff4757; - padding: 2px 8px; - border-radius: 10px; -} - -.tag-text { - color: #ffffff; - font-size: 12px; -} - -.address-detail { - font-size: 14px; - color: #333333; - line-height: 1.4; -} - -.no-address { - flex: 1; - display: flex; - justify-content: space-between; - align-items: center; -} - -.no-address-text { - font-size: 16px; - color: #999999; -} - -.no-address-arrow { - color: #999999; - font-size: 18px; -} - -.products-section { - background-color: #ffffff; - margin-bottom: 10px; - padding: 0 15px; -} - -.debug-info { - background-color: #f8f9fa; - padding: 10px 15px; - border-bottom: 1px solid #e5e5e5; - margin-bottom: 10px; -} - -.debug-text { - font-size: 12px; - color: #666; - display: block; - text-align: center; -} - -.shop-group { - background-color: #fff; - margin: 10px 0; - border-radius: 12px; - padding: 10px; -} - -.shop-header { - display: flex; - flex-direction: row; - align-items: center; - padding-bottom: 10px; - border-bottom: 1px solid #f0f0f0; -} - -.shop-icon { - font-size: 18px; - margin-right: 8px; -} - -.shop-name { - font-size: 16px; - font-weight: bold; - color: #333; -} - -.shop-subtotal { - display: flex; - justify-content: flex-end; /* 右对齐 */ - align-items: center; - padding-top: 10px; - margin-top: 5px; - border-top: 1px dashed #f0f0f0; - font-size: 14px; -} - -.subtotal-label { - color: #666; - margin-right: 10px; -} - -.subtotal-value { - color: #333; -} - -.subtotal-text { - color: #333; - margin-right: 5px; -} - -.subtotal-price { - color: #ff4757; - font-weight: bold; - font-size: 16px; -} - -.product-item { - display: flex; - padding: 15px 0; - border-bottom: 1px solid #f5f5f5; -} - -.product-item:last-child { - border-bottom: none; -} - -.product-image { - width: 80px; - height: 80px; - border-radius: 5px; - margin-right: 15px; -} - -.product-info { - flex: 1; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.product-name { - font-size: 14px; - color: #333333; - line-height: 1.4; - margin-bottom: 5px; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; -} - -.product-spec { - font-size: 12px; - color: #999999; - margin-bottom: 10px; -} - -.product-bottom { - display: flex; - justify-content: space-between; - align-items: center; -} - -.product-price { - font-size: 16px; - color: #ff4757; - font-weight: bold; -} - -.product-quantity { - font-size: 14px; - color: #666666; -} - -.delivery-section, -.coupon-section, -.remark-section, -.price-section { - background-color: #ffffff; - margin-bottom: 10px; - padding: 15px; -} - -.section-title { - font-size: 16px; - font-weight: bold; - color: #333333; - margin-bottom: 15px; -} - -.delivery-options { - display: flex; - flex-direction: column; - gap: 10px; -} - -.delivery-option { - display: flex; - align-items: center; - justify-content: space-between; - padding: 15px; - border: 1px solid #e5e5e5; - border-radius: 8px; -} - -.delivery-option.selected { - border-color: #007aff; - background-color: #f0f8ff; -} - -.option-name { - font-size: 14px; - color: #333333; -} - -.option-price { - font-size: 14px; - color: #ff4757; - font-weight: bold; -} - -.option-selected { - color: #007aff; - font-size: 16px; -} - -.coupon-info { - display: flex; - align-items: center; - justify-content: space-between; - padding: 15px; - border: 1px solid #e5e5e5; - border-radius: 8px; -} - -.coupon-selected { - font-size: 14px; - color: #007aff; -} - -.coupon-placeholder { - font-size: 14px; - color: #999999; -} - -.coupon-arrow { - color: #999999; - font-size: 16px; -} - -.remark-input { - width: 100%; - min-height: 40px; - padding: 10px; - border: 1px solid #e5e5e5; - border-radius: 8px; - font-size: 14px; - color: #333333; -} - -.price-detail { - padding: 15px; - background-color: #f8f9fa; - border-radius: 8px; -} - -.price-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; -} - -.price-row.total { - border-top: 1px solid #e5e5e5; - margin-top: 8px; - padding-top: 15px; -} - -.price-label { - font-size: 14px; - color: #666666; -} - -.price-value { - font-size: 14px; - color: #333333; -} - -.price-value.discount { - color: #4caf50; -} - -.price-value.total-price { - font-size: 18px; - color: #ff4757; - font-weight: bold; -} - -.bottom-bar { - background-color: #ffffff; - padding: 15px; - border-top: 1px solid #e5e5e5; - display: flex; - align-items: center; - justify-content: space-between; -} - -.price-summary { - display: flex; - align-items: baseline; -} - -.summary-label { - font-size: 14px; - color: #333333; - margin-right: 5px; -} - -.summary-price { - font-size: 20px; - color: #ff4757; - font-weight: bold; -} - -.submit-btn { - background-color: #007aff; - color: #ffffff; - padding: 0 40px; - height: 45px; - border-radius: 22.5px; - font-size: 16px; - font-weight: bold; - border: none; -} - -/* 地址选择弹窗样式 */ -.address-popup-mask { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: flex-end; - justify-content: center; - z-index: 9998; -} - -.address-popup { - background-color: #ffffff; - width: 100%; - max-height: 70vh; - border-radius: 20px 20px 0 0; - display: flex; - flex-direction: column; -} - -.popup-header { - padding: 20px 15px; - border-bottom: 1px solid #e5e5e5; - display: flex; - align-items: center; - justify-content: space-between; -} - -.popup-title { - font-size: 18px; - font-weight: bold; - color: #333333; -} - -.popup-close { - font-size: 24px; - color: #999999; - padding: 5px; -} - -.address-list-container { - flex: 1; - padding: 15px; - max-height: 50vh; -} - -.popup-address-item { - padding: 15px; - margin-bottom: 10px; - border: 1px solid #e5e5e5; - border-radius: 8px; - position: relative; -} - -.popup-address-item:last-child { - margin-bottom: 0; -} - -.popup-address-header { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.popup-address-name { - font-size: 16px; - font-weight: bold; - color: #333333; - margin-right: 15px; -} - -.popup-address-phone { - font-size: 14px; - color: #666666; - margin-right: 10px; -} - -.popup-default-tag { - background-color: #ff4757; - padding: 2px 8px; - border-radius: 10px; -} - -.popup-tag-text { - color: #ffffff; - font-size: 12px; -} - -.popup-address-detail { - font-size: 14px; - color: #333333; - line-height: 1.4; - display: block; -} - -.popup-selected-indicator { - position: absolute; - top: 15px; - right: 15px; - color: #007aff; - font-size: 16px; -} - -.popup-empty-address { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 20px; -} - -.popup-empty-icon { - font-size: 60px; - margin-bottom: 15px; -} - -.popup-empty-text { - font-size: 16px; - color: #999999; -} - -.popup-add-address-btn { - background-color: #007aff; - margin: 15px; - padding: 15px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; -} - -.popup-btn-icon { - color: #ffffff; - font-size: 20px; - margin-right: 10px; -} - -.popup-btn-text { - color: #ffffff; - font-size: 16px; - font-weight: bold; -} - -/* 新建地址表单弹窗样式 */ -.address-form-mask { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; -} - -.address-form-popup { - background-color: #ffffff; - width: 90%; - max-width: 500px; - max-height: 80vh; - border-radius: 12px; - display: flex; - flex-direction: column; -} - -.form-header { - padding: 20px 15px; - border-bottom: 1px solid #e5e5e5; - display: flex; - align-items: center; - justify-content: space-between; -} - -.form-title { - font-size: 18px; - font-weight: bold; - color: #333333; -} - -.form-close { - font-size: 24px; - color: #999999; - padding: 5px; -} - -.form-content { - flex: 1; - padding: 15px; - max-height: 50vh; -} - -.form-item { - margin-bottom: 20px; -} - -.form-item:last-child { - margin-bottom: 0; -} - -.form-label { - font-size: 14px; - color: #333333; - margin-bottom: 8px; - display: block; -} - -.form-input { - width: 100%; - padding: 12px; - border: 1px solid #e5e5e5; - border-radius: 8px; - font-size: 14px; - color: #333333; - box-sizing: border-box; -} - -.form-input[readonly] { - background-color: #f9f9f9; - color: #666666; -} - -.region-inputs { - display: flex; - gap: 10px; -} - -.region-input { - flex: 1; -} - -.form-textarea { - width: 100%; - min-height: 80px; - padding: 12px; - border: 1px solid #e5e5e5; - border-radius: 8px; - font-size: 14px; - color: #333333; - box-sizing: border-box; -} - -.smart-address-input { - min-height: 60px; -} - -.smart-tip { - font-size: 12px; - color: #999999; - margin-top: 5px; - display: block; -} - -.checkbox-item { - margin-top: 20px; -} - -.checkbox-wrapper { - display: flex; - align-items: center; -} - -.checkbox { - width: 20px; - height: 20px; - border: 1px solid #e5e5e5; - border-radius: 4px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; -} - -.checkbox.checked { - background-color: #007aff; - border-color: #007aff; -} - -.checkbox-check { - color: #ffffff; - font-size: 14px; -} - -.checkbox-label { - font-size: 14px; - color: #333333; -} - -.form-buttons { - display: flex; - padding: 15px; - border-top: 1px solid #e5e5e5; - gap: 10px; -} - -.form-cancel-btn { - flex: 1; - background-color: #f5f5f5; - color: #333333; - padding: 12px; - border-radius: 8px; - font-size: 16px; - border: none; -} - -.form-submit-btn { - flex: 1; - background-color: #007aff; - color: #ffffff; - padding: 12px; - border-radius: 8px; - font-size: 16px; - border: none; -} -/* 确认弹窗样式 */ -.confirm-popup-mask { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10002; -} - -.confirm-popup { - background-color: #ffffff; - width: 80%; - max-width: 320px; - border-radius: 12px; - overflow: hidden; -} - -.confirm-header { - padding: 20px 0 10px; - text-align: center; -} - -.confirm-title { - font-size: 18px; - font-weight: bold; - color: #333333; -} - -.confirm-content { - padding: 0 20px 20px; - text-align: center; -} - -.confirm-message { - font-size: 16px; - color: #666666; -} - -.confirm-buttons { - display: flex; - border-top: 1px solid #e5e5e5; -} - -.confirm-btn { - flex: 1; - height: 50px; - line-height: 50px; - text-align: center; - font-size: 16px; - background-color: #ffffff; - border: none; - border-radius: 0; -} - -.confirm-btn::after { - border: none; -} - -.confirm-btn.cancel { - color: #666666; - border-right: 1px solid #e5e5e5; -} - -.confirm-btn.confirm { - color: #007aff; - font-weight: bold; -} - +.address-info { flex: 1; } +.address-header { display: flex; align-items: center; margin-bottom: 10px; } +.recipient { font-size: 16px; font-weight: bold; color: #333333; margin-right: 15px; } +.phone { font-size: 14px; color: #666666; margin-right: 10px; } +.default-tag { background-color: #ff4757; padding: 2px 8px; border-radius: 10px; } +.tag-text { color: #ffffff; font-size: 12px; } +.address-detail { font-size: 14px; color: #333333; line-height: 1.4; } +.no-address { flex: 1; display: flex; justify-content: space-between; align-items: center; } +.no-address-text { font-size: 16px; color: #999999; } +.no-address-arrow { color: #999999; font-size: 18px; } + +.products-section { background-color: #ffffff; margin-bottom: 10px; padding: 0 15px; } +.debug-info { background-color: #f8f9fa; padding: 10px 15px; border-bottom: 1px solid #e5e5e5; margin-bottom: 10px; } +.debug-text { font-size: 12px; color: #666; display: block; text-align: center; } + +.shop-group { background-color: #fff; margin: 10px 0; border-radius: 12px; padding: 10px; } +.shop-header { display: flex; flex-direction: row; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; } +.shop-icon { font-size: 18px; margin-right: 8px; } +.shop-name { font-size: 16px; font-weight: bold; color: #333; } +.shop-subtotal { display: flex; justify-content: flex-end; align-items: center; padding-top: 10px; margin-top: 5px; border-top: 1px dashed #f0f0f0; font-size: 14px; } +.subtotal-label { color: #666; margin-right: 10px; } +.subtotal-value { color: #333; } +.subtotal-text { color: #333; margin-right: 5px; } +.subtotal-price { color: #ff4757; font-weight: bold; font-size: 16px; } + +.product-item { display: flex; padding: 15px 0; border-bottom: 1px solid #f5f5f5; } +.product-item:last-child { border-bottom: none; } +.product-image { width: 80px; height: 80px; border-radius: 5px; margin-right: 15px; } +.product-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; } +.product-name { font-size: 14px; color: #333333; line-height: 1.4; margin-bottom: 5px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } +.product-spec { font-size: 12px; color: #999999; margin-bottom: 10px; } +.product-bottom { display: flex; justify-content: space-between; align-items: center; } +.product-price { font-size: 16px; color: #ff4757; font-weight: bold; } +.product-quantity { font-size: 14px; color: #666666; } + +.delivery-section, .coupon-section, .remark-section, .price-section { background-color: #ffffff; margin-bottom: 10px; padding: 15px; } +.section-title { font-size: 16px; font-weight: bold; color: #333333; margin-bottom: 15px; } + +.delivery-options { display: flex; flex-direction: column; gap: 10px; } +.delivery-option { display: flex; align-items: center; justify-content: space-between; padding: 15px; border: 1px solid #e5e5e5; border-radius: 8px; } +.delivery-option.selected { border-color: #007aff; background-color: #f0f8ff; } +.option-name { font-size: 14px; color: #333333; } +.option-price { font-size: 14px; color: #ff4757; font-weight: bold; } +.option-selected { color: #007aff; font-size: 16px; } + +.coupon-info { display: flex; align-items: center; justify-content: space-between; padding: 15px; border: 1px solid #e5e5e5; border-radius: 8px; } +.coupon-selected { font-size: 14px; color: #007aff; } +.coupon-placeholder { font-size: 14px; color: #999999; } +.coupon-arrow { color: #999999; font-size: 16px; } + +.remark-input { width: 100%; min-height: 40px; padding: 10px; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 14px; color: #333333; } + +.price-detail { padding: 15px; background-color: #f8f9fa; border-radius: 8px; } +.price-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; } +.price-row.total { border-top: 1px solid #e5e5e5; margin-top: 8px; padding-top: 15px; } +.price-label { font-size: 14px; color: #666666; } +.price-value { font-size: 14px; color: #333333; } +.price-value.discount { color: #4caf50; } +.price-value.total-price { font-size: 18px; color: #ff4757; font-weight: bold; } + +.bottom-bar { background-color: #ffffff; padding: 15px; border-top: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; } +.price-summary { display: flex; align-items: baseline; } +.summary-label { font-size: 14px; color: #333333; margin-right: 5px; } +.summary-price { font-size: 20px; color: #ff4757; font-weight: bold; } +.submit-btn { background-color: #007aff; color: #ffffff; padding: 0 40px; height: 45px; border-radius: 22.5px; font-size: 16px; font-weight: bold; border: none; } + +/* 弹窗样式 */ +.address-popup-mask, .address-form-mask, .confirm-popup-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); z-index: 9998; } +.address-popup-mask { display: flex; align-items: flex-end; justify-content: center; } +.address-form-mask, .confirm-popup-mask { display: flex; align-items: center; justify-content: center; } +.address-popup { background-color: #ffffff; width: 100%; max-height: 70vh; border-radius: 20px 20px 0 0; display: flex; flex-direction: column; } +.address-form-popup { background-color: #ffffff; width: 90%; max-width: 500px; max-height: 80vh; border-radius: 12px; display: flex; flex-direction: column; } +.confirm-popup { background-color: #ffffff; width: 80%; max-width: 320px; border-radius: 12px; overflow: hidden; } + +.popup-header, .form-header { padding: 20px 15px; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; } +.popup-title, .form-title { font-size: 18px; font-weight: bold; color: #333333; } +.popup-close, .form-close { font-size: 24px; color: #999999; padding: 5px; } + +.address-list-container, .form-content { flex: 1; padding: 15px; max-height: 50vh; } +.popup-address-item { padding: 15px; margin-bottom: 10px; border: 1px solid #e5e5e5; border-radius: 8px; position: relative; } +.popup-address-header { display: flex; align-items: center; margin-bottom: 10px; } +.popup-address-name { font-size: 16px; font-weight: bold; color: #333333; margin-right: 15px; } +.popup-address-phone { font-size: 14px; color: #666666; margin-right: 10px; } +.popup-default-tag { background-color: #ff4757; padding: 2px 8px; border-radius: 10px; } +.popup-tag-text { color: #ffffff; font-size: 12px; } +.popup-address-detail { font-size: 14px; color: #333333; line-height: 1.4; display: block; } +.popup-selected-indicator { position: absolute; top: 15px; right: 15px; color: #007aff; font-size: 16px; } + +.popup-empty-address { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; } +.popup-empty-icon { font-size: 60px; margin-bottom: 15px; } +.popup-empty-text { font-size: 16px; color: #999999; } +.popup-add-address-btn { background-color: #007aff; margin: 15px; padding: 15px; border-radius: 8px; display: flex; align-items: center; justify-content: center; } +.popup-btn-icon { color: #ffffff; font-size: 20px; margin-right: 10px; } +.popup-btn-text { color: #ffffff; font-size: 16px; font-weight: bold; } + +.form-item { margin-bottom: 20px; } +.form-label { font-size: 14px; color: #333333; margin-bottom: 8px; display: block; } +.form-input { width: 100%; padding: 12px; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 14px; color: #333333; box-sizing: border-box; } +.form-input[readonly] { background-color: #f9f9f9; color: #666666; } +.region-inputs { display: flex; gap: 10px; } +.region-input { flex: 1; } +.form-textarea { width: 100%; min-height: 80px; padding: 12px; border: 1px solid #e5e5e5; border-radius: 8px; font-size: 14px; color: #333333; box-sizing: border-box; } +.smart-address-input { min-height: 60px; } +.smart-tip { font-size: 12px; color: #999999; margin-top: 5px; display: block; } +.checkbox-item { margin-top: 20px; } +.checkbox-wrapper { display: flex; align-items: center; } +.checkbox { width: 20px; height: 20px; border: 1px solid #e5e5e5; border-radius: 4px; margin-right: 10px; display: flex; align-items: center; justify-content: center; } +.checkbox.checked { background-color: #007aff; border-color: #007aff; } +.checkbox-check { color: #ffffff; font-size: 14px; } +.checkbox-label { font-size: 14px; color: #333333; } +.form-buttons { display: flex; padding: 15px; border-top: 1px solid #e5e5e5; gap: 10px; } +.form-cancel-btn { flex: 1; background-color: #f5f5f5; color: #333333; padding: 12px; border-radius: 8px; font-size: 16px; border: none; } +.form-submit-btn { flex: 1; background-color: #007aff; color: #ffffff; padding: 12px; border-radius: 8px; font-size: 16px; border: none; } + +.confirm-header { padding: 20px 0 10px; text-align: center; } +.confirm-title { font-size: 18px; font-weight: bold; color: #333333; } +.confirm-content { padding: 0 20px 20px; text-align: center; } +.confirm-message { font-size: 16px; color: #666666; } +.confirm-buttons { display: flex; border-top: 1px solid #e5e5e5; } +.confirm-btn { flex: 1; height: 50px; line-height: 50px; text-align: center; font-size: 16px; background-color: #ffffff; border: none; border-radius: 0; } +.confirm-btn.cancel { color: #666666; border-right: 1px solid #e5e5e5; } +.confirm-btn.confirm { color: #007aff; font-weight: bold; } + \ No newline at end of file diff --git a/mall/pages/mall/consumer/doc/CHAT_SHOPPING_SUPABASE_ARCH.md b/mall/pages/mall/consumer/doc/CHAT_SHOPPING_SUPABASE_ARCH.md new file mode 100644 index 00000000..0c1e3279 --- /dev/null +++ b/mall/pages/mall/consumer/doc/CHAT_SHOPPING_SUPABASE_ARCH.md @@ -0,0 +1,155 @@ +# 基于 Supabase 实现消费者-商家聊天购物闭环方案 + +> **结论:** 是的,仅依赖 Supabase (配合前端逻辑) 完全可以实现“聊天购物闭环”的核心业务流程。 Supabase 提供的 Authentication(身份验证)、Database(数据库)、Realtime(实时订阅)、Storage(存储)以及 Edge Functions(边缘函数)覆盖了即时通讯和订单状态流转所需的所有基础设施。 +> +> *注:实际资金的支付(扣款)通常需要对接微信支付/支付宝/Stripe等第三方支付网关,但 Supabase 可以完美托管支付前后的数据流、状态流和 Webhook 处理。* + +--- + +## 1. 核心架构概览 + +在不引入额外后端服务(如 Node.js/Java 服务端)的情况下,架构如下: + +* **客户端 (Uni-app / Vue)**: 消费者端消费者 App,商家端管理后台。直接通过 `supabase-js` SDK 与 Supabase 交互。 +* **身份验证 (Auth)**: 区分消费者(Consumer)和商家(Merchant/Admin)。利用 RLS (Row Level Security) 确保数据隔离。 +* **实时通讯 (Realtime)**: 监听 `messages` 和 `orders` 表的变动,实现毫秒级消息推送和订单状态更新。 +* **业务逻辑 (Database + Edge Functions)**: 使用 Postgres 函数处理复杂的原子操作(如创建订单),使用 Edge Functions 处理支付回调。 + +--- + +## 2. 数据库设计 (Schema) + +为了支持“边聊边买”,我们需要设计能关联聊天与订单的数据结构。 + +### 2.1 核心表结构 + +**1. 聊天室表 (`chat_rooms`)** +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `id` | uuid | 主键 | +| `consumer_id` | uuid | 关联 `auth.users` (消费者) | +| `merchant_id` | uuid | 关联 `auth.users` (商家) | +| `last_message` | jsonb | 最后一条消息快照(用于列表展示) | +| `updated_at` | timestamp | 排序用 | + +**2. 消息表 (`messages`)** +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `id` | uuid | 主键 | +| `room_id` | uuid | 外键关联 `chat_rooms` | +| `sender_id` | uuid | 发送者 ID | +| `type` | text | 消息类型: `text`, `image`, `product`, `order`, `system` | +| `payload` | jsonb | 消息内容。如果是 `product`,存商品快照;如果是 `order`,存订单摘要 | +| `is_read` | boolean | 已读状态 | +| `created_at` | timestamp | 发送时间 | + +**3. 订单表 (`orders`)** +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `id` | uuid | 主键 | +| `room_id` | uuid | **关键关联**:该订单属于哪个聊天上下文 | +| `consumer_id` | uuid | 买家 | +| `merchant_id` | uuid | 卖家 | +| `items` | jsonb | 商品列表 | +| `total_amount` | numeric | 总金额 | +| `status` | text | `pending`, `paid`, `shipped`, `completed`, `cancelled` | +| `payment_status`| text | `unpaid`, `success` | + +--- + +## 3. “聊天购物”闭环流程详解 + +### 场景一:商品咨询与卡片发送 +1. **场景**: 消费者在商品详情页点击“联系商家”。 +2. **动作**: + * 前端检查 `chat_rooms` 是否存在该(消费者, 商家)的记录,没有则插入(`upsert`)。 + * 跳转至聊天页。 + * **特色功能**: 自动发送一条 `type: product` 的消息,包含当前浏览的商品卡片 (`payload: { id, title, price, image }`)。 +3. **实现 (Supabase)**: + * 直接写入 `messages` 表。 + * 商家端通过 `supabase.channel('messages').on(...)` 实时收到商品卡片,知道用户对什么感兴趣。 + +### 场景二:商家发起收款(创建订单) +1. **场景**: 双方沟通确认购买意向后,商家点击“发起订单”或“直接改价”。 +2. **动作**: + * 商家选择商品,填写金额,生成预订单。 + * 向 `orders` 表插入一条状态为 `pending` 的记录。 + * 向 `messages` 表插入一条 `type: order` 的消息,`payload` 包含 `order_id` 和摘要。 +3. **表现**: 消费者在聊天流中看到一个“待支付订单卡片”。 + +### 场景三:聊天中支付 +1. **场景**: 消费者点击聊天气泡中的“立即支付”按钮。 +2. **动作**: + * APP 唤起支付(微信/支付宝)。 + * 支付成功后,支付平台回调 Supabase Edge Function (或通过前端验证)。 + * 更新 `orders` 表 `status` 为 `paid`。 +3. **闭环**: + * Supabase 监听到 `orders` 表 `status` 变为 `paid`。 + * **触发器 (Trigger)** 或客户端逻辑自动插入一条 `type: system` 的消息:“订单已支付,等待发货”。 + * 商家端聊天界面实时更新订单状态为“已支付”。 + +--- + +## 4. 关键技术实现点 + +### 4.1 RLS (行级安全策略) +必须配置严格的 RLS,防止用户偷看他人聊天。 + +```sql +-- 示例:只能查看属于自己的聊天室 +create policy "Users can view their own rooms" +on chat_rooms for select +using (auth.uid() = consumer_id or auth.uid() = merchant_id); +``` + +### 4.2 实时订阅 (Realtime) + +前端代码示例 (Prolog/Vue): + +```typescript +// 订阅消息 +const messageChannel = supabase + .channel('chat-room-123') + .on( + 'postgres_changes', + { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` }, + (payload) => { + messages.value.push(payload.new) + scrollToBottom() + } + ) + .subscribe() + +// 订阅订单状态变更 (实现卡片状态自动刷新) +const orderChannel = supabase + .channel('order-updates') + .on( + 'postgres_changes', + { event: 'UPDATE', schema: 'public', table: 'orders', filter: `room_id=eq.${roomId}` }, + (payload) => { + updateOrderCardStatus(payload.new.id, payload.new.status) + } + ) + .subscribe() +``` + +### 4.3 边缘函数 (Edge Functions) 处理 Webhook +由于前端不能直接处理支付回调(不安全),需要使用 Supabase Edge Functions。 + +* `functions/payment-webhook/index.ts`: 接收微信支付回调,验证签名,然后使用 Service Role Key 更新 `orders` 表。 + +--- + +## 5. 总结 + +完全依赖 Supabase 实现聊天购物闭环是**可行且高效**的方案。 + +* **开发快**: 免去后端 CRUD 接口开发,直接操作 DB。 +* **实时性**: Realtime 功能天然契合聊天场景。 +* **成本低**: 无需维护长连接服务器 (WebSocket)。 +* **闭环体验**: 订单与消息在同一数据流中,用户体验流畅。 + +**下一步建议:** +1. 在 Supabase 中创建 `chat_rooms`, `messages`, `orders` 表。 +2. 配置 RLS。 +3. 开发消息列表 UI,支持多类型消息渲染(特别是商品卡片和订单卡片)。 diff --git a/mall/pages/mall/consumer/index.uvue b/mall/pages/mall/consumer/index.uvue index 93fb139a..be9e199f 100644 --- a/mall/pages/mall/consumer/index.uvue +++ b/mall/pages/mall/consumer/index.uvue @@ -348,8 +348,8 @@ const loadCategories = async () => { desc: cat.description || '', // 使用description字段 color: '#4CAF50' // 默认颜色,表中可能没有color字段 })) - // 随机打乱顺序 - categories.value = mappedCategories.sort(() => Math.random() - 0.5) + // 保持原始顺序或按ID排序,移除随机打乱 + categories.value = mappedCategories } catch (error) { console.error('加载分类数据失败:', error) // 如果加载失败,使用默认分类作为后备 @@ -361,8 +361,8 @@ const loadCategories = async () => { const loadBrands = async () => { try { const brandsData = await supabaseService.getBrands() - // 随机打乱 - brands.value = brandsData.sort(() => Math.random() - 0.5) + // 保持原始顺序 + brands.value = brandsData } catch (e) { console.error('加载品牌失败:', e) brands.value = [] @@ -587,8 +587,8 @@ const switchCategory = (category: any) => { console.log('=== switchCategory函数开始执行 ===') console.log('分类ID:', category.id, '分类名称:', category.name) - // 清除可能存在的旧数据 - uni.removeStorageSync('selectedCategory') + // 使用Storage传递参数,确保switchTab后能被读取 + uni.setStorageSync('selectedCategory', category.id) // 生成唯一的时间戳和随机参数,确保每次跳转都是新的页面 const timestamp = Date.now() diff --git a/mall/pages/mall/consumer/product-detail.uvue b/mall/pages/mall/consumer/product-detail.uvue index 64918dcb..672c13ca 100644 --- a/mall/pages/mall/consumer/product-detail.uvue +++ b/mall/pages/mall/consumer/product-detail.uvue @@ -40,6 +40,19 @@ {{ product.usage }} + + + + + + + {{ coupon.name }} + + + + 领券 > + + 商品参数 @@ -96,6 +109,11 @@ + + + 💬 + 客服 + 🛒 购物车 @@ -174,6 +192,34 @@ + + + + + + 优惠券 + × + + + + + + ¥{{ coupon.discount_value }} + + 满{{ coupon.min_order_amount }}可用 + + + + {{ coupon.name }} + {{ formatDate(coupon.start_time) }}-{{ formatDate(coupon.end_time) }} + + + + + + + + @@ -219,7 +265,10 @@ export default { selectedSpec: '', quantity: 1, isFavorite: false, - showParams: false + showParams: false, + // 新增: 优惠券相关 + coupons: [] as any[], + showCoupons: false } }, onLoad(options: any) { @@ -398,9 +447,6 @@ export default { } } catch(e) {} } - - // Load SKUs - // this.loadProductSkus(productId) // If SKU logic exists } else { throw new Error('No product found') } @@ -416,6 +462,8 @@ export default { // Load Merchant and SKUs if (this.product.merchant_id) { await this.loadMerchantInfo(this.product.merchant_id) + // 加载优惠券 + this.loadCoupons() } if (this.product.id) { this.loadProductSkus(this.product.id) @@ -508,10 +556,74 @@ export default { } catch (e) { console.error('Fetch SKUs error', e) } + }, + + // 新增:加载优惠券 + async loadCoupons() { + if (!this.product.merchant_id) return + // Safety check for cached service definition + // @ts-ignore + if (typeof supabaseService.fetchShopCoupons === 'function') { + this.coupons = await supabaseService.fetchShopCoupons(this.product.merchant_id) + } else if (typeof supabaseService.getAvailableCoupons === 'function') { + this.coupons = await supabaseService.getAvailableCoupons(this.product.merchant_id) + } else { + console.warn('SupabaseService.fetchShopCoupons method missing in runtime. Please restart project.') + } + }, - - // 如果没有从数据库加载到SKU,则不显示规格选择,直接作为无规格商品添加 - // 移除之前的Mock逻辑,因为Mock的ID不符合UUID格式会导致数据库错误 + // 新增:联系客服(商家) + contactMerchant() { + if (!supabaseService.getCurrentUserId()) { + uni.navigateTo({ url: '/pages/auth/login' }) + return + } + // Navigate to chat + const merchId = this.merchant.user_id || this.merchant.id || this.product.merchant_id; + uni.navigateTo({ + url: `/pages/mall/consumer/chat?merchantId=${merchId}&merchantName=${this.merchant.shop_name}` + }) + }, + + // 新增:优惠券弹窗 + showCouponModal() { + this.showCoupons = true + }, + hideCouponModal() { + this.showCoupons = false + }, + + // 新增:领取优惠券 + async claimCoupon(coupon: any) { + const userId = supabaseService.getCurrentUserId() + if (!userId) { + uni.navigateTo({ url: '/pages/auth/login' }) + return + } + uni.showLoading({ title: '领取中' }) + + let success = false + // @ts-ignore + if (typeof supabaseService.claimShopCoupon === 'function') { + success = await supabaseService.claimShopCoupon(coupon.id, userId) + } else if (typeof supabaseService.claimCoupon === 'function') { + success = await supabaseService.claimCoupon(coupon.id, userId) + } else { + console.warn('claimCoupon method missing') + } + + uni.hideLoading() + if (success) { + uni.showToast({ title: '领取成功', icon: 'success' }) + } else { + uni.showToast({ title: '领取失败或已领取', icon: 'none' }) + } + }, + + formatDate(dateStr: string): string { + if (!dateStr) return '' + const date = new Date(dateStr) + return `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}` }, onSwiperChange(e: any) { @@ -549,41 +661,26 @@ export default { return } - // 显示加载中 - uni.showLoading({ - title: '添加中...' - }) + uni.showLoading({ title: '添加中...' }) try { - // 调用 Supabase 服务添加到购物车 - // 传递 productId, quantity, skuId const success = await supabaseService.addToCart( this.product.id, this.quantity, this.selectedSkuId ) - uni.hideLoading() if (success) { - uni.showToast({ - title: '已添加到购物车', - icon: 'success' - }) + uni.showToast({ title: '已添加到购物车', icon: 'success' }) } else { console.error('添加购物车返回失败') - uni.showToast({ - title: '添加失败,请登录重试', - icon: 'none' - }) + uni.showToast({ title: '添加失败,请登录重试', icon: 'none' }) } } catch (e) { uni.hideLoading() console.error('添加购物车异常', e) - uni.showToast({ - title: '添加异常', - icon: 'none' - }) + uni.showToast({ title: '添加异常', icon: 'none' }) } }, @@ -598,32 +695,20 @@ export default { const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null - // 调试:打印价格信息 - console.log('立即购买 - 商品价格信息:') - console.log('SKU价格:', sku ? sku.price : '无SKU') - console.log('商品价格:', this.product.price) - console.log('选择的价格:', (sku ? sku.price : this.product.price)) - console.log('数量:', this.quantity) + const selectedItem = { + id: this.selectedSkuId, + product_id: this.product.id, + sku_id: this.selectedSkuId, + product_name: this.product.name, + product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0], + sku_specifications: sku ? sku.specifications : {}, + price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)), + quantity: Number(this.quantity) + } - const selectedItem = { - id: this.selectedSkuId, - product_id: this.product.id, - sku_id: this.selectedSkuId, - product_name: this.product.name, - product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0], - sku_specifications: sku ? sku.specifications : {}, - price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)), - quantity: Number(this.quantity) - } - - // 调试:打印最终传递的数据 - console.log('立即购买 - 传递的商品数据:', selectedItem) - - // 使用Storage传递数据,避免EventChannel可能的问题 uni.setStorageSync('checkout_type', 'buy_now') uni.setStorageSync('checkout_items', JSON.stringify([selectedItem])) - // 跳转到订单确认页 uni.navigateTo({ url: '/pages/mall/consumer/checkout', success: (res) => { @@ -634,14 +719,7 @@ export default { }) }, - goToShop() { - uni.navigateTo({ - url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}` - }) - }, - checkFavoriteStatus(id: string) { - // console.log('product-detail checkFavoriteStatus id:', id) this.checkFavorite(id) }, @@ -652,65 +730,46 @@ export default { async toggleFavorite() { if (!this.product.id) return - - // 显示loading uni.showLoading({ title: '处理中' }) try { - // 记录操作前的状态 const wasFavorite = this.isFavorite - - // 执行切换,返回的是最新的状态(true=已收藏,false=未收藏) const isNowFavorite = await supabaseService.toggleFavorite(this.product.id) uni.hideLoading() if (isNowFavorite !== wasFavorite) { - // 状态发生了改变,说明操作成功 this.isFavorite = isNowFavorite uni.showToast({ title: isNowFavorite ? '收藏成功' : '已取消收藏', icon: 'success' }) } else { - // 状态未改变,说明操作失败 - uni.showToast({ - title: '操作失败', - icon: 'none' - }) - // 确保状态同步 + uni.showToast({ title: '操作失败', icon: 'none' }) this.checkFavoriteStatus(this.product.id) } } catch (e) { uni.hideLoading() console.error('Toggle favorite failed', e) - uni.showToast({ - title: '操作异常', - icon: 'none' - }) + uni.showToast({ title: '操作异常', icon: 'none' }) } }, goToHome() { - uni.switchTab({ - url: '/pages/mall/consumer/home' - }) + uni.switchTab({ url: '/pages/mall/consumer/home' }) }, goToShop() { if (this.merchant.user_id) { uni.navigateTo({ - url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.user_id}` + url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}` }) } }, goToCart() { - uni.switchTab({ - url: '/pages/mall/consumer/cart' - }) + uni.switchTab({ url: '/pages/mall/consumer/cart' }) }, - // 数量选择相关方法 decreaseQuantity() { if (this.quantity > 1) { this.quantity-- @@ -722,37 +781,23 @@ export default { if (this.quantity < maxQuantity) { this.quantity++ } else { - uni.showToast({ - title: `最多只能购买${maxQuantity}件`, - icon: 'none' - }) + uni.showToast({ title: `最多只能购买${maxQuantity}件`, icon: 'none' }) } }, validateQuantity() { - // 确保数量是数字 let num = parseInt(this.quantity) - if (isNaN(num)) { - num = 1 - } - - // 限制在1和最大库存之间 + if (isNaN(num)) num = 1 const maxQuantity = this.getMaxQuantity() - if (num < 1) { - num = 1 - } else if (num > maxQuantity) { + if (num < 1) num = 1 + else if (num > maxQuantity) { num = maxQuantity - uni.showToast({ - title: `最多只能购买${maxQuantity}件`, - icon: 'none' - }) + uni.showToast({ title: `最多只能购买${maxQuantity}件`, icon: 'none' }) } - this.quantity = num }, getMaxQuantity() { - // 如果有选择SKU,使用SKU的库存,否则使用商品总库存 if (this.selectedSkuId) { const sku = this.productSkus.find(s => s.id === this.selectedSkuId) if (sku) return sku.stock @@ -857,7 +902,7 @@ export default { padding: 30rpx; margin-bottom: 20rpx; display: flex; - flex-direction: row; /* 显式横向排列 */ + flex-direction: row; align-items: center; } @@ -871,7 +916,7 @@ export default { .shop-details { flex: 1; display: flex; - flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */ + flex-direction: column; justify-content: center; } @@ -884,7 +929,7 @@ export default { .shop-stats-row { display: flex; - flex-direction: row; /* 显式横向排列 */ + flex-direction: row; align-items: center; } @@ -899,6 +944,142 @@ export default { color: #666; } +/* Coupon Entry Styles */ +.coupon-entry { + background-color: #fff; + padding: 30rpx; + margin-bottom: 20rpx; + display: flex; + align-items: center; + justify-content: space-between; +} +.coupon-entry-left { + display: flex; + align-items: center; + flex: 1; +} +.coupon-entry-label { + font-size: 30rpx; + color: #333; + width: 120rpx; + font-weight: bold; +} +.coupon-tags-row { + flex: 1; + display: flex; + flex-direction: row; +} +.coupon-tag { + font-size: 20rpx; + color: #ff4444; + border: 1px solid #ff4444; + padding: 2rpx 10rpx; + border-radius: 4rpx; + margin-right: 15rpx; +} +.coupon-arrow { + font-size: 26rpx; + color: #999; +} + +/* Modal Popup Styles */ +.popup-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-end; + z-index: 1000; +} +.popup-content { + background-color: #fff; + width: 100%; + max-height: 80vh; + border-radius: 20rpx 20rpx 0 0; + padding: 30rpx; +} +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; + padding-bottom: 20rpx; + border-bottom: 1rpx solid #eee; +} +.popup-title { + font-size: 32rpx; + font-weight: bold; + color: #333; +} +.close-btn { + font-size: 48rpx; + color: #999; +} +.coupon-list-scroll { + max-height: 60vh; +} +.coupon-item { + display: flex; + background-color: #fff5f5; + border-radius: 10rpx; + padding: 20rpx; + margin-bottom: 20rpx; +} +.coupon-left { + width: 180rpx; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-right: 1px dashed #ffccc7; + color: #ff4444; +} +.coupon-amount { + font-size: 40rpx; + font-weight: bold; +} +.symbol { + font-size: 24rpx; +} +.coupon-cond { + font-size: 22rpx; + margin-top: 5rpx; +} +.coupon-right { + flex: 1; + padding-left: 20rpx; + display: flex; + justify-content: space-between; + align-items: center; +} +.coupon-info-text { + display: flex; + flex-direction: column; +} +.coupon-name { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 10rpx; +} +.coupon-time { + font-size: 22rpx; + color: #999; +} +.coupon-btn { + background-color: #ff4444; + color: #fff; + font-size: 24rpx; + padding: 0 24rpx; + height: 50rpx; + line-height: 50rpx; + border-radius: 25rpx; + margin: 0; +} + .spec-section { background-color: #fff; padding: 30rpx; @@ -1012,21 +1193,21 @@ export default { padding: 10rpx 20rpx; box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1); display: flex; - flex-direction: row; /* 显式设置横向排列 */ + flex-direction: row; align-items: center; justify-content: space-between; } .action-buttons { display: flex; - flex-direction: row; /* 显式设置横向排列 */ + flex-direction: row; align-items: center; margin-right: 20rpx; } .action-btn { display: flex; - flex-direction: column; /* 图标文字保持纵向 */ + flex-direction: column; align-items: center; justify-content: center; margin-right: 20rpx; @@ -1046,7 +1227,7 @@ export default { .btn-group { flex: 1; display: flex; - flex-direction: row; /* 显式设置横向排列 */ + flex-direction: row; align-items: center; } @@ -1105,11 +1286,6 @@ export default { color: #333; } -.close-btn { - font-size: 48rpx; - color: #999; -} - .spec-list { max-height: 60vh; overflow-y: auto; @@ -1306,4 +1482,4 @@ export default { margin-left: 20rpx; } } - + \ No newline at end of file diff --git a/mall/pages/mall/consumer/shop-detail.uvue b/mall/pages/mall/consumer/shop-detail.uvue index cb4d5aa2..6210d1b1 100644 --- a/mall/pages/mall/consumer/shop-detail.uvue +++ b/mall/pages/mall/consumer/shop-detail.uvue @@ -15,6 +15,24 @@ {{ merchant.shop_description || '这家店很懒,什么都没写~' }} + + + + + + + + ¥{{ coupon.discount_value }} + 满{{ coupon.min_order_amount }} + 无门槛 + + + 领取 + + + + + @@ -63,6 +81,7 @@ const merchant = ref({ const products = ref([]) const isFollowed = ref(false) +const coupons = ref([]) // 新增优惠券 onMounted(() => { const pages = getCurrentPages() @@ -72,6 +91,7 @@ onMounted(() => { if (merchantId) { loadShopData(merchantId) loadShopProducts(merchantId) + loadCoupons(merchantId) // 加载优惠券 } }) @@ -95,8 +115,56 @@ const loadShopData = async (id: string) => { } } +const loadCoupons = async (id: string) => { + // 安全检查,防止因编译器可以缓存导致的方法未定义错误 + // @ts-ignore + if (typeof supabaseService.fetchShopCoupons === 'function') { + coupons.value = await supabaseService.fetchShopCoupons(id) + } else if (typeof supabaseService.getAvailableCoupons === 'function') { + // Fallback to old name + coupons.value = await supabaseService.getAvailableCoupons(id) + } else { + console.warn('SupabaseService.fetchShopCoupons method missing. Please rebuild project.') + } +} + +const claimCoupon = async (coupon: any) => { + const userId = supabaseService.getCurrentUserId() + if (!userId) { + uni.navigateTo({ url: '/pages/auth/login' }) + return + } + uni.showLoading({ title: '领取中' }) + + let success = false + // @ts-ignore + if (typeof supabaseService.claimShopCoupon === 'function') { + success = await supabaseService.claimShopCoupon(coupon.id, userId) + } else if (typeof supabaseService.claimCoupon === 'function') { + success = await supabaseService.claimCoupon(coupon.id, userId) + } else { + console.warn('claimCoupon not found') + } + + uni.hideLoading() + if (success) { + uni.showToast({ title: '领取成功', icon: 'success' }) + } else { + uni.showToast({ title: '领取失败', icon: 'none' }) + } +} + const loadShopProducts = async (id: string) => { + // @ts-ignore + if (typeof supabaseService.getProductsByMerchantId !== 'function') { + console.error('getProductsByMerchantId missing') + return + } const res = await supabaseService.getProductsByMerchantId(id) + console.log('shop-detail getProductsByMerchantId res:', JSON.stringify(res)) + if (res.error) { + console.error('shop-detail error:', res.error) + } if (res.data.length > 0) { products.value = res.data.map((item): ProductType => { // 解析图片数组 @@ -123,13 +191,17 @@ const loadShopProducts = async (id: string) => { } } } - if (images.length === 0 && item.image) { - images.push(item.image!) - } if (images.length === 0 && item.main_image_url) { images.push(item.main_image_url!) } + // 安全获取属性的方式,处理字段名称不一样的问题 + const safeItem = item as any + const safePrice = (safeItem['base_price'] || safeItem['price'] || 0) as number + const safeMarketPrice = (safeItem['market_price'] || safeItem['original_price'] || safePrice) as number + const safeStock = (safeItem['total_stock'] || safeItem['available_stock'] || safeItem['stock'] || 0) as number + const safeSales = (safeItem['sale_count'] || safeItem['sales'] || 0) as number + return { id: item.id, merchant_id: item.merchant_id, @@ -137,10 +209,10 @@ const loadShopProducts = async (id: string) => { name: item.name, description: item.description || '', images: images, - price: item.price, - original_price: item.original_price || item.price, - stock: item.stock || 0, - sales: item.sales || 0, + price: safePrice, + original_price: safeMarketPrice, + stock: safeStock, + sales: safeSales, status: 1, created_at: item.created_at || '' } @@ -267,6 +339,61 @@ const goToProduct = (id: string) => { line-height: 1.4; } +/* Coupon Styles */ +.shop-coupons { + margin-top: 15px; + padding: 0 15px; +} +.coupon-scroll { + width: 100%; + white-space: nowrap; +} +.coupon-wrapper { + display: flex; + flex-direction: row; +} +.coupon-card { + display: inline-flex; + background-color: #fff5f5; + border: 1px solid #ffccc7; + border-radius: 4px; + margin-right: 10px; + width: 140px; + height: 60px; + overflow: hidden; +} +.coupon-left { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-right: 1px dashed #ffccc7; + padding: 0 5px; +} +.coupon-amount { + color: #ff4444; + font-weight: bold; + font-size: 18px; +} +.coupon-cond { + color: #999; + font-size: 10px; +} +.coupon-right { + width: 40px; + display: flex; + justify-content: center; + align-items: center; + background-color: #ff4444; + writing-mode: vertical-rl; /* Note: writing-mode may not work in all environments, used flex direction in product detail instead, but let's try or use flex col */ +} +.coupon-btn-label { + color: #fff; + font-size: 12px; + writing-mode: vertical-rl; +} + .product-section { padding: 15px; } diff --git a/mall/utils/supabaseService.uts b/mall/utils/supabaseService.uts index a74a7222..b44a1304 100644 --- a/mall/utils/supabaseService.uts +++ b/mall/utils/supabaseService.uts @@ -48,6 +48,13 @@ export interface Product { attributes?: string // JSON string created_at?: string updated_at?: string + // Alias fields for compatibility + price?: number + original_price?: number + stock?: number + sales?: number + images?: string + cover?: string // View fields brand_name?: string category_name?: string @@ -928,21 +935,51 @@ class SupabaseService { } } + // 获取与特定商家的聊天记录 + async getChatMessages(merchantId: string): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return [] + + const response = await supa + .from('ml_chat_messages') + .select('*') + .or(`and(sender_id.eq.${userId},receiver_id.eq.${merchantId}),and(sender_id.eq.${merchantId},receiver_id.eq.${userId})`) + .order('created_at', { ascending: false }) + .limit(50) + .execute() + + if (response.error) { + console.error('获取聊天记录失败:', response.error) + return [] + } + return response.data as ChatMessage[] + } catch (e) { + console.error('获取聊天记录异常:', e) + return [] + } + } + // 发送聊天消息 - async sendChatMessage(content: string, type: string = 'text'): Promise { + async sendChatMessage(content: string, toId: string | null = null, type: string = 'text'): Promise { try { const userId = this.getCurrentUserId() if (!userId) return false + const payload : any = { + sender_id: userId, + content: content, + msg_type: type, + is_from_user: true, + created_at: new Date().toISOString() + } + if (toId != null) { + payload['receiver_id'] = toId + } + const response = await supa .from('ml_chat_messages') - .insert({ - sender_id: userId, - content: content, - msg_type: type, - is_from_user: true, - created_at: new Date().toISOString() - }) + .insert(payload) .execute() if (response.error) { @@ -2732,6 +2769,94 @@ class SupabaseService { return 0 } } + + // 获取店铺/商品可用优惠券 + async getAvailableCoupons(merchantId: string): Promise { + return this.fetchShopCoupons(merchantId) + } + + // ALIAS for Cache busting: 获取店铺优惠券 + async fetchShopCoupons(merchantId: string): Promise { + try { + // 查询该商家的优惠券 + 平台通用券 (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) + .gt('end_time', new Date().toISOString()) + .order('discount_value', { ascending: false }) + .execute() + + if (response.error) { + console.error('Fetch coupons failed:', response.error) + return [] + } + + return response.data + } catch (e) { + console.error('Fetch coupons error:', e) + return [] + } + } + + // 领取优惠券 + async claimCoupon(templateId: string, userId: string): Promise { + return this.claimShopCoupon(templateId, userId) + } + + // ALIAS for Cache busting + async claimShopCoupon(templateId: string, userId: string): Promise { + try { + // 1. Fetch template details to get merchant_id and validity + const tmplRes = await supa + .from('ml_coupon_templates') + .select('*') + .eq('id', templateId) + .single() + + if (tmplRes.error) { + console.error('Claim Coupon: Template not found', tmplRes.error) + return false + } + + const template = tmplRes.data as any + + // Calculate expire_at + let expireAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + if (template['valid_days'] && (template['valid_days'] as number) > 0) { + expireAt = new Date(Date.now() + (template['valid_days'] as number) * 24 * 60 * 60 * 1000).toISOString() + } else if (template['end_time']) { + expireAt = template['end_time'] as string + } + + // 2. Insert into user coupons with merchant_id + const insertData = { + user_id: userId, + template_id: templateId, + merchant_id: template['merchant_id'], // Important for shop filtering + coupon_code: 'C' + Date.now() + Math.floor(Math.random() * 1000), + status: 1, + expire_at: expireAt, + received_at: new Date().toISOString() + } + + const response = await supa + .from('ml_user_coupons') + .insert(insertData) + .execute() + + if (response.error) { + console.error('Claim Coupon: Insert failed', response.error) + return false + } + return true + } catch(e) { + console.error('Claim coupon error:', e) + return false + } + } } // 导出单例实例 diff --git a/pages.json b/pages.json index b318b18b..93b04048 100644 --- a/pages.json +++ b/pages.json @@ -1,12 +1,5 @@ { "pages": [ - { - "path": "pages/mall/admin/homePage/index", - "style": { - "navigationBarTitleText": "管理后台", - "navigationStyle": "custom" - } - }, { "path": "pages/user/login", "style": { @@ -14,6 +7,13 @@ "navigationStyle": "custom" } }, + { + "path": "pages/mall/admin/homePage/index", + "style": { + "navigationBarTitleText": "管理后台", + "navigationStyle": "custom" + } + }, { "path": "pages/user/boot", "style": { diff --git a/pages/mall/consumer/index.uvue b/pages/mall/consumer/index.uvue index 78b849e3..373ea2b8 100644 --- a/pages/mall/consumer/index.uvue +++ b/pages/mall/consumer/index.uvue @@ -15,12 +15,12 @@ 请输入药品名称、症状或品牌 - + 🔳 - + 📷 @@ -210,7 +210,7 @@ - + + 加入购物车 @@ -692,7 +692,8 @@ const loadMore = async () => { } // 添加到购物车 -const addToCart = async (product: any) => { +const addToCart = async (product: any, e: any | null = null) => { + e?.stopPropagation() uni.showLoading({ title: '添加中...' }) try { // 尝试调用 Supabase 服务添加 @@ -749,6 +750,44 @@ const navigateToPrescription = () => uni.navigateTo({ url: '/pages/medicine/pres const navigateToOTC = () => uni.navigateTo({ url: '/pages/medicine/otc' }) const navigateToHealthTools = () => uni.navigateTo({ url: '/pages/medicine/tools' }) const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders' }) + +// 扫码功能 +const onScan = (e: any | null) => { + e?.stopPropagation() + uni.scanCode({ + success: (res) => { + console.log('扫码结果:' + res.result) + uni.showToast({ + title: '扫码成功', + icon: 'success' + }) + // 这里可以添加基于扫码结果的逻辑,比如跳转到商品详情 + }, + fail: (err) => { + console.error('扫码失败:', err) + } + }) +} + +// 相机功能e: any | null) => { + e?.stopPropagation() +const onCamera = () => { + uni.chooseImage({ + count: 1, + sourceType: ['camera'], + success: (res) => { + console.log('拍照结果:', res.tempFilePaths) + uni.showToast({ + title: '拍摄成功', + icon: 'success' + }) + // 这里可以添加基于图片的逻辑,比如图搜 + }, + fail: (err) => { + console.error('拍照失败:', err) + } + }) +}