From 06b73694944abcedc3f870c352714cd01c49d361 Mon Sep 17 00:00:00 2001
From: cyh666666 <2398882793@qq.com>
Date: Thu, 5 Feb 2026 17:27:22 +0800
Subject: [PATCH] =?UTF-8?q?consumer=E6=A8=A1=E5=9D=97=E5=AE=8C=E6=88=9090%?=
=?UTF-8?q?=EF=BC=8C=E5=AE=8C=E5=96=84=E5=BA=97=E9=93=BA=E5=95=86=E5=93=81?=
=?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8=E9=A2=86=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ak/config.uts | 28 +-
ak/configbackup.uts | 38 -
ak/configme.uts | 29 -
check_db_coupons.py | 57 +
check_db_schema.py | 34 +
check_products_schema.py | 39 +
.../sql/add_coupons_for_existing_shops.sql | 135 ++
main.uts | 2 +
mall/pages/mall/consumer/category.uvue | 43 +-
mall/pages/mall/consumer/chat.uvue | 176 +--
mall/pages/mall/consumer/chat_new.uvue | 618 +++++++++
mall/pages/mall/consumer/checkout.uvue | 1131 +++--------------
.../doc/CHAT_SHOPPING_SUPABASE_ARCH.md | 155 +++
mall/pages/mall/consumer/index.uvue | 12 +-
mall/pages/mall/consumer/product-detail.uvue | 398 ++++--
mall/pages/mall/consumer/shop-detail.uvue | 141 +-
mall/utils/supabaseService.uts | 141 +-
pages.json | 14 +-
pages/mall/consumer/index.uvue | 47 +-
pages/mall/consumer/shop-detail.uvue | 11 +-
utils/supabaseService.uts | 107 ++
verify_coupons.js | 26 +
22 files changed, 2096 insertions(+), 1286 deletions(-)
delete mode 100644 ak/configbackup.uts
delete mode 100644 ak/configme.uts
create mode 100644 check_db_coupons.py
create mode 100644 check_db_schema.py
create mode 100644 check_products_schema.py
create mode 100644 doc_mall/consumer/sql/add_coupons_for_existing_shops.sql
create mode 100644 mall/pages/mall/consumer/chat_new.uvue
create mode 100644 mall/pages/mall/consumer/doc/CHAT_SHOPPING_SUPABASE_ARCH.md
create mode 100644 verify_coupons.js
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 @@
‹
{{ 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)
+ }
+ })
+}