From 1cca27096a4b78611871f803077c33cc5b4b4d3a Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Wed, 28 Jan 2026 10:29:09 +0800
Subject: [PATCH 01/18] =?UTF-8?q?=E4=B8=BB=E5=88=86=E6=94=AF=E5=90=88?=
=?UTF-8?q?=E5=B9=B6=E5=8F=8A=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package-lock.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/package-lock.json b/package-lock.json
index 4a86479c..507bf6c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
+ "name": "mall",
"dependencies": {
"echarts": "^6.0.0"
},
From af316e6d94c477505554b040f022f3e40457146c Mon Sep 17 00:00:00 2001
From: cyh666666 <2398882793@qq.com>
Date: Wed, 28 Jan 2026 10:46:54 +0800
Subject: [PATCH 02/18] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=98=A8=E6=99=9A?=
=?UTF-8?q?=E8=87=B3=E4=BB=8A=E6=97=A9=E7=9A=84=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages.json | 18 +
pages/info/settings.uvue | 90 ++
pages/mall/consumer/cart copy.uvue | 1220 ++++++++++++++++
pages/mall/consumer/cart.uvue | 302 ++--
pages/mall/consumer/checkout copy.uvue | 1730 ++++++++++++++++++++++
pages/mall/consumer/checkout.uvue | 73 +-
pages/mall/consumer/checkoutgood.uvue | 1733 +++++++++++++++++++++++
pages/mall/consumer/product-detail.uvue | 43 +-
pages/mall/consumer/settings.uvue | 157 +-
pages/mall/consumer/wallet.uvue | 20 +-
pages/user/bind-email.uvue | 544 +++++++
pages/user/bind-phone.uvue | 548 +++++++
pages/user/change-password.uvue | 353 +++++
13 files changed, 6635 insertions(+), 196 deletions(-)
create mode 100644 pages/mall/consumer/cart copy.uvue
create mode 100644 pages/mall/consumer/checkout copy.uvue
create mode 100644 pages/mall/consumer/checkoutgood.uvue
create mode 100644 pages/user/bind-email.uvue
create mode 100644 pages/user/bind-phone.uvue
create mode 100644 pages/user/change-password.uvue
diff --git a/pages.json b/pages.json
index a6b8041f..c07e0e02 100644
--- a/pages.json
+++ b/pages.json
@@ -169,6 +169,24 @@
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
+ },
+ {
+ "path": "pages/user/change-password",
+ "style": {
+ "navigationBarTitleText": "修改密码"
+ }
+ },
+ {
+ "path": "pages/user/bind-phone",
+ "style": {
+ "navigationBarTitleText": "绑定手机"
+ }
+ },
+ {
+ "path": "pages/user/bind-email",
+ "style": {
+ "navigationBarTitleText": "绑定邮箱"
+ }
}
],
"tabBar": {
diff --git a/pages/info/settings.uvue b/pages/info/settings.uvue
index 6a96b7f5..465042e1 100644
--- a/pages/info/settings.uvue
+++ b/pages/info/settings.uvue
@@ -1033,4 +1033,94 @@ import i18n from '@/uni_modules/i18n/index.uts' // 保留用于语言切换
.action-btn.primary .action-text {
color: #ffffff;
}
+
+/* 响应式布局优化 */
+@media screen and (min-width: 480px) and (max-width: 767px) {
+ .settings-section {
+ margin: 12px 20px;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ }
+
+ .section-header {
+ width: 100%;
+ }
+
+ .setting-item {
+ flex: 1 0 calc(50% - 20px);
+ margin: 10px;
+ border: 1px solid #f0f0f0;
+ border-radius: 8px;
+ padding: 16px;
+ box-sizing: border-box;
+ border-bottom: none;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .item-content {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ .item-label, .item-desc, .item-value {
+ display: block;
+ width: 100%;
+ }
+
+ .toggle-switch, .item-arrow {
+ align-self: flex-end;
+ margin-top: auto;
+ }
+}
+
+@media screen and (min-width: 768px) {
+ .settings-page {
+ padding: 20px;
+ background-color: #f5f5f5;
+ }
+
+ .settings-section {
+ margin: 0 auto 20px;
+ max-width: 800px;
+ border-radius: 12px;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ }
+
+ .section-header {
+ width: 100%;
+ }
+
+ .setting-item {
+ flex: 1 0 calc(33.333% - 20px);
+ margin: 10px;
+ border: 1px solid #f0f0f0;
+ border-radius: 8px;
+ padding: 16px;
+ box-sizing: border-box;
+ border-bottom: none;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .item-content {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ .item-label, .item-desc, .item-value {
+ display: block;
+ width: 100%;
+ }
+
+ .toggle-switch, .item-arrow {
+ align-self: flex-end;
+ margin-top: auto;
+ }
+}
diff --git a/pages/mall/consumer/cart copy.uvue b/pages/mall/consumer/cart copy.uvue
new file mode 100644
index 00000000..7d6e20f8
--- /dev/null
+++ b/pages/mall/consumer/cart copy.uvue
@@ -0,0 +1,1220 @@
+
+
+
+
+
+
+ 购物车
+
+
+ {{ isManageMode ? '✓' : '⚙️' }}
+ {{ isManageMode ? '完成' : '管理' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 🛒
+ 购物车是空的
+ 快去挑选喜欢的商品吧
+
+
+
+
+
+
+
+
+
+
+
+
+ ✓
+
+
+
+
+
+
+
+ {{ item.name }}
+ {{ item.spec }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ✓
+
+ 全选
+
+
+
+
+
+ 合计:
+ ¥{{ totalPrice }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ product.name }}
+
+ ¥{{ product.price }}
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/cart.uvue b/pages/mall/consumer/cart.uvue
index 69346032..5809cc95 100644
--- a/pages/mall/consumer/cart.uvue
+++ b/pages/mall/consumer/cart.uvue
@@ -82,6 +82,32 @@
+
+
+
+
+
+ ✓
+
+ 全选
+
+
+
+
+
+ 合计:
+ ¥{{ totalPrice }}
+
+
+
+
+
+
+
-
-
-
+ -->
@@ -415,15 +439,18 @@ const addToCart = (product: any) => {
}
}
- // 检查商品是否已存在
- const existingItem = currentItems.find((item: any) => item.id === product.id)
+ // 检查商品是否已存在 (使用商品ID匹配,因为推荐商品没有SKU)
+ const existingItem = currentItems.find((item: any) =>
+ item.productId === product.id || item.id === product.id
+ )
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
currentItems.push({
- id: product.id,
+ id: product.id, // 商品ID(因为没有SKU)
+ productId: product.id, // 同样存储商品ID
shopId: product.shopId || 'shop_recommend',
shopName: product.shopName || '推荐好物',
name: product.name,
@@ -453,7 +480,11 @@ const goShopping = () => {
}
const navigateToProduct = (product: any) => {
- uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
+ // 使用productId(如果存在)作为跳转的商品ID,否则使用id
+ const productId = product.productId || product.id
+ uni.navigateTo({
+ url: `/pages/mall/consumer/product-detail?id=${productId}&name=${encodeURIComponent(product.name)}&price=${product.price}&image=${encodeURIComponent(product.image)}`
+ })
}
const goToCheckout = () => {
@@ -465,7 +496,10 @@ const goToCheckout = () => {
return
}
- // 获取选中的商品
+ // 确保最新状态已保存到本地存储
+ saveCartData()
+
+ // 获取选中的商品 (直接过滤cartItems,不依赖cartGroups)
const selectedItems = cartItems.value
.filter(item => item.selected)
.map(item => ({
@@ -475,10 +509,14 @@ const goToCheckout = () => {
product_name: item.name,
product_image: item.image,
sku_specifications: item.spec,
- price: item.price,
- quantity: item.quantity
+ price: Number(item.price), // 确保是数字
+ quantity: Number(item.quantity) // 确保是数字
}))
+ // 关键修复:将结算数据写入 Storage,确保 checkout 页面能稳定获取
+ uni.setStorageSync('checkout_type', 'cart')
+ uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
+
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
@@ -1059,83 +1097,171 @@ const goToCheckout = () => {
}
}
-/* 底部结算栏 */
-.cart-footer {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- height: 60px;
- background-color: white;
- border-top: 1px solid #eee;
- display: flex;
- align-items: center;
- justify-content: center; /* 居中内容 */
- padding: 0 15px;
- z-index: 900;
- padding-bottom: env(safe-area-inset-bottom);
-}
+ /* 购物车操作栏样式 - 自适应横向排列 */
+ .cart-action-bar {
+ background-color: white;
+ margin: 10px;
+ border-radius: 12px;
+ padding: 15px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+ }
-.footer-content {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
+ .action-bar-content {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ }
-.footer-left {
- display: flex;
- align-items: center;
-}
+ .action-left, .action-right {
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+ }
-.select-all {
- display: flex;
- align-items: center;
-}
-
-.select-all-text {
- margin-left: 8px;
- font-size: 14px;
- color: #333;
-}
-
-.footer-right {
- display: flex;
- align-items: center;
-}
-
-.total-info {
- margin-right: 15px;
- text-align: right;
-}
-
-.total-text {
- font-size: 14px;
- color: #333;
- margin-right: 5px;
-}
-
-.total-price {
- font-size: 18px;
- color: #ff5000;
- font-weight: bold;
-}
-
-.checkout-btn {
- background-color: #ff5000;
- color: white;
- border: none;
- border-radius: 25px;
- padding: 8px 20px;
- font-size: 14px;
-}
-
-.delete-btn {
- background-color: #ff3b30; /* 红色删除按钮 */
- color: white;
- border: none;
- border-radius: 25px;
- padding: 8px 25px;
- font-size: 14px;
-}
+ .action-right {
+ justify-content: flex-end;
+ flex: 1;
+ min-width: 0; /* 防止溢出 */
+ }
+
+ /* 合计信息区域 - 自适应横向排列 */
+ .total-info {
+ display: flex;
+ align-items: center;
+ margin-right: 12px;
+ flex-shrink: 0;
+ }
+
+ .total-text {
+ font-size: 14px;
+ color: #333;
+ margin-right: 5px;
+ white-space: nowrap;
+ }
+
+ .total-price {
+ font-size: 18px;
+ color: #ff5000;
+ font-weight: bold;
+ white-space: nowrap;
+ }
+
+ /* 结算按钮 */
+ .checkout-btn, .delete-btn {
+ background-color: #ff5000;
+ color: white;
+ border: none;
+ border-radius: 25px;
+ padding: 8px 20px;
+ font-size: 14px;
+ white-space: nowrap;
+ flex-shrink: 0;
+ }
+
+ .delete-btn {
+ background-color: #ff3b30; /* 红色删除按钮 */
+ padding: 8px 25px;
+ }
+
+ /* 全选区域 */
+ .select-all {
+ display: flex;
+ align-items: center;
+ }
+
+ .select-all-text {
+ margin-left: 8px;
+ font-size: 14px;
+ color: #333;
+ white-space: nowrap;
+ }
+
+ /* 响应式调整 */
+ /* 手机端小屏幕优化 */
+ @media screen and (max-width: 375px) {
+ .action-bar-content {
+ gap: 8px;
+ }
+
+ .total-text {
+ font-size: 13px;
+ }
+
+ .total-price {
+ font-size: 16px;
+ }
+
+ .checkout-btn, .delete-btn {
+ padding: 8px 15px;
+ font-size: 13px;
+ }
+
+ .select-all-text {
+ font-size: 13px;
+ }
+ }
+
+ /* 平板端优化 */
+ @media screen and (min-width: 768px) {
+ .cart-action-bar {
+ margin: 20px auto;
+ max-width: 95%;
+ padding: 20px;
+ }
+
+ .action-bar-content {
+ gap: 20px;
+ }
+
+ .total-price {
+ font-size: 20px;
+ }
+
+ .checkout-btn, .delete-btn {
+ padding: 10px 30px;
+ font-size: 16px;
+ }
+ }
+
+ /* 桌面端优化 */
+ @media screen and (min-width: 1024px) {
+ .cart-action-bar {
+ max-width: 1200px;
+ padding: 20px 30px;
+ }
+
+ .action-bar-content {
+ justify-content: space-between;
+ }
+
+ .total-info {
+ margin-right: 30px;
+ }
+
+ .total-text {
+ font-size: 16px;
+ }
+
+ .total-price {
+ font-size: 22px;
+ }
+
+ .checkout-btn, .delete-btn {
+ padding: 12px 40px;
+ font-size: 16px;
+ }
+
+ .select-all-text {
+ font-size: 16px;
+ }
+ }
+
+ /* 大屏幕优化 */
+ @media screen and (min-width: 1400px) {
+ .cart-action-bar {
+ max-width: 1400px;
+ }
+ }
diff --git a/pages/mall/consumer/checkout copy.uvue b/pages/mall/consumer/checkout copy.uvue
new file mode 100644
index 00000000..7318fbf1
--- /dev/null
+++ b/pages/mall/consumer/checkout copy.uvue
@@ -0,0 +1,1730 @@
+
+
+
+
+
+
+
+
+ {{ getFullAddress(selectedAddress) }}
+
+
+ 请选择收货地址
+ ›
+
+
+
+
+
+
+
+ 调试:共{{ checkoutItems.length }}件商品,总价计算:{{ totalAmount }}
+
+
+
+
+
+ {{ item.product_name }}
+ {{ getSpecText(item.sku_specifications) }}
+
+ ¥{{ item.price }}
+ ×{{ item.quantity }}
+
+
+
+
+
+ 暂无商品信息
+
+
+
+
+
+ 配送方式
+
+
+ {{ option.name }}
+ ¥{{ option.price }}
+ ✓
+
+
+
+
+
+
+ 优惠券
+
+ {{ selectedCoupon.template?.name || '优惠券' }}
+ 选择优惠券
+ ›
+
+
+
+
+
+
+
+
+ 价格明细
+
+
+ 商品总价
+ ¥{{ totalAmount.toFixed(2) }}
+
+
+ 运费
+ +¥{{ deliveryFee.toFixed(2) }}
+
+
+ 优惠减免
+ -¥{{ discountAmount.toFixed(2) }}
+
+
+ 应付金额
+ ¥{{ actualAmount.toFixed(2) }}
+
+
+
+
+
+
+
+
+ 合计:
+ ¥{{ actualAmount.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/checkout.uvue b/pages/mall/consumer/checkout.uvue
index 57f3d413..3c744516 100644
--- a/pages/mall/consumer/checkout.uvue
+++ b/pages/mall/consumer/checkout.uvue
@@ -394,45 +394,49 @@ onLoad(() => {
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
+
+ // 默认先尝试从本地存储加载(确保有数据)
+ loadFromLocalStorage()
+
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
let items = data.selectedItems || []
- processCheckoutItems(items)
+ if (items.length > 0) {
+ processCheckoutItems(items)
+ }
loadDefaultAddress()
})
- } else {
- // 如果没有eventChannel,尝试从本地存储加载(例如从购物车进入)
- loadFromLocalStorage()
}
})
// 处理商品数据清洗
const processCheckoutItems = (items: any[]) => {
- // 数据清洗:确保价格和数量是数字类型
+ // 数据清洗:确保价格和数量是数字类型
if (items && items.length > 0) {
items = items.map((item: any) => {
// 确保价格是数字
- let price = item.price
- if (price !== undefined && price !== null) {
- price = typeof price === 'string' ? parseFloat(price) : Number(item.price)
+ let price = 0
+ if (item.price !== undefined && item.price !== null) {
+ price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
- } else {
- price = 0
}
// 确保数量是数字
- let quantity = item.quantity
- if (quantity !== undefined && quantity !== null) {
- quantity = typeof quantity === 'string' ? parseInt(quantity) : Number(item.quantity)
+ let quantity = 1
+ if (item.quantity !== undefined && item.quantity !== null) {
+ quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
- } else {
- quantity = 1
}
return {
- ...item,
- price: Number(price.toFixed(2)), // 保留两位小数
+ id: item.id,
+ product_id: item.product_id || item.id,
+ sku_id: item.sku_id || item.id,
+ product_name: item.product_name || item.name || '',
+ product_image: item.product_image || item.image || '',
+ sku_specifications: item.sku_specifications || item.spec || {},
+ price: Number(price.toFixed(2)),
quantity: quantity
}
})
@@ -466,6 +470,9 @@ onMounted(() => {
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
+ // 离开页面时清除结算数据,防止下次进入时显示旧数据
+ uni.removeStorageSync('checkout_type')
+ uni.removeStorageSync('checkout_items')
})
// 从本地存储加载结算数据(例如从购物车进入)
@@ -479,17 +486,27 @@ const loadFromLocalStorage = () => {
const selectedCartItems = cartItems.filter(item => item.selected === true)
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
- const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => ({
- id: item.id,
- product_id: item.productId,
- sku_id: item.id, // 购物车中item.id就是SKU ID
- product_name: item.name,
- product_image: item.image,
- sku_specifications: item.spec ? { spec: item.spec } : {},
- price: item.price,
- quantity: item.quantity
- }))
- checkoutItems.value = convertedItems
+ const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => {
+ // 确保价格和数量是数字
+ let price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
+ if (isNaN(price)) price = 0
+
+ let quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
+ if (isNaN(quantity) || quantity < 1) quantity = 1
+
+ return {
+ id: item.id,
+ product_id: item.product_id || item.productId || item.id,
+ sku_id: item.sku_id || item.id,
+ product_name: item.name || '',
+ product_image: item.image || '',
+ sku_specifications: item.spec ? { spec: item.spec } : {},
+ price: price,
+ quantity: quantity
+ }
+ })
+ // 再次经过process处理确保类型正确
+ processCheckoutItems(convertedItems)
}
} catch (e) {
console.error('解析购物车数据失败:', e)
diff --git a/pages/mall/consumer/checkoutgood.uvue b/pages/mall/consumer/checkoutgood.uvue
new file mode 100644
index 00000000..3c744516
--- /dev/null
+++ b/pages/mall/consumer/checkoutgood.uvue
@@ -0,0 +1,1733 @@
+
+
+
+
+
+
+
+
+ {{ getFullAddress(selectedAddress) }}
+
+
+ 请选择收货地址
+ ›
+
+
+
+
+
+
+
+ 调试:共{{ checkoutItems.length }}件商品,总价计算:{{ totalAmount }}
+
+
+
+
+
+ {{ item.product_name }}
+ {{ getSpecText(item.sku_specifications) }}
+
+ ¥{{ item.price }}
+ ×{{ item.quantity }}
+
+
+
+
+
+ 暂无商品信息
+
+
+
+
+
+ 配送方式
+
+
+ {{ option.name }}
+ ¥{{ option.price }}
+ ✓
+
+
+
+
+
+
+ 优惠券
+
+ {{ selectedCoupon.template?.name || '优惠券' }}
+ 选择优惠券
+ ›
+
+
+
+
+
+
+
+
+ 价格明细
+
+
+ 商品总价
+ ¥{{ totalAmount.toFixed(2) }}
+
+
+ 运费
+ +¥{{ deliveryFee.toFixed(2) }}
+
+
+ 优惠减免
+ -¥{{ discountAmount.toFixed(2) }}
+
+
+ 应付金额
+ ¥{{ actualAmount.toFixed(2) }}
+
+
+
+
+
+
+
+
+ 合计:
+ ¥{{ actualAmount.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/product-detail.uvue b/pages/mall/consumer/product-detail.uvue
index fc8a8495..ef3baf52 100644
--- a/pages/mall/consumer/product-detail.uvue
+++ b/pages/mall/consumer/product-detail.uvue
@@ -234,21 +234,27 @@ export default {
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
- // 根据商品ID生成不同的商品名称,使其更真实
- const productNames = [
- '高品质运动休闲鞋',
- '时尚简约双肩背包',
- '多功能智能手环',
- '便携式蓝牙音箱',
- '全自动雨伞',
- '抗菌防螨床上四件套',
- '不锈钢保温杯',
- '无线充电器',
- '高清行车记录仪',
- '智能体脂秤'
- ]
- const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
- const productName = options.name ? options.name : productNames[nameIndex]
+ // 优先使用传入的商品名称,否则根据商品ID生成名称
+ const productName = options.name ? decodeURIComponent(options.name) : (() => {
+ // 如果options.name未传入,使用默认的商品名称生成逻辑
+ const productNames = [
+ '高品质运动休闲鞋',
+ '时尚简约双肩背包',
+ '多功能智能手环',
+ '便携式蓝牙音箱',
+ '全自动雨伞',
+ '抗菌防螨床上四件套',
+ '不锈钢保温杯',
+ '无线充电器',
+ '高清行车记录仪',
+ '智能体脂秤'
+ ]
+ const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
+ return productNames[nameIndex]
+ })()
+
+ // 优先使用传入的图片,否则使用默认图片
+ const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
@@ -261,7 +267,7 @@ export default {
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
- '/static/product1.jpg',
+ productImage,
'/static/product2.jpg',
'/static/product3.jpg'
],
@@ -307,9 +313,10 @@ export default {
// 模拟加载商品SKU数据
const basePrice = this.product.price
+ // 使用 productId 作为前缀生成唯一的 SKU ID,防止不同商品的 SKU ID 冲突
this.productSkus = [
{
- id: 'sku_001',
+ id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
@@ -319,7 +326,7 @@ export default {
status: 1
},
{
- id: 'sku_002',
+ id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
diff --git a/pages/mall/consumer/settings.uvue b/pages/mall/consumer/settings.uvue
index d62bb723..5c113dc3 100644
--- a/pages/mall/consumer/settings.uvue
+++ b/pages/mall/consumer/settings.uvue
@@ -2,10 +2,10 @@
-
-
+
🔒
修改密码
@@ -283,7 +283,7 @@ const calculateCacheSize = () => {
// 跳转到个人资料
const goToProfile = () => {
uni.navigateTo({
- url: '/pages/mall/consumer/profile'
+ url: '/pages/user/profile'
})
}
@@ -573,47 +573,126 @@ const goBack = () => {
\ No newline at end of file
+
diff --git a/pages/mall/consumer/wallet.uvue b/pages/mall/consumer/wallet.uvue
index 715e894f..8b638472 100644
--- a/pages/mall/consumer/wallet.uvue
+++ b/pages/mall/consumer/wallet.uvue
@@ -2,11 +2,9 @@
-
@@ -214,7 +218,7 @@ const driverInfo = ref({
})
const workStatus = ref(1) // 1 工作中 0 休息中
-const currentLocation = ref('朝阳区建国门附近')
+const currentLocation = ref('朝阳区建国门附近') // 默认位置
const taskCounts = ref({ total: 0, pending: 0, ongoing: 0, completed: 0 })
@@ -364,6 +368,41 @@ function toggleWorkStatus() {
})
}
+// --- 新增:重新定位逻辑 ---
+function showRelocateConfirm() {
+ uni.showModal({
+ title: '重新定位',
+ content: '确定要更新当前位置吗?',
+ confirmText: '立即定位',
+ success: (res) => {
+ if (res.confirm) {
+ relocate()
+ }
+ }
+ })
+}
+
+function relocate() {
+ uni.showLoading({
+ title: '获取位置中...',
+ mask: true
+ })
+
+ // 模拟定位耗时
+ setTimeout(() => {
+ uni.hideLoading()
+
+ // 模拟定位成功,更新位置(实际项目中应调用API获取真实位置)
+ currentLocation.value = '朝阳区建国门外大街附近'
+
+ uni.showToast({
+ title: '定位成功',
+ icon: 'success',
+ duration: 1500
+ })
+ }, 1200)
+}
+
function contactCustomer() {
uni.showActionSheet({
itemList: ['拨打电话', '发送短信'],
@@ -398,10 +437,10 @@ function goToRatings() {
uni.navigateTo({ url: '/pages/mall/delivery/ratings' })
}
function goToHelp() {
- uni.navigateTo({ url: '/pages/mall/common/help' })
+ uni.navigateTo({ url: '/pages/mall/delivery/help-center' })
}
function goToFeedback() {
- uni.navigateTo({ url: '/pages/mall/common/feedback' })
+ uni.navigateTo({ url: '/pages/mall/delivery/feedback' })
}
@@ -410,7 +449,7 @@ function goToFeedback() {
.profile-header {
position: relative;
}
-.back-box {
+.nav-left {
position: absolute;
left: 30rpx;
top: 50%;
@@ -423,15 +462,15 @@ function goToFeedback() {
align-items: center;
justify-content: center;
}
-.back-box:active {
+.nav-left:active {
background: rgba(0, 0, 0, .3);
}
-.back-icon {
+.nav-icon {
font-size: 40rpx;
color: #fff;
}
-/* ---------- 以下与原样式一致 ---------- */
+/* ---------- 以下与原样式一致,仅修改工作状态区域 ---------- */
.delivery-profile {
padding: 0 0 120rpx 0;
background-color: #f5f5f5;
@@ -541,14 +580,39 @@ function goToFeedback() {
.toggle-switch.active .toggle-handle {
left: 55rpx;
}
-.current-location {
+
+/* --- 修改后的定位信息区域:地址 + 图标 --- */
+.current-location-section {
+ display: flex;
+ align-items: center;
padding: 15rpx 20rpx;
- background: #e8f4fd;
+ background: #e8f4fd; /* 与原样式保持一致 */
border-radius: 15rpx;
}
.location-text {
+ font-size: 24rpx; /* 与截图字号一致 */
+ color: #74b9ff; /* 与截图颜色一致 */
+ flex: 1; /* 让文字占据剩余空间 */
+ margin-right: 16rpx; /* 与图标留空隙 */
+}
+.relocate-icon {
+ width: 40rpx;
+ height: 40rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: rgba(116, 185, 255, 0.15); /* 浅蓝底 */
+ color: #74b9ff; /* 主色文字 */
font-size: 24rpx;
- color: #74b9ff;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+.relocate-icon:hover {
+ background: rgba(116, 185, 255, 0.3); /* 悬停效果 */
+}
+.relocate-icon:active {
+ background: rgba(116, 185, 255, 0.4); /* 点击效果 */
}
.task-tabs {
@@ -556,10 +620,10 @@ function goToFeedback() {
justify-content: space-between;
}
.task-tab {
+ flex: 1;
display: flex;
flex-direction: column;
align-items: center;
- flex: 1;
position: relative;
}
.tab-icon {
diff --git a/pages/mall/delivery/test.uvue b/pages/mall/delivery/test.uvue
new file mode 100644
index 00000000..ed67b4e9
--- /dev/null
+++ b/pages/mall/delivery/test.uvue
@@ -0,0 +1,418 @@
+
+
+
+
+
+ 当前配置:
+ URL: {{ config.url }}
+ Key: {{ config.keyMasked }}
+
+
+
+
+
+
+
+ 测试结果
+
+ {{ result.success ? '✅' : '❌' }}
+ {{ result.name }}
+ {{ result.message }}
+ {{ result.errorDetails }}
+
+
+
+
+ 单项测试
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From ab038ec029662aaa9dcd7c624be85d5e35dc6e96 Mon Sep 17 00:00:00 2001
From: cyh666666 <2398882793@qq.com>
Date: Thu, 29 Jan 2026 17:28:47 +0800
Subject: [PATCH 05/18] =?UTF-8?q?consumer=E6=A8=A1=E5=9D=97=E5=AE=8C?=
=?UTF-8?q?=E6=88=90=E5=BA=A685%=EF=BC=8C=E8=BF=9E=E6=8E=A5=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E5=99=A8supabase=EF=BC=8C=E6=96=B0=E5=BB=BA=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components/supadb/aksupa.uts | 34 +-
pages/mall/consumer/address-list.uvue | 170 +-
pages/mall/consumer/category - 副本.uvue | 1408 +++++++++++-
pages/mall/consumer/category.uvue | 418 +---
.../category完成分类及商品数据获取.uvue | 1131 ++++++++++
...- 副本 (4)完成分类、热销药品数据获取.uvue} | 921 ++++----
pages/mall/consumer/index - 副本.uvue | 1923 -----------------
pages/mall/consumer/index.uvue | 323 +--
pages/mall/consumer/indexback.uvue | 1540 -------------
.../mall/consumer/product-detail - 副本.uvue | 979 +++++++++
pages/mall/consumer/product-detail.uvue | 139 +-
pages/mall/consumer/search.uvue | 130 +-
pages/mall/consumer/wallet.uvue | 13 +
pages/user/login.uvue | 38 +-
utils/supabaseService.uts | 101 +-
15 files changed, 4475 insertions(+), 4793 deletions(-)
create mode 100644 pages/mall/consumer/category完成分类及商品数据获取.uvue
rename pages/mall/consumer/{indexgood.uvue => index - 副本 (4)完成分类、热销药品数据获取.uvue} (70%)
delete mode 100644 pages/mall/consumer/index - 副本.uvue
delete mode 100644 pages/mall/consumer/indexback.uvue
create mode 100644 pages/mall/consumer/product-detail - 副本.uvue
diff --git a/components/supadb/aksupa.uts b/components/supadb/aksupa.uts
index c47057f4..8980aaba 100644
--- a/components/supadb/aksupa.uts
+++ b/components/supadb/aksupa.uts
@@ -82,7 +82,26 @@ export class AkSupaQueryBuilder {
is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
- not(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'not', value); }
+ not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder {
+ if (value !== undefined) {
+ // 三元形式:field, operator, value
+ // 例如 not('badge', 'is', null) -> badge=not.is.null
+ const combinedOp = 'not.' + opOrValue;
+ // 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
+ let safeValue = value;
+ if (value === null) {
+ safeValue = 'null';
+ }
+ return this._addCond(field, combinedOp, safeValue);
+ } else {
+ // 二元形式:field, value
+ let safeValue = opOrValue;
+ if (opOrValue === null) {
+ safeValue = 'null';
+ }
+ return this._addCond(field, 'not', safeValue);
+ }
+ }
and() : AkSupaQueryBuilder { this._nextLogic = 'and'; return this; }
or(str ?: string) : AkSupaQueryBuilder {
@@ -97,7 +116,12 @@ export class AkSupaQueryBuilder {
private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder {
//console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!!
- this._conditions.push({ field, op, value, logic: this._nextLogic });
+ // 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
+ let safeValue = value;
+ if (value === null) {
+ safeValue = 'null';
+ }
+ this._conditions.push({ field, op, value: safeValue, logic: this._nextLogic });
//console.log(this._conditions)
this._nextLogic = 'and';
return this;
@@ -213,8 +237,8 @@ export class AkSupaQueryBuilder {
const val = cond.value;
if ((op == 'in' || op == 'not.in') && Array.isArray(val)) {
params.push(`${k}=${op}.(${val.map(x => typeof x == 'object' ? encodeURIComponent(JSON.stringify(x)) : encodeURIComponent(x.toString())).join(',')})`);
- } else if (op == 'is' && (val == null || val == 'null')) {
- params.push(`${k}=is.null`);
+ } else if ((op == 'is' || op == 'not.is') && (val == null || val == 'null')) {
+ params.push(`${k}=${op}.null`);
} else {
const opvalstr: string = (typeof val == 'object') ? JSON.stringify(val) : (val as string);
params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`);
@@ -1051,4 +1075,4 @@ export function createClient(url : string, key : string) : AkSupa {
return new AkSupa(url, key);
}
-export default AkSupa;
\ No newline at end of file
+export default AkSupa;
diff --git a/pages/mall/consumer/address-list.uvue b/pages/mall/consumer/address-list.uvue
index 67c6133d..1b6aeefa 100644
--- a/pages/mall/consumer/address-list.uvue
+++ b/pages/mall/consumer/address-list.uvue
@@ -156,6 +156,42 @@ const selectAddress = (item: Address) => {
diff --git a/pages/mall/consumer/category - 副本.uvue b/pages/mall/consumer/category - 副本.uvue
index d217ab27..5c02cd56 100644
--- a/pages/mall/consumer/category - 副本.uvue
+++ b/pages/mall/consumer/category - 副本.uvue
@@ -1,131 +1,1419 @@
-
-
\ No newline at end of file
diff --git a/pages/mall/consumer/category.uvue b/pages/mall/consumer/category.uvue
index 5c02cd56..ec734cdf 100644
--- a/pages/mall/consumer/category.uvue
+++ b/pages/mall/consumer/category.uvue
@@ -111,320 +111,60 @@
+
+
diff --git a/pages/mall/consumer/indexgood.uvue b/pages/mall/consumer/index - 副本 (4)完成分类、热销药品数据获取.uvue
similarity index 70%
rename from pages/mall/consumer/indexgood.uvue
rename to pages/mall/consumer/index - 副本 (4)完成分类、热销药品数据获取.uvue
index f780dd9d..d074d86d 100644
--- a/pages/mall/consumer/indexgood.uvue
+++ b/pages/mall/consumer/index - 副本 (4)完成分类、热销药品数据获取.uvue
@@ -6,40 +6,34 @@
class="smart-navbar"
:style="{
paddingTop: statusBarHeight + 'px',
- transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)',
- opacity: showNavbar ? 1 : 0
+ transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)'
}"
>
-
-
-
-
-
-
- 康乐医药商城
- 官方认证·正品保障
-
+
+
+
+ 请输入药品名称、症状或品牌
+
+
+
+ 🔳
+
+
+
+
+ 📷
-
-
-
-
- 🔍
-
-
- 语音
- 拍照
-
-
-
+
+
+ 搜索
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
- {{ product.tag }}
- {{ product.featured }}
-
-
-
-
- {{ product.name }}
- {{ product.specification }}
-
-
-
- ⭐
- {{ product.rating }}
-
- {{ product.reviews }}条评价
-
-
-
-
- ¥
- {{ product.price }}
-
-
- ¥{{ product.originalPrice }}
-
-
-
-
-
- 🛒
-
-
-
-
-
-
-
-
-
- 加载中...
-
-
-
- --- 已加载全部内容 ---
-
-
+
@@ -352,6 +268,9 @@
-
-
\ No newline at end of file
diff --git a/pages/mall/consumer/index.uvue b/pages/mall/consumer/index.uvue
index 854af7d0..d074d86d 100644
--- a/pages/mall/consumer/index.uvue
+++ b/pages/mall/consumer/index.uvue
@@ -244,85 +244,7 @@
-
-
-
-
-
-
-
-
-
- {{ product.tag }}
- {{ product.featured }}
-
-
-
-
- {{ product.name }}
- {{ product.specification }}
-
-
-
- ⭐
- {{ product.rating }}
-
- {{ product.reviews }}条评价
-
-
-
-
- ¥
- {{ product.price }}
-
-
- ¥{{ product.originalPrice }}
-
-
-
-
-
- 🛒
-
-
-
-
-
-
-
-
-
- 加载中...
-
-
-
- --- 已加载全部内容 ---
-
-
+
@@ -347,6 +269,8 @@
-
-
\ No newline at end of file
diff --git a/pages/mall/consumer/product-detail - 副本.uvue b/pages/mall/consumer/product-detail - 副本.uvue
new file mode 100644
index 00000000..500dd3ed
--- /dev/null
+++ b/pages/mall/consumer/product-detail - 副本.uvue
@@ -0,0 +1,979 @@
+
+
+
+
+
+
+
+
+
+
+ {{ currentImageIndex + 1 }} / {{ product.images.length }}
+
+
+
+
+
+ ¥{{ product.price }}
+ ¥{{ product.original_price }}
+
+ {{ product.name }}
+ 已售{{ product.sales }}件 · 库存{{ product.stock }}件
+
+
+
+
+
+
+ {{ merchant.shop_name }}
+
+ 评分: {{ merchant.rating.toFixed(1) }}
+ 销量: {{ merchant.total_sales }}
+
+
+ 进店 >
+
+
+
+
+ 规格
+ {{ selectedSpec || '请选择规格' }}
+ >
+
+
+
+
+ 数量
+
+
+ -
+
+
+
+ +
+
+
+ 库存{{ getAvailableStock() }}件
+
+
+
+
+ 商品详情
+ {{ product.description || '暂无详细描述' }}
+
+
+
+
+
+
+ 🛒
+ 购物车
+
+
+ {{ isFavorite ? '❤️' : '🤍' }}
+ {{ isFavorite ? '已收藏' : '收藏' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getSkuSpecText(sku) }}
+ ¥{{ sku.price }}
+ 库存{{ sku.stock }}
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/product-detail.uvue b/pages/mall/consumer/product-detail.uvue
index 500dd3ed..27e3757e 100644
--- a/pages/mall/consumer/product-detail.uvue
+++ b/pages/mall/consumer/product-detail.uvue
@@ -109,6 +109,7 @@
+```
+
+### 3.2 API 调用方式
+
+#### 3.2.1 查询数据 (SELECT)
+
+```typescript
+// 简单查询
+const res = await supa.select('ml_products', null, {
+ limit: 10,
+ order: 'created_at.desc'
+})
+
+// 带过滤条件
+const res = await supa.select('ml_products', {
+ status: 1,
+ category_id: categoryId
+}, {
+ limit: 20,
+ order: 'sale_count.desc'
+})
+
+// 复杂过滤 (PostgREST 操作符)
+const res = await supa.select('ml_products', {
+ base_price: { gte: 100, lte: 500 },
+ name: { ilike: '%商品%' },
+ category_id: { in: [id1, id2, id3] }
+}, {
+ limit: 20
+})
+
+// 单条记录
+const res = await supa.select('ml_products', { id: productId }, {
+ single: true
+})
+
+// 选择特定字段
+const res = await supa.select('ml_products', null, {
+ columns: 'id,name,base_price,main_image_url',
+ limit: 20
+})
+```
+
+#### 3.2.2 插入数据 (INSERT)
+
+```typescript
+// 插入订单
+const orderRes = await supa.insert('ml_orders', {
+ user_id: userId,
+ merchant_id: merchantId,
+ total_amount: 100.00,
+ order_status: 1,
+ payment_status: 1,
+ shipping_status: 1,
+ shipping_address: {
+ receiver_name: '张三',
+ receiver_phone: '13800138000',
+ address_detail: '北京市朝阳区xxx'
+ }
+})
+
+// 批量插入
+const items = [
+ { order_id: orderId, product_id: productId1, quantity: 2 },
+ { order_id: orderId, product_id: productId2, quantity: 1 }
+]
+const itemsRes = await supa.insert('ml_order_items', items)
+```
+
+#### 3.2.3 更新数据 (UPDATE)
+
+```typescript
+// 更新商品状态
+await supa.update('ml_products',
+ { id: productId }, // 过滤条件
+ {
+ status: 2, // 下架
+ updated_at: new Date().toISOString()
+ }
+)
+
+// 更新订单状态
+await supa.update('ml_orders',
+ { id: orderId },
+ {
+ order_status: 2, // 待发货
+ updated_at: new Date().toISOString()
+ }
+)
+```
+
+#### 3.2.4 删除数据 (DELETE)
+
+```typescript
+// 删除收藏
+await supa.delete('ml_user_favorites', { id: favoriteId })
+
+// 删除购物车商品
+await supa.delete('ml_shopping_cart', {
+ user_id: userId,
+ product_id: productId
+})
+```
+
+#### 3.2.5 调用数据库函数 (RPC)
+
+```typescript
+// 计算购物车总金额
+const totalRes = await supa.rpc('calculate_cart_total', {
+ p_user_id: userId
+})
+
+// 生成订单号
+const orderNoRes = await supa.rpc('generate_order_no')
+
+// 获取用户默认地址
+const addressRes = await supa.rpc('get_user_default_address', {
+ p_user_id: userId
+})
+```
+
+### 3.3 链式查询构建器
+
+```typescript
+// 使用链式 API
+const res = await supa
+ .from('ml_products')
+ .eq('status', 1)
+ .gte('base_price', 100)
+ .lte('base_price', 500)
+ .like('name', '%商品%')
+ .order('created_at', { ascending: false })
+ .limit(20)
+ .select()
+```
+
+---
+
+## 四、调试工具和方法
+
+### 4.1 浏览器开发者工具
+
+#### 网络请求调试
+1. **打开 Chrome DevTools** (F12)
+2. **Network 标签页**
+ - 查看所有 HTTP 请求
+ - 检查请求 URL、Headers、Body
+ - 查看响应状态码、数据
+
+3. **Console 标签页**
+ - 查看 `console.log()` 输出
+ - 查看错误信息
+ - 执行调试代码
+
+#### 示例: 检查 API 请求
+
+```typescript
+// 在代码中添加日志
+console.log('请求商品列表:', {
+ table: 'ml_products',
+ filter: { status: 1 },
+ options: { limit: 20 }
+})
+
+const res = await supa.select('ml_products', { status: 1 }, { limit: 20 })
+
+console.log('API 响应:', {
+ success: res.success,
+ data: res.data,
+ error: res.error,
+ status: res.status
+})
+```
+
+### 4.2 Supabase Dashboard
+
+#### 实时查看数据
+1. **Table Editor**
+ - 查看表数据
+ - 手动编辑数据
+ - 验证数据是否正确
+
+2. **SQL Editor**
+ - 执行 SQL 查询
+ - 测试数据库函数
+ - 验证 RLS 策略
+
+3. **API Logs**
+ - 查看 API 请求日志
+ - 检查错误信息
+ - 分析性能问题
+
+#### 示例: 测试查询
+
+```sql
+-- 在 Supabase Dashboard SQL Editor 中执行
+SELECT * FROM ml_products
+WHERE status = 1
+ORDER BY created_at DESC
+LIMIT 20;
+
+-- 测试 RLS 策略
+SET ROLE authenticated;
+SET request.jwt.claim.sub = 'user-uuid-here';
+SELECT * FROM ml_user_profiles;
+```
+
+### 4.3 Postman / Insomnia
+
+#### 直接测试 Supabase API
+
+```http
+# 获取商品列表
+GET https://your-project.supabase.co/rest/v1/ml_products?status=eq.1&limit=20
+Headers:
+ apikey: your-anon-key
+ Authorization: Bearer your-jwt-token
+ Content-Type: application/json
+
+# 创建订单
+POST https://your-project.supabase.co/rest/v1/ml_orders
+Headers:
+ apikey: your-anon-key
+ Authorization: Bearer your-jwt-token
+ Content-Type: application/json
+ Prefer: return=representation
+Body:
+{
+ "user_id": "user-uuid",
+ "merchant_id": "merchant-uuid",
+ "total_amount": 100.00,
+ "order_status": 1
+}
+
+# 调用 RPC 函数
+POST https://your-project.supabase.co/rest/v1/rpc/calculate_cart_total
+Headers:
+ apikey: your-anon-key
+ Authorization: Bearer your-jwt-token
+ Content-Type: application/json
+Body:
+{
+ "p_user_id": "user-uuid"
+}
+```
+
+### 4.4 数据库客户端工具
+
+#### pgAdmin / DBeaver / DataGrip
+
+```sql
+-- 直接连接 PostgreSQL 数据库
+-- Host: localhost (或 192.168.0.150)
+-- Port: 5432
+-- Database: postgres
+-- User: postgres
+-- Password: (从 supa.env 获取)
+
+-- 查看表结构
+SELECT * FROM information_schema.tables
+WHERE table_schema = 'public' AND table_name LIKE 'ml_%';
+
+-- 查看数据
+SELECT * FROM ml_products LIMIT 10;
+
+-- 查看 RLS 策略
+SELECT * FROM pg_policies WHERE tablename = 'ml_products';
+```
+
+### 4.5 uni-app-x 调试
+
+#### HBuilderX 调试工具
+1. **控制台输出**
+ - 查看 `console.log()` 输出
+ - 查看错误堆栈
+
+2. **网络请求监控**
+ - 查看所有网络请求
+ - 检查请求参数和响应
+
+3. **断点调试**
+ - 在代码中设置断点
+ - 单步执行
+ - 查看变量值
+
+---
+
+## 五、常见联调场景
+
+### 5.1 场景一: 查询商品列表失败
+
+#### 问题现象
+```typescript
+// 前端代码
+const res = await supa.select('ml_products', null, { limit: 20 })
+// res.success = false
+// res.error = "relation 'ml_products' does not exist"
+```
+
+#### 排查步骤
+1. **检查数据库表是否存在**
+ ```sql
+ SELECT table_name FROM information_schema.tables
+ WHERE table_schema = 'public' AND table_name = 'ml_products';
+ ```
+
+2. **检查表是否已创建**
+ - 执行 `complete_mall_database.sql` 脚本
+ - 验证脚本执行成功
+
+3. **检查连接配置**
+ ```typescript
+ console.log('Supabase URL:', SUPA_URL)
+ console.log('Supabase Key:', SUPA_KEY)
+ ```
+
+#### 解决方案
+```bash
+# 重新执行数据库脚本
+psql -h localhost -U postgres -d postgres -f doc_mall/database/complete_mall_database.sql
+```
+
+### 5.2 场景二: RLS 策略阻止数据访问
+
+#### 问题现象
+```typescript
+// 查询用户数据返回空
+const res = await supa.select('ml_user_profiles', { user_id: userId })
+// res.data = [] 或 null
+```
+
+#### 排查步骤
+1. **检查用户是否已登录**
+ ```typescript
+ const session = await supa.getSession()
+ console.log('当前用户:', session.user)
+ ```
+
+2. **检查 RLS 策略**
+ ```sql
+ -- 查看表的 RLS 策略
+ SELECT * FROM pg_policies WHERE tablename = 'ml_user_profiles';
+
+ -- 测试 RLS 策略
+ SET ROLE authenticated;
+ SET request.jwt.claim.sub = 'auth-user-id';
+ SELECT * FROM ml_user_profiles;
+ ```
+
+3. **检查 auth_id 关联**
+ ```sql
+ -- 验证 ak_users.auth_id 是否正确
+ SELECT id, auth_id FROM ak_users WHERE id = 'user-uuid';
+ ```
+
+#### 解决方案
+```typescript
+// 确保用户已登录
+await supa.signIn('user@example.com', 'password')
+
+// 或检查 Token
+const token = AkReq.getToken()
+console.log('JWT Token:', token)
+```
+
+### 5.3 场景三: 插入数据失败
+
+#### 问题现象
+```typescript
+// 插入订单失败
+const res = await supa.insert('ml_orders', orderData)
+// res.success = false
+// res.error = "new row violates row-level security policy"
+```
+
+#### 排查步骤
+1. **检查必填字段**
+ ```typescript
+ console.log('订单数据:', JSON.stringify(orderData, null, 2))
+ ```
+
+2. **检查外键约束**
+ ```sql
+ -- 验证 user_id 和 merchant_id 是否存在
+ SELECT id FROM ak_users WHERE id IN ('user-id', 'merchant-id');
+ ```
+
+3. **检查 RLS INSERT 策略**
+ ```sql
+ SELECT * FROM pg_policies
+ WHERE tablename = 'ml_orders' AND cmd = 'INSERT';
+ ```
+
+#### 解决方案
+```typescript
+// 确保数据完整
+const orderData = {
+ user_id: userId, // 必须存在
+ merchant_id: merchantId, // 必须存在
+ total_amount: 100.00,
+ order_status: 1,
+ payment_status: 1,
+ shipping_status: 1,
+ shipping_address: addressData // 必须提供
+}
+
+// 确保用户有权限
+await supa.signIn('user@example.com', 'password')
+```
+
+### 5.4 场景四: 实时数据同步不工作
+
+#### 问题现象
+```typescript
+// 订阅订单状态更新,但没有收到推送
+supa.realtime.subscribe('ml_orders', {
+ filter: `id=eq.${orderId}`,
+ event: 'UPDATE',
+ callback: (payload) => {
+ console.log('订单更新:', payload) // 没有触发
+ }
+})
+```
+
+#### 排查步骤
+1. **检查 WebSocket 连接**
+ ```typescript
+ console.log('WebSocket URL:', WS_URL)
+ ```
+
+2. **检查表是否启用 Realtime**
+ ```sql
+ -- 在 Supabase Dashboard 中检查
+ -- Database -> Replication -> 确保 ml_orders 表已启用
+ ```
+
+3. **检查网络连接**
+ - 确保 WebSocket 连接没有被防火墙阻止
+ - 检查浏览器控制台是否有 WebSocket 错误
+
+#### 解决方案
+```typescript
+// 确保 WebSocket URL 正确
+export const WS_URL: string = 'wss://your-project.supabase.co/realtime/v1/websocket'
+
+// 在 Supabase Dashboard 中启用表的 Realtime
+// Database -> Replication -> 找到 ml_orders -> 启用
+```
+
+### 5.5 场景五: 数据库函数调用失败
+
+#### 问题现象
+```typescript
+// 调用 RPC 函数失败
+const res = await supa.rpc('calculate_cart_total', { p_user_id: userId })
+// res.success = false
+// res.error = "function calculate_cart_total does not exist"
+```
+
+#### 排查步骤
+1. **检查函数是否存在**
+ ```sql
+ SELECT routine_name FROM information_schema.routines
+ WHERE routine_schema = 'public' AND routine_name = 'calculate_cart_total';
+ ```
+
+2. **检查函数参数**
+ ```sql
+ -- 查看函数定义
+ \df calculate_cart_total
+ ```
+
+3. **测试函数**
+ ```sql
+ SELECT calculate_cart_total('user-uuid-here');
+ ```
+
+#### 解决方案
+```bash
+# 重新执行数据库脚本,确保函数已创建
+psql -h localhost -U postgres -d postgres -f doc_mall/database/complete_mall_database.sql
+```
+
+---
+
+## 六、问题排查
+
+### 6.1 排查清单
+
+#### 连接问题
+- [ ] Supabase URL 是否正确
+- [ ] API Key 是否正确
+- [ ] 网络连接是否正常
+- [ ] 防火墙是否阻止连接
+
+#### 认证问题
+- [ ] 用户是否已登录
+- [ ] JWT Token 是否有效
+- [ ] Token 是否过期
+- [ ] auth_id 是否正确关联
+
+#### 数据问题
+- [ ] 表是否存在
+- [ ] 字段名是否正确
+- [ ] 数据类型是否匹配
+- [ ] 外键约束是否满足
+
+#### 权限问题
+- [ ] RLS 策略是否正确
+- [ ] 用户是否有权限
+- [ ] 策略条件是否满足
+
+### 6.2 常用调试命令
+
+#### 前端调试
+```typescript
+// 打印完整请求信息
+console.log('请求详情:', {
+ url: `${SUPA_URL}/rest/v1/ml_products`,
+ headers: {
+ apikey: SUPA_KEY,
+ Authorization: `Bearer ${token}`
+ },
+ filter: filter,
+ options: options
+})
+
+// 打印完整响应
+console.log('响应详情:', {
+ success: res.success,
+ status: res.status,
+ data: res.data,
+ error: res.error,
+ raw: res
+})
+```
+
+#### 数据库调试
+```sql
+-- 查看表结构
+\d ml_products
+
+-- 查看索引
+\di ml_products*
+
+-- 查看触发器
+\d+ ml_products
+
+-- 查看 RLS 策略
+SELECT * FROM pg_policies WHERE tablename = 'ml_products';
+
+-- 测试查询性能
+EXPLAIN ANALYZE SELECT * FROM ml_products WHERE status = 1;
+```
+
+### 6.3 错误码参考
+
+| HTTP 状态码 | 含义 | 常见原因 |
+| ----------- | ---------- | ---------------------- |
+| 200 | 成功 | - |
+| 400 | 请求错误 | 参数错误、数据格式错误 |
+| 401 | 未授权 | Token 无效、未登录 |
+| 403 | 禁止访问 | RLS 策略阻止 |
+| 404 | 未找到 | 表不存在、记录不存在 |
+| 500 | 服务器错误 | 数据库错误、函数错误 |
+
+### 6.4 日志收集
+
+#### 前端日志
+```typescript
+// 创建日志工具
+class DebugLogger {
+ static log(module: string, action: string, data: any) {
+ console.log(`[${module}] ${action}:`, data)
+ // 可以发送到日志服务器
+ }
+}
+
+// 使用
+DebugLogger.log('MallAPI', '查询商品', { filter, options })
+```
+
+#### 后端日志
+```sql
+-- 启用 PostgreSQL 日志
+-- 在 postgresql.conf 中设置
+log_statement = 'all'
+log_duration = on
+log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
+```
+
+---
+
+## 七、最佳实践
+
+### 7.1 开发环境配置
+
+1. **使用环境变量**
+ ```typescript
+ // 开发环境
+ const isDev = process.env.NODE_ENV === 'development'
+ export const SUPA_URL = isDev
+ ? 'http://192.168.0.150:8080'
+ : 'https://ak3.oulog.com'
+ ```
+
+2. **统一错误处理**
+ ```typescript
+ async function safeApiCall(apiCall: () => Promise>) {
+ try {
+ const res = await apiCall()
+ if (!res.success) {
+ console.error('API 调用失败:', res.error)
+ uni.showToast({ title: '操作失败', icon: 'error' })
+ }
+ return res
+ } catch (error) {
+ console.error('API 调用异常:', error)
+ uni.showToast({ title: '网络错误', icon: 'error' })
+ throw error
+ }
+ }
+ ```
+
+3. **请求重试机制**
+ ```typescript
+ async function retryApiCall(
+ apiCall: () => Promise>,
+ maxRetries = 3
+ ) {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ const res = await apiCall()
+ if (res.success) return res
+ } catch (error) {
+ if (i === maxRetries - 1) throw error
+ await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
+ }
+ }
+ }
+ ```
+
+### 7.2 联调流程
+
+1. **数据库准备**
+ - 执行数据库脚本
+ - 插入测试数据
+ - 验证表结构
+
+2. **前端配置**
+ - 配置 Supabase URL 和 Key
+ - 测试连接
+ - 验证认证
+
+3. **功能测试**
+ - 测试 CRUD 操作
+ - 测试 RLS 策略
+ - 测试实时同步
+
+4. **问题排查**
+ - 查看日志
+ - 检查网络请求
+ - 验证数据库数据
+
+---
+
+## 📚 相关文档
+
+- [模块分析报告](./MODULE_ANALYSIS.md)
+- [数据库创建报告](./database/database_creation_report.md)
+- [完整部署指南](./database/complete_deployment_guide.md)
+- [Supabase 官方文档](https://supabase.com/docs)
+
+---
+
+**生成时间**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 完整联调指南
diff --git a/mall_sql/docs/MALL_README.md b/mall_sql/docs/MALL_README.md
new file mode 100644
index 00000000..568aa3f4
--- /dev/null
+++ b/mall_sql/docs/MALL_README.md
@@ -0,0 +1,216 @@
+# 商城系统文档目录
+
+## 📁 目录结构
+
+```
+doc_mall/
+├── README.md # 本文件 - 文档目录索引
+├── user_reuse_summary.md # 用户表复用方案总结
+├── analysis/ # 分析文档
+│ └── user_compatibility_analysis.md # 用户表兼容性详细分析
+├── database/ # 数据库相关
+│ ├── complete_mall_database.sql # 🎯 完整商城数据库(推荐使用)
+│ ├── database_creation_report.md # 📊 数据库创建完成报告
+│ ├── database_syntax_fix_report.md # 数据库语法修正报告
+│ ├── user_compatibility_implementation.sql # 用户兼容性实施脚本
+│ ├── product_database.sql # 商品数据库设计脚本
+│ ├── mock_data_insert.sql # 模拟数据插入脚本
+│ ├── mock_data_documentation.md # 模拟数据说明文档
+│ ├── deployment_guide.md # 快速部署指南
+│ ├── validation_test.sql # 数据库验证测试脚本
+│ └── complete_deployment_guide.md # 完整部署与测试指南
+└── reports/ # 生成报告
+ ├── system_generation_report.md # 系统生成报告
+ ├── detail_pages_report.md # 详情页生成报告
+ └── profile_pages_report.md # 个人中心页面报告
+```
+
+## 📋 文档说明
+
+### 核心文档
+
+#### 📊 [模块深度分析报告](./MODULE_ANALYSIS.md) ⭐ **新增**
+- **内容**: 完整的模块分析,包括数据库存储、交互方式、开发模式、开发流程
+- **适用**: 了解模块整体架构和设计理念
+
+#### 🔧 [技术实现拆解](./TECHNICAL_IMPLEMENTATION.md) ⭐ **新增**
+- **内容**: 详细的技术实现拆解,包括数据库层、API层、前端实现、数据流机制
+- **适用**: 深入了解具体实现细节和开发方式
+
+#### 🔗 [前后端联调指南](./FRONTEND_BACKEND_DEBUGGING.md) ⭐ **新增**
+- **内容**: 完整的前后端联调指南,包括环境配置、调试工具、常见问题
+- **适用**: 开发调试和问题排查
+
+#### 🎯 [用户表复用方案总结](./user_reuse_summary.md)
+- **问题**: 商城系统是否可以复用运动训练平台的 `ak_users` 用户表?
+- **结论**: ✅ 可以复用,采用混合扩展方案
+- **方案**: 保持 `ak_users` 主表不变,增加商城扩展表
+- **优势**: 单点登录、数据一致性、业务隔离
+
+#### 🔍 [用户兼容性详细分析](./analysis/user_compatibility_analysis.md)
+- 字段级兼容性对比分析
+- 三种方案对比(共用/独立/混合)
+- 风险评估和解决方案
+- 具体实施步骤和建议
+
+### 数据库脚本
+
+#### 🎯 [完整商城数据库](./database/complete_mall_database.sql) **← 推荐使用**
+- **全新设计**: 使用 `ml_` 前缀的独立商城数据库
+- **复用优化**: 仅复用 `ak_users` 用户主表
+- **功能完整**: 21张表覆盖所有商城功能
+- **Supabase优化**: 包含RLS策略、触发器、函数、视图
+- **性能优化**: 完整索引设计和查询优化
+
+#### 📊 [数据库创建报告](./database/database_creation_report.md)
+- 详细的数据库架构说明
+- 21张表的功能分析和设计理念
+- 索引、触发器、函数、视图的完整清单
+- 部署步骤和性能优化建议
+
+#### 🛠️ [数据库语法修正报告](./database/database_syntax_fix_report.md)
+- 修正了RLS策略的语法错误
+- 提供了修正前后的对比
+- 包含常见问题解答和修正建议
+
+#### 🔧 [类型错误修正报告](./database/type_error_fix_report.md)
+- **问题分析**: auth_id 字段 UUID 类型错误的详细分析
+- **修正措施**: 完整的问题解决方案和预防措施
+- **验证工具**: 新增的验证脚本和部署指南
+- **流程优化**: 改进的部署流程和错误监控建议
+
+#### 🎯 [SEO 优化实施报告](./database/seo_optimization_report.md)
+- **优化成果**: CID 自增字段的完整实施效果
+- **性能提升**: URL 结构、查询性能、存储空间的全面优化
+- **技术细节**: 索引、视图、函数的具体实现
+- **收益分析**: SEO 表现、用户体验、开发效率的预期提升
+
+#### 💾 [用户兼容性实施脚本](./database/user_compatibility_implementation.sql)
+- 商城用户扩展表 `mall_user_profiles`
+- 用户地址表 `ak_user_addresses`
+- 用户收藏、搜索、浏览历史表
+- 触发器、索引、RLS策略
+- 数据迁移和权限设置
+
+#### 🛍️ [商品数据库设计脚本](./database/product_database.sql)
+- 完整的商品管理数据库设计
+- 商品、SKU、分类、品牌、规格等表
+- 支持多规格、库存管理、营销活动
+- 推荐使用独立商品表,不复用 `ak_contents`
+
+#### 🔍 [数据库验证测试脚本](./database/validation_test.sql)
+- **环境检查**: 验证 PostgreSQL 扩展和依赖项
+- **表结构验证**: 检查 `ak_users` 表和商城表的完整性
+- **语法测试**: 验证 RLS 策略和 UUID 类型的正确性
+- **数据统计**: 检查模拟数据的插入情况
+
+#### 📚 [完整部署与测试指南](./database/complete_deployment_guide.md)
+- **部署前检查**: 详细的环境要求和准备工作
+- **分步骤部署**: PostgreSQL 和 Supabase 的完整部署流程
+- **验证测试**: 部署后的功能验证和性能检查
+- **问题解决**: 常见错误的详细解决方案和预防措施
+- **维护建议**: 数据维护、备份和性能优化指导
+
+#### 🧪 [模拟数据插入脚本](./database/mock_data_insert.sql)
+- **测试专用**: 为开发和测试生成完整的模拟数据
+- **数据丰富**: 包含8个测试用户、6个商品、多个订单等
+- **场景完整**: 涵盖购物车、优惠券、评价、配送等业务场景
+- **依赖**: 需要先执行 `complete_mall_database.sql`
+
+#### 📋 [模拟数据说明文档](./database/mock_data_documentation.md)
+- **详细说明**: 所有测试数据的详细说明和使用指南
+- **用户角色**: 8个测试用户的账号信息和权限说明
+- **测试场景**: 完整的业务流程测试建议
+- **数据维护**: 数据更新和维护的最佳实践
+
+#### 🚀 [快速部署指南](./database/deployment_guide.md)
+- **部署步骤**: PostgreSQL 和 Supabase 的详细部署指南
+- **执行顺序**: 脚本执行的正确顺序和注意事项
+- **测试验证**: 部署后的功能验证和性能测试
+- **问题排查**: 常见问题的解决方案和检查清单
+
+#### 🔍 [SEO 优化指南](./database/seo_optimization_guide.md)
+- **CID 自增字段**: 为主要表添加 SEO 友好的自增 ID
+- **URL 结构优化**: 提供简洁、语义化的 URL 路径
+- **函数工具**: 完整的 SEO 相关查询和工具函数
+- **前端集成**: Vue Router 配置和 API 调用示例
+- **性能监控**: 索引优化和查询性能监控指导
+
+### 生成报告
+
+#### 📊 [系统生成报告](./reports/system_generation_report.md)
+- 6个角色端首页代码生成完成
+- 类型定义和UTS Android兼容性说明
+- 页面功能模块和技术特点总结
+
+#### 📄 [详情页生成报告](./reports/detail_pages_report.md)
+- 商品详情、订单详情、店铺详情等页面
+- 具体功能实现和代码结构
+- UTS Android语法规范遵循情况
+
+#### 👤 [个人中心页面报告](./reports/profile_pages_report.md)
+- 6个角色端个人中心页面生成
+- 用户信息管理、设置、统计等功能
+- 响应式设计和现代UI实现
+
+## 🔗 相关文件
+
+### 类型定义
+- `../types/mall-types.uts` - 商城系统完整类型定义
+
+### 页面代码
+- `../pages/mall/` - 所有角色端页面代码
+- `../pages/mall/pages-config.json` - 页面路由配置
+
+### 订阅功能(本次实现)
+- 数据库脚本:`./create_mall_subscription_tables.sql`
+- RLS/权限:`./subscription_rls_policies.sql`
+- 消费端页面:
+ - `../pages/mall/consumer/subscription/plan-list.uvue`
+ - `../pages/mall/consumer/subscription/plan-detail.uvue`
+ - `../pages/mall/consumer/subscription/subscribe-checkout.uvue`
+ - `../pages/mall/consumer/subscription/my-subscriptions.uvue`
+- 管理端页面:
+ - `../pages/mall/admin/subscription/plan-management.uvue`
+ - `../pages/mall/admin/subscription/user-subscriptions.uvue`
+
+### 业务需求
+- `../mall.md` - 原始业务需求文档
+
+## 🎯 核心结论
+
+### 🆕 最新推荐方案
+✅ **使用完整商城数据库设计**:
+- 使用 `complete_mall_database.sql` 创建独立商城系统
+- 仅复用 `ak_users` 用户主表,其他表全部独立
+- 包含 20+ 张表,覆盖用户、商品、订单、营销、配送等全部功能
+- 优化的 Supabase 兼容设计(RLS、触发器、函数、视图)
+
+### 用户表复用方案
+✅ **推荐采用混合扩展方案**:
+- 保持 `ak_users` 表作为用户主表
+- 创建 `ml_user_profiles` 商城扩展表
+- 新建 `ml_user_addresses` 地址管理表
+- 实现业务数据隔离的同时保持账号统一
+
+### 商品表设计方案
+❌ **不推荐复用 `ak_contents` 表**:
+- 语义不匹配、字段冲突、业务逻辑差异
+- 强烈建议使用独立的商品数据库设计
+
+### 技术实现标准
+✅ **严格遵循UTS Android兼容性**:
+- 全部使用 `type` 声明,避免 `interface`
+- 数组类型使用 `Array` 格式
+- 无 `undefined` 类型,变量类型明确
+- JSON对象使用 `UTSJSONObject`
+
+## 📞 支持
+
+本文档集涵盖了商城系统与运动训练平台用户表复用的完整分析和实施方案。如需了解更多技术细节,请查看相应的具体文档文件。
+
+---
+
+**生成时间**: 2025年7月11日
+**版本**: v1.0
+**状态**: ✅ 已完成分析和实施方案
diff --git a/mall_sql/docs/MIGRATION_CHECKLIST.md b/mall_sql/docs/MIGRATION_CHECKLIST.md
new file mode 100644
index 00000000..a3fa6a22
--- /dev/null
+++ b/mall_sql/docs/MIGRATION_CHECKLIST.md
@@ -0,0 +1,254 @@
+# ✅ doc_mall 项目迁移检查清单
+
+## 📋 文件迁移清单
+
+### 1. 文档和数据库脚本 (`doc_mall/`)
+
+#### 核心文档
+- [ ] `README.md` - 文档索引
+- [ ] `TECHNICAL_IMPLEMENTATION.md` - 技术实现拆解
+- [ ] `MODULE_ANALYSIS.md` - 模块深度分析
+- [ ] `FRONTEND_BACKEND_DEBUGGING.md` - 前后端联调指南
+- [ ] `user_reuse_summary.md` - 用户表复用方案
+- [ ] `migration_complete_report.md` - 迁移完成报告
+- [ ] `README_subscription_consumer.md` - 订阅功能说明
+- [ ] `裂变红包.md` - 红包功能文档
+- [ ] `MIGRATION_GUIDE.md` - 迁移指南(本文档)
+- [ ] `MIGRATION_CHECKLIST.md` - 迁移清单(本文件)
+
+#### SQL 脚本
+- [ ] `create_mall_subscription_tables.sql` - 订阅表创建脚本
+- [ ] `subscription_guard_trigger.sql` - 订阅触发器
+- [ ] `subscription_rls_policies.sql` - 订阅RLS策略
+
+#### 分析文档目录 (`analysis/`)
+- [ ] `analysis/user_compatibility_analysis.md` - 用户兼容性分析
+
+#### 数据库目录 (`database/`)
+- [ ] `database/complete_mall_database.sql` - 完整数据库(推荐)
+- [ ] `database/database_creation_report.md` - 数据库创建报告
+- [ ] `database/database_syntax_fix_report.md` - 语法修正报告
+- [ ] `database/type_error_fix_report.md` - 类型错误修正报告
+- [ ] `database/seo_optimization_report.md` - SEO优化报告
+- [ ] `database/seo_optimization_guide.md` - SEO优化指南
+- [ ] `database/user_compatibility_implementation.sql` - 用户兼容性实施脚本
+- [ ] `database/product_database.sql` - 商品数据库设计
+- [ ] `database/mock_data_insert.sql` - 模拟数据插入脚本
+- [ ] `database/mock_data_documentation.md` - 模拟数据说明
+- [ ] `database/deployment_guide.md` - 快速部署指南
+- [ ] `database/validation_test.sql` - 数据库验证测试脚本
+- [ ] `database/complete_deployment_guide.md` - 完整部署与测试指南
+- [ ] `database/database_creation_report.md` - 数据库创建报告
+- [ ] `database/[其他SQL文件]` - 其他数据库脚本
+
+#### 报告目录 (`reports/`)
+- [ ] `reports/system_generation_report.md` - 系统生成报告
+- [ ] `reports/detail_pages_report.md` - 详情页生成报告
+- [ ] `reports/profile_pages_report.md` - 个人中心页面报告
+
+---
+
+### 2. 前端页面代码 (`pages/mall/`)
+
+#### 管理端页面 (`admin/`)
+- [ ] `admin/index.uvue` - 管理端首页
+- [ ] `admin/profile.uvue` - 管理端个人中心
+- [ ] `admin/user-detail.uvue` - 用户详情
+- [ ] `admin/subscription/plan-management.uvue` - 订阅方案管理
+- [ ] `admin/subscription/user-subscriptions.uvue` - 用户订阅管理
+
+#### 数据分析端页面 (`analytics/`)
+- [ ] `analytics/index.uvue` - 数据分析首页
+- [ ] `analytics/profile.uvue` - 数据分析个人中心
+- [ ] `analytics/report-detail.uvue` - 报表详情
+
+#### 消费者端页面 (`consumer/`)
+- [ ] `consumer/index.uvue` - 消费者首页
+- [ ] `consumer/product-detail.uvue` - 商品详情
+- [ ] `consumer/order-detail.uvue` - 订单详情
+- [ ] `consumer/profile.uvue` - 消费者个人中心
+- [ ] `consumer/subscription/plan-list.uvue` - 订阅方案列表
+- [ ] `consumer/subscription/plan-detail.uvue` - 订阅方案详情
+- [ ] `consumer/subscription/subscribe-checkout.uvue` - 订阅确认
+- [ ] `consumer/subscription/my-subscriptions.uvue` - 我的订阅
+- [ ] `consumer/subscription/README.md` - 订阅功能说明
+
+#### 配送端页面 (`delivery/`)
+- [ ] `delivery/index.uvue` - 配送端首页
+- [ ] `delivery/order-detail.uvue` - 配送订单详情
+- [ ] `delivery/profile.uvue` - 配送员个人中心
+
+#### 商家端页面 (`merchant/`)
+- [ ] `merchant/index.uvue` - 商家端首页
+- [ ] `merchant/product-detail.uvue` - 商品管理详情
+- [ ] `merchant/profile.uvue` - 商家个人中心
+
+#### 客服端页面 (`service/`)
+- [ ] `service/index.uvue` - 客服工作台首页
+- [ ] `service/profile.uvue` - 客服个人中心
+- [ ] `service/ticket-detail.uvue` - 工单详情
+
+#### NFC 功能页面 (`nfc/`) - 可选
+- [ ] `nfc/admin/index.uvue`
+- [ ] `nfc/librarian/index.uvue`
+- [ ] `nfc/merchant/pos-cashier.uvue`
+- [ ] `nfc/parent/index.uvue`
+- [ ] `nfc/security/index.uvue`
+- [ ] `nfc/student/index.uvue`
+- [ ] `nfc/student/nfc-pay.uvue`
+- [ ] `nfc/teacher/index.uvue`
+
+#### 配置文件
+- [ ] `mall.md` - 业务需求文档
+- [ ] `nfc.md` - NFC功能文档
+- [ ] `nfc-modules-guide.md` - NFC模块指南
+- [ ] `pages-config.json` - 主要页面路由配置
+- [ ] `pages-admin.json` - 管理端路由配置
+- [ ] `pages-librarian.json` - 图书管理员路由配置
+- [ ] `pages-merchant.json` - 商家路由配置
+- [ ] `pages-parent.json` - 家长路由配置
+- [ ] `pages-security.json` - 安全员路由配置
+- [ ] `pages-student.json` - 学生路由配置
+- [ ] `pages-teacher.json` - 教师路由配置
+
+---
+
+### 3. 类型定义文件
+
+- [ ] `types/mall-types.uts` - 商城系统完整类型定义(**必须迁移**)
+
+---
+
+### 4. 依赖文件(可选,根据实际情况)
+
+#### Supabase 客户端封装
+- [ ] `components/supadb/aksupainstance.uts` - Supabase实例
+- [ ] `components/supadb/aksupa.uts` - Supabase客户端封装
+- [ ] `components/supadb/aksuparealtime.uts` - 实时订阅封装
+- [ ] `components/supadb/[其他相关文件]` - 其他Supabase相关文件
+
+#### 工具函数(根据实际引用)
+- [ ] 检查 `pages/mall/` 中所有 `@/utils/` 的引用
+- [ ] 迁移需要的工具函数文件
+
+---
+
+## 🔗 依赖关系检查
+
+### 数据库依赖
+- [ ] 确定用户表处理方案(独立表/复用表/API服务)
+- [ ] 更新相关外键引用(如需要)
+- [ ] 配置 Supabase 项目连接信息
+
+### 代码依赖
+- [ ] 检查所有 `@/types/mall-types.uts` 引用(应已迁移)
+- [ ] 检查所有 `@/components/supadb` 引用
+- [ ] 检查所有 `@/utils/` 引用
+- [ ] 更新导入路径(如需要)
+
+### 配置文件依赖
+- [ ] Supabase 项目 URL 和 API Key
+- [ ] 环境变量配置
+- [ ] 路由配置(pages-config.json 等)
+
+---
+
+## 🗄️ 数据库迁移步骤
+
+### 环境准备
+- [ ] 创建新的 Supabase 项目(或确定使用现有项目)
+- [ ] 获取 Supabase 项目 URL 和 API Key
+- [ ] 准备 PostgreSQL 客户端工具(如需要)
+
+### 数据库脚本执行
+- [ ] 执行 `database/complete_mall_database.sql` - 创建完整数据库结构
+- [ ] 执行 `subscription_rls_policies.sql` - 订阅RLS策略
+- [ ] 执行 `subscription_guard_trigger.sql` - 订阅触发器
+- [ ] 执行 `database/mock_data_insert.sql` - 插入测试数据(可选)
+- [ ] 执行 `database/validation_test.sql` - 验证数据库状态
+
+### 数据库验证
+- [ ] 验证所有表已创建
+- [ ] 验证 RLS 策略已生效
+- [ ] 验证触发器已创建
+- [ ] 验证索引已创建
+- [ ] 测试数据查询功能
+
+---
+
+## 🔧 代码适配检查
+
+### 路径更新
+- [ ] 检查所有文件中的 `@/types/mall-types` 导入路径
+- [ ] 检查所有文件中的 `@/components/supadb` 导入路径
+- [ ] 检查所有文件中的 `@/utils/` 导入路径
+- [ ] 更新为正确的相对路径或配置别名
+
+### 配置更新
+- [ ] 更新 Supabase 客户端初始化配置
+- [ ] 更新环境变量配置
+- [ ] 更新路由配置文件(如需要)
+
+### 文档更新
+- [ ] 更新文档中的路径引用
+- [ ] 更新文档中的配置说明
+
+---
+
+## 🧪 测试验证
+
+### 编译测试
+- [ ] 项目可以正常编译
+- [ ] 无类型错误
+- [ ] 无导入路径错误
+
+### 运行时测试
+- [ ] 应用可以正常启动
+- [ ] Supabase 连接正常
+- [ ] 页面可以正常加载
+
+### 功能测试
+- [ ] 用户认证功能正常
+- [ ] 商品浏览功能正常
+- [ ] 订单创建功能正常
+- [ ] 权限控制正常(RLS)
+
+### 数据库测试
+- [ ] CRUD 操作正常
+- [ ] RLS 策略生效
+- [ ] 触发器正常工作
+
+---
+
+## 📝 迁移记录
+
+### 迁移信息
+- **迁移日期**: ___________
+- **源项目路径**: ___________
+- **目标项目路径**: ___________
+- **迁移人员**: ___________
+
+### 迁移问题记录
+| 问题描述 | 解决方案 | 状态 |
+| -------- | -------- | ---- |
+| | | |
+| | | |
+
+### 待处理事项
+- [ ]
+- [ ]
+- [ ]
+
+---
+
+## ✅ 迁移完成确认
+
+- [ ] 所有文件已迁移
+- [ ] 所有依赖已处理
+- [ ] 数据库已配置
+- [ ] 代码已适配
+- [ ] 测试已通过
+- [ ] 文档已更新
+
+**迁移完成签名**: ___________
+**完成日期**: ___________
diff --git a/mall_sql/docs/MIGRATION_GUIDE.md b/mall_sql/docs/MIGRATION_GUIDE.md
new file mode 100644
index 00000000..30a77be6
--- /dev/null
+++ b/mall_sql/docs/MIGRATION_GUIDE.md
@@ -0,0 +1,542 @@
+# 🚀 doc_mall 项目迁移指南
+
+## 📋 迁移概述
+
+本指南将帮助你将 `doc_mall` 商城系统模块从当前项目迁移到一个独立的仓库中,确保所有相关模块和依赖项都能正确复用。
+
+---
+
+## 📁 需要迁移的文件和目录清单
+
+### 1. 核心文档和数据库脚本 (`doc_mall/`)
+
+**完整目录结构**:
+```
+doc_mall/
+├── README.md # 文档索引
+├── TECHNICAL_IMPLEMENTATION.md # 技术实现拆解
+├── MODULE_ANALYSIS.md # 模块深度分析
+├── FRONTEND_BACKEND_DEBUGGING.md # 前后端联调指南
+├── user_reuse_summary.md # 用户表复用方案
+├── migration_complete_report.md # 迁移完成报告
+├── README_subscription_consumer.md # 订阅功能说明
+├── 裂变红包.md # 红包功能文档
+├── create_mall_subscription_tables.sql # 订阅表创建脚本
+├── subscription_guard_trigger.sql # 订阅触发器
+├── subscription_rls_policies.sql # 订阅RLS策略
+├── analysis/ # 分析文档目录
+│ └── user_compatibility_analysis.md
+├── database/ # 数据库脚本目录
+│ ├── complete_mall_database.sql # 完整数据库(推荐)
+│ ├── database_creation_report.md
+│ ├── database_syntax_fix_report.md
+│ ├── user_compatibility_implementation.sql
+│ ├── product_database.sql
+│ ├── mock_data_insert.sql
+│ ├── mock_data_documentation.md
+│ ├── deployment_guide.md
+│ ├── validation_test.sql
+│ ├── complete_deployment_guide.md
+│ └── [其他SQL和文档文件]
+└── reports/ # 生成报告目录
+ ├── system_generation_report.md
+ ├── detail_pages_report.md
+ └── profile_pages_report.md
+```
+
+**迁移操作**:
+```bash
+# 直接复制整个 doc_mall 目录
+cp -r doc_mall/ /path/to/new-repo/doc_mall/
+```
+
+---
+
+### 2. 前端页面代码 (`pages/mall/`)
+
+**完整目录结构**:
+```
+pages/mall/
+├── admin/ # 管理端页面
+│ ├── index.uvue
+│ ├── profile.uvue
+│ ├── user-detail.uvue
+│ └── subscription/
+│ ├── plan-management.uvue
+│ └── user-subscriptions.uvue
+├── analytics/ # 数据分析端页面
+│ ├── index.uvue
+│ ├── profile.uvue
+│ └── report-detail.uvue
+├── consumer/ # 消费者端页面
+│ ├── index.uvue
+│ ├── product-detail.uvue
+│ ├── order-detail.uvue
+│ ├── profile.uvue
+│ └── subscription/
+│ ├── plan-list.uvue
+│ ├── plan-detail.uvue
+│ ├── subscribe-checkout.uvue
+│ ├── my-subscriptions.uvue
+│ └── README.md
+├── delivery/ # 配送端页面
+│ ├── index.uvue
+│ ├── order-detail.uvue
+│ └── profile.uvue
+├── merchant/ # 商家端页面
+│ ├── index.uvue
+│ ├── product-detail.uvue
+│ └── profile.uvue
+├── service/ # 客服端页面
+│ ├── index.uvue
+│ ├── profile.uvue
+│ └── ticket-detail.uvue
+├── nfc/ # NFC支付相关(可选)
+│ ├── admin/index.uvue
+│ ├── librarian/index.uvue
+│ ├── merchant/pos-cashier.uvue
+│ ├── parent/index.uvue
+│ ├── security/index.uvue
+│ ├── student/index.uvue
+│ ├── teacher/index.uvue
+│ └── [其他NFC相关文件]
+├── mall.md # 业务需求文档
+├── nfc.md # NFC功能文档
+├── nfc-modules-guide.md # NFC模块指南
+├── pages-config.json # 页面路由配置(主要)
+├── pages-admin.json # 管理端路由配置
+├── pages-librarian.json # 图书管理员路由配置
+├── pages-merchant.json # 商家路由配置
+├── pages-parent.json # 家长路由配置
+├── pages-security.json # 安全员路由配置
+├── pages-student.json # 学生路由配置
+└── pages-teacher.json # 教师路由配置
+```
+
+**迁移操作**:
+```bash
+# 复制整个 pages/mall 目录
+cp -r pages/mall/ /path/to/new-repo/pages/mall/
+```
+
+---
+
+### 3. 类型定义文件 (`types/mall-types.uts`)
+
+**文件路径**:
+```
+types/mall-types.uts
+```
+
+**说明**:
+- 包含所有商城系统相关的 TypeScript/UTS 类型定义
+- 所有 `pages/mall/` 下的页面都依赖此文件
+- 必须迁移,否则前端代码无法编译
+
+**迁移操作**:
+```bash
+# 复制类型定义文件
+mkdir -p /path/to/new-repo/types
+cp types/mall-types.uts /path/to/new-repo/types/
+```
+
+---
+
+## 🔗 依赖关系分析
+
+### 3.1 内部依赖(必须迁移)
+
+#### Supabase 客户端封装
+- **依赖文件**:`components/supadb/aksupainstance.uts`
+- **使用情况**:所有商城页面都通过 Supabase 客户端访问数据库
+- **迁移建议**:
+ - 如果新仓库也需要使用 Supabase,需要迁移此文件
+ - 或者创建新的 Supabase 客户端封装
+
+#### 工具函数
+- **检查项目**:是否需要 `utils/` 下的工具函数
+- **常见使用**:日期格式化、数据验证等
+- **迁移建议**:
+ - 检查商城页面中 `@/utils/` 的引用
+ - 根据需要迁移相应的工具函数
+
+### 3.2 外部依赖(需要配置)
+
+#### 数据库依赖
+- **依赖表**:`ak_users` (用户主表)
+- **说明**:商城系统复用现有用户表实现单点登录
+- **迁移策略**:
+ - **方案A**:在新仓库中创建独立的用户表
+ - **方案B**:保持与现有用户表的关联(需要跨数据库访问)
+ - **方案C**:通过 API 服务访问用户数据
+
+#### Supabase 配置
+- **需要配置**:Supabase 项目 URL 和 API Key
+- **配置文件**:通常保存在环境变量或配置文件中
+- **迁移步骤**:
+ 1. 在新仓库创建 Supabase 项目(或使用现有项目)
+ 2. 执行数据库脚本创建表结构
+ 3. 配置 RLS 策略和权限
+ 4. 更新前端配置中的 Supabase 连接信息
+
+---
+
+## 📝 迁移步骤详解
+
+### 步骤 1: 创建新仓库
+
+```bash
+# 1. 创建新仓库目录
+mkdir mall-system
+cd mall-system
+
+# 2. 初始化 Git 仓库
+git init
+
+# 3. 创建基础目录结构
+mkdir -p doc_mall/{analysis,database,reports}
+mkdir -p pages/mall
+mkdir -p types
+mkdir -p components/supadb # 如果需要
+mkdir -p utils # 如果需要
+```
+
+### 步骤 2: 迁移文档和数据库脚本
+
+```bash
+# 从原项目复制 doc_mall 目录
+cp -r /path/to/akmon/doc_mall/* ./doc_mall/
+
+# 验证文件完整性
+ls -la doc_mall/
+```
+
+### 步骤 3: 迁移前端代码
+
+```bash
+# 复制页面代码
+cp -r /path/to/akmon/pages/mall/* ./pages/mall/
+
+# 复制类型定义
+cp /path/to/akmon/types/mall-types.uts ./types/
+
+# 验证关键文件
+ls -la pages/mall/
+ls -la types/mall-types.uts
+```
+
+### 步骤 4: 迁移依赖文件(可选)
+
+```bash
+# 如果需要 Supabase 客户端
+cp -r /path/to/akmon/components/supadb/* ./components/supadb/
+
+# 检查并迁移需要的工具函数
+# 查看 pages/mall/ 下文件中的 import 语句
+grep -r "from '@/utils" pages/mall/
+```
+
+### 步骤 5: 更新导入路径
+
+在新仓库中,需要检查并更新以下内容:
+
+#### 5.1 检查类型导入
+```bash
+# 查找所有 mall-types 的引用
+grep -r "from '@/types/mall-types" pages/mall/
+```
+
+确保导入路径正确:
+```typescript
+// 应该是相对路径或配置的别名
+import type { ProductType } from '@/types/mall-types.uts'
+```
+
+#### 5.2 检查 Supabase 导入
+```bash
+# 查找 Supabase 引用
+grep -r "from '@/components/supadb" pages/mall/
+```
+
+#### 5.3 检查工具函数导入
+```bash
+# 查找工具函数引用
+grep -r "from '@/utils" pages/mall/
+```
+
+### 步骤 6: 配置数据库
+
+#### 6.1 创建 Supabase 项目
+1. 访问 Supabase 控制台
+2. 创建新项目或使用现有项目
+3. 记录项目 URL 和 API Key
+
+#### 6.2 执行数据库脚本
+```bash
+# 方式1: 通过 Supabase Dashboard SQL Editor
+# 打开 doc_mall/database/complete_mall_database.sql
+# 复制内容到 Supabase SQL Editor 执行
+
+# 方式2: 通过 psql 命令行(如果使用自建 PostgreSQL)
+psql -h localhost -U postgres -d your_database -f doc_mall/database/complete_mall_database.sql
+```
+
+#### 6.3 配置 RLS 策略
+确保执行以下脚本:
+- `doc_mall/subscription_rls_policies.sql`
+- `doc_mall/subscription_guard_trigger.sql`
+- 数据库脚本中已包含的 RLS 策略
+
+#### 6.4 插入测试数据(可选)
+```bash
+psql -h localhost -U postgres -d your_database -f doc_mall/database/mock_data_insert.sql
+```
+
+### 步骤 7: 配置前端环境
+
+#### 7.1 创建配置文件
+在新仓库根目录创建配置文件(根据你的项目结构):
+
+**示例:`config/supabase.config.ts`**
+```typescript
+export const supabaseConfig = {
+ url: 'https://your-project.supabase.co',
+ anonKey: 'your-anon-key',
+ // 其他配置
+}
+```
+
+#### 7.2 更新 Supabase 客户端初始化
+如果迁移了 `components/supadb/aksupainstance.uts`,确保它使用新的配置。
+
+#### 7.3 配置路由
+检查并更新以下路由配置文件:
+- `pages/mall/pages-config.json` (主要路由配置)
+- 其他角色端路由配置 JSON 文件
+
+### 步骤 8: 处理用户表依赖
+
+商城系统依赖 `ak_users` 表,有几种处理方案:
+
+#### 方案 A: 创建独立的用户表(推荐用于完全独立部署)
+```sql
+-- 创建独立的用户表(简化版)
+CREATE TABLE public.mall_users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ phone VARCHAR(20) UNIQUE NOT NULL,
+ email VARCHAR(255),
+ nickname VARCHAR(100),
+ -- 其他必要字段
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+```
+
+然后更新所有 `ml_user_profiles` 等表的外键引用:
+```sql
+-- 修改外键引用
+ALTER TABLE public.ml_user_profiles
+DROP CONSTRAINT ml_user_profiles_user_id_fkey;
+
+ALTER TABLE public.ml_user_profiles
+ADD CONSTRAINT ml_user_profiles_user_id_fkey
+FOREIGN KEY (user_id) REFERENCES public.mall_users(id) ON DELETE CASCADE;
+```
+
+#### 方案 B: 通过 API 服务访问用户数据
+- 保持数据库结构不变
+- 通过 API 服务层访问用户数据
+- 前端不直接访问 `ak_users` 表
+
+#### 方案 C: 跨数据库访问(复杂,不推荐)
+- 保持现有的数据库引用
+- 配置跨数据库访问权限
+
+### 步骤 9: 测试和验证
+
+#### 9.1 编译测试
+```bash
+# 尝试编译项目(根据你的构建工具)
+npm run build
+# 或
+uni-app-x build
+```
+
+#### 9.2 数据库连接测试
+- 测试 Supabase 连接
+- 验证 RLS 策略是否生效
+- 测试 CRUD 操作
+
+#### 9.3 功能测试
+- 测试各个角色端页面是否能正常加载
+- 测试核心业务流程(商品浏览、下单等)
+- 测试权限控制
+
+---
+
+## 🔧 迁移后需要修改的内容
+
+### 1. 更新导入路径
+
+检查并更新所有文件中的导入路径,确保它们指向正确的位置:
+
+```typescript
+// 原项目中的导入
+import type { ProductType } from '@/types/mall-types.uts'
+import { supabase } from '@/components/supadb/aksupainstance.uts'
+
+// 新仓库中可能需要调整为
+import type { ProductType } from '@/types/mall-types.uts'
+import { supabase } from '@/lib/supabase/client.uts' // 如果路径改变了
+```
+
+### 2. 更新数据库表引用
+
+如果用户表方案改变,需要更新所有相关的外键和查询:
+
+```sql
+-- 原项目中引用 ak_users
+user_id UUID NOT NULL REFERENCES public.ak_users(id)
+
+-- 如果改为独立用户表
+user_id UUID NOT NULL REFERENCES public.mall_users(id)
+```
+
+### 3. 更新环境变量和配置
+
+- Supabase 项目 URL 和 API Key
+- 数据库连接字符串
+- 其他环境相关配置
+
+### 4. 更新文档中的路径引用
+
+检查 `doc_mall/` 下的文档,更新其中的文件路径引用:
+
+```markdown
+# 原文档中的路径
+- `../types/mall-types.uts`
+
+# 新仓库中应该为
+- `types/mall-types.uts`
+```
+
+---
+
+## ✅ 迁移检查清单
+
+使用以下清单确保迁移完整:
+
+- [ ] **文档迁移**
+ - [ ] `doc_mall/` 目录完整复制
+ - [ ] 所有子目录(analysis, database, reports)已迁移
+ - [ ] 文档中的路径引用已更新
+
+- [ ] **前端代码迁移**
+ - [ ] `pages/mall/` 所有页面文件已迁移
+ - [ ] `types/mall-types.uts` 已迁移
+ - [ ] 所有路由配置文件(pages-*.json)已迁移
+ - [ ] 业务需求文档(mall.md)已迁移
+
+- [ ] **依赖文件迁移**
+ - [ ] Supabase 客户端封装已迁移或重新创建
+ - [ ] 必要的工具函数已迁移
+ - [ ] 组件依赖已处理
+
+- [ ] **数据库配置**
+ - [ ] Supabase 项目已创建并配置
+ - [ ] 数据库脚本已执行
+ - [ ] RLS 策略已配置
+ - [ ] 测试数据已插入(可选)
+ - [ ] 用户表依赖已处理
+
+- [ ] **代码适配**
+ - [ ] 所有导入路径已更新
+ - [ ] 数据库表引用已更新(如需要)
+ - [ ] 配置文件已更新
+ - [ ] 环境变量已配置
+
+- [ ] **测试验证**
+ - [ ] 项目可以正常编译
+ - [ ] 数据库连接正常
+ - [ ] 页面可以正常加载
+ - [ ] 核心功能测试通过
+
+---
+
+## 🚨 常见问题和解决方案
+
+### 问题 1: 编译错误 - 找不到类型定义
+
+**错误信息**:
+```
+Cannot find module '@/types/mall-types.uts'
+```
+
+**解决方案**:
+1. 确认 `types/mall-types.uts` 文件已迁移
+2. 检查 TypeScript/UTS 配置中的路径别名设置
+3. 确保 `@/types` 正确映射到 `types/` 目录
+
+### 问题 2: Supabase 连接失败
+
+**错误信息**:
+```
+Failed to connect to Supabase
+```
+
+**解决方案**:
+1. 检查 Supabase 项目 URL 和 API Key 配置
+2. 验证网络连接
+3. 检查 Supabase 项目的状态
+
+### 问题 3: RLS 策略导致权限错误
+
+**错误信息**:
+```
+new row violates row-level security policy
+```
+
+**解决方案**:
+1. 确认已执行所有 RLS 策略脚本
+2. 检查用户认证状态
+3. 验证 RLS 策略的 SELECT/INSERT/UPDATE 权限设置
+
+### 问题 4: 外键约束错误
+
+**错误信息**:
+```
+foreign key constraint "ml_user_profiles_user_id_fkey" fails
+```
+
+**解决方案**:
+1. 如果使用独立用户表,需要更新外键引用
+2. 确保引用的用户记录存在
+3. 检查外键约束的 CASCADE 设置
+
+---
+
+## 📚 参考资料
+
+- [Supabase 官方文档](https://supabase.com/docs)
+- [uni-app-x 官方文档](https://uniapp.dcloud.net.cn/uni-app-x/)
+- [PostgreSQL 官方文档](https://www.postgresql.org/docs/)
+- 项目内部文档:
+ - `doc_mall/TECHNICAL_IMPLEMENTATION.md` - 技术实现详情
+ - `doc_mall/FRONTEND_BACKEND_DEBUGGING.md` - 调试指南
+ - `doc_mall/database/complete_deployment_guide.md` - 数据库部署指南
+
+---
+
+## 📞 迁移支持
+
+如果在迁移过程中遇到问题:
+
+1. **查看文档**:先查看 `doc_mall/` 下的相关文档
+2. **检查日志**:查看编译日志和运行时日志
+3. **数据库验证**:使用 `doc_mall/database/validation_test.sql` 验证数据库状态
+4. **联系开发团队**:提供详细的错误信息和迁移步骤
+
+---
+
+**最后更新**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 完整迁移指南
diff --git a/mall_sql/docs/MIGRATION_SUMMARY.md b/mall_sql/docs/MIGRATION_SUMMARY.md
new file mode 100644
index 00000000..175941ff
--- /dev/null
+++ b/mall_sql/docs/MIGRATION_SUMMARY.md
@@ -0,0 +1,180 @@
+# 📦 doc_mall 迁移工作总览
+
+## ✅ 已完成的准备工作
+
+### 📄 迁移文档
+- ✅ **MIGRATION_GUIDE.md** - 完整的迁移指南(543行)
+- ✅ **MIGRATION_CHECKLIST.md** - 详细的迁移检查清单(255行)
+- ✅ **QUICK_START_MIGRATION.md** - 快速开始指南
+- ✅ **MIGRATION_SUMMARY.md** - 本文件,迁移工作总览
+
+### 🔧 迁移工具
+- ✅ **migrate.ps1** - Windows PowerShell 迁移脚本(191行)
+- ✅ **migrate.sh** - Linux/Mac Bash 迁移脚本(179行)
+
+### 📊 迁移统计
+
+#### 文档和数据库脚本 (`doc_mall/`)
+- **文件数量**: 约 48 个文件
+- **主要目录**:
+ - `analysis/` - 分析文档
+ - `database/` - 数据库脚本(15+ SQL文件,12+ MD文档)
+ - `reports/` - 生成报告
+
+#### 前端页面代码 (`pages/mall/`)
+- **文件数量**: 约 45 个文件
+- **主要目录**:
+ - `admin/` - 管理端页面(5个文件)
+ - `analytics/` - 数据分析端(3个文件)
+ - `consumer/` - 消费者端(9个文件,含订阅功能)
+ - `delivery/` - 配送端(3个文件)
+ - `merchant/` - 商家端(3个文件)
+ - `service/` - 客服端(3个文件)
+ - `nfc/` - NFC支付相关(8个文件,可选)
+
+#### 类型定义文件
+- ✅ `types/mall-types.uts` - 商城系统完整类型定义(必须)
+
+---
+
+## 🚀 执行迁移
+
+### 方式 1: 使用自动化脚本(推荐)
+
+#### Windows 系统
+```powershell
+# 1. 切换到项目目录
+cd D:\datas\hfkj\akmon
+
+# 2. 预览迁移(推荐先执行)
+.\doc_mall\migrate.ps1 -TargetPath "D:\path\to\new-repo" -DryRun
+
+# 3. 执行实际迁移
+.\doc_mall\migrate.ps1 -TargetPath "D:\path\to\new-repo"
+
+# 4. 如果需要包含 Supabase 组件
+.\doc_mall\migrate.ps1 -TargetPath "D:\path\to\new-repo" -CopySupabaseComponents
+
+# 5. 如果需要包含工具函数
+.\doc_mall\migrate.ps1 -TargetPath "D:\path\to\new-repo" -CopyUtils
+```
+
+#### Linux/Mac 系统
+```bash
+# 1. 切换到项目目录
+cd /path/to/akmon
+
+# 2. 添加执行权限
+chmod +x doc_mall/migrate.sh
+
+# 3. 预览迁移
+./doc_mall/migrate.sh /path/to/new-repo --dry-run
+
+# 4. 执行实际迁移
+./doc_mall/migrate.sh /path/to/new-repo
+
+# 5. 包含可选组件
+./doc_mall/migrate.sh /path/to/new-repo --copy-supabase --copy-utils
+```
+
+### 方式 2: 手动迁移
+
+如果不想使用脚本,可以手动执行以下步骤:
+
+```powershell
+# 1. 创建目标目录结构
+New-Item -ItemType Directory -Path "D:\path\to\new-repo\doc_mall\analysis" -Force
+New-Item -ItemType Directory -Path "D:\path\to\new-repo\doc_mall\database" -Force
+New-Item -ItemType Directory -Path "D:\path\to\new-repo\doc_mall\reports" -Force
+New-Item -ItemType Directory -Path "D:\path\to\new-repo\pages\mall" -Force
+New-Item -ItemType Directory -Path "D:\path\to\new-repo\types" -Force
+
+# 2. 复制文件
+Copy-Item -Path "doc_mall\*" -Destination "D:\path\to\new-repo\doc_mall\" -Recurse -Force
+Copy-Item -Path "pages\mall\*" -Destination "D:\path\to\new-repo\pages\mall\" -Recurse -Force
+Copy-Item -Path "types\mall-types.uts" -Destination "D:\path\to\new-repo\types\mall-types.uts" -Force
+```
+
+---
+
+## 📋 迁移后必做事项
+
+### 1. 验证文件完整性
+
+使用检查清单验证所有文件已迁移:
+- 打开 `MIGRATION_CHECKLIST.md`
+- 逐项检查文件是否存在
+- 确认文件数量和大小
+
+### 2. 配置 Supabase
+
+```typescript
+// 创建 config/supabase.config.ts
+export const supabaseConfig = {
+ url: 'https://your-project.supabase.co',
+ anonKey: 'your-anon-key',
+}
+```
+
+### 3. 执行数据库脚本
+
+按照 `database/complete_deployment_guide.md` 执行:
+1. 执行 `complete_mall_database.sql` - 创建数据库结构
+2. 执行 `subscription_rls_policies.sql` - RLS策略
+3. 执行 `subscription_guard_trigger.sql` - 触发器
+4. 执行 `validation_test.sql` - 验证数据库
+
+### 4. 更新代码路径
+
+检查并更新以下导入路径:
+- `@/types/mall-types.uts`
+- `@/components/supadb/*`
+- `@/utils/*`(如需要)
+
+### 5. 测试验证
+
+- [ ] 项目可以编译
+- [ ] 页面可以加载
+- [ ] 数据库连接正常
+- [ ] 核心功能测试通过
+
+---
+
+## 📚 文档结构
+
+```
+doc_mall/
+├── MIGRATION_GUIDE.md # 详细迁移指南 ⭐
+├── MIGRATION_CHECKLIST.md # 迁移检查清单 ⭐
+├── QUICK_START_MIGRATION.md # 快速开始 ⭐
+├── MIGRATION_SUMMARY.md # 本文档
+├── migrate.ps1 # PowerShell 脚本 ⭐
+├── migrate.sh # Bash 脚本 ⭐
+└── [其他原有文档和脚本]
+```
+
+---
+
+## 🎯 下一步
+
+1. **确定目标路径**:决定新仓库的位置
+2. **执行预览**:使用 `-DryRun` 参数预览迁移
+3. **执行迁移**:运行迁移脚本
+4. **验证文件**:使用检查清单验证
+5. **配置环境**:设置 Supabase 和数据库
+6. **测试验证**:确保一切正常工作
+
+---
+
+## 💡 提示
+
+- **预览模式**:强烈建议先使用 `-DryRun` 预览,确认无误后再执行
+- **备份重要数据**:迁移前备份重要文件
+- **分批迁移**:如果文件很多,可以分批测试迁移
+- **记录问题**:在 `MIGRATION_CHECKLIST.md` 中记录遇到的问题
+
+---
+
+**创建时间**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 迁移工具已准备就绪,可以开始迁移
diff --git a/mall_sql/docs/MODULE_ANALYSIS.md b/mall_sql/docs/MODULE_ANALYSIS.md
new file mode 100644
index 00000000..92d01596
--- /dev/null
+++ b/mall_sql/docs/MODULE_ANALYSIS.md
@@ -0,0 +1,710 @@
+# 📊 doc_mall 模块深度分析报告
+
+## 📋 目录
+1. [模块概述](#模块概述)
+2. [数据库存储方式](#数据库存储方式)
+3. [数据库交互方式](#数据库交互方式)
+4. [开发模式](#开发模式)
+5. [开发流程](#开发流程)
+6. [技术架构总结](#技术架构总结)
+
+---
+
+## 一、模块概述
+
+### 1.1 模块定位
+`doc_mall` 是一个**电商商城系统模块**,属于"梅州市智慧医养数字赋能平台"中的医养商城子系统。该模块提供医疗用品、保健产品、医养结合服务的在线销售平台。
+
+### 1.2 核心功能
+- 🛒 **商品管理**: 商品展示、分类、品牌、多规格SKU
+- 🏪 **店铺管理**: 商家店铺信息、认证、营业管理
+- 📦 **订单系统**: 订单创建、支付、发货、收货、评价全流程
+- 🛍️ **购物车**: 商品选择、数量管理
+- 🎫 **营销系统**: 优惠券、收藏、浏览历史、搜索记录
+- 🚚 **配送管理**: 配送员管理、配送任务、实时位置跟踪
+- ⭐ **评价系统**: 商品评价、商家回复、匿名评价
+
+### 1.3 技术栈
+- **数据库**: PostgreSQL 13+ / Supabase
+- **前端框架**: uni-app-x (UTS Android 兼容)
+- **认证系统**: Supabase Auth
+- **API方式**: Supabase REST API + PostgREST
+
+---
+
+## 二、数据库存储方式
+
+### 2.1 数据库类型
+**PostgreSQL + Supabase 兼容架构**
+
+- **主数据库**: PostgreSQL 13+
+- **云服务**: Supabase (PostgreSQL 托管 + 扩展服务)
+- **兼容性**: 同时支持标准 PostgreSQL 和 Supabase 环境
+
+### 2.2 存储架构设计
+
+#### 2.2.1 表命名规范
+- **前缀策略**: 所有商城表使用 `ml_` 前缀 (mall)
+- **复用策略**: 仅复用 `ak_users` 用户主表,其他表全部独立
+- **命名示例**:
+ - `ml_products` - 商品表
+ - `ml_orders` - 订单表
+ - `ml_user_profiles` - 用户扩展表
+
+#### 2.2.2 数据表结构 (21张表)
+
+| 功能模块 | 表数量 | 主要表名 | 说明 |
+| ------------ | ------ | ---------------------------------------------------------------------------------- | --------------------------- |
+| **用户管理** | 2张 | `ml_user_profiles`, `ml_user_addresses` | 用户扩展信息、地址管理 |
+| **商品管理** | 5张 | `ml_products`, `ml_product_skus`, `ml_categories`, `ml_brands`, `ml_product_specs` | 商品、SKU、分类、品牌、规格 |
+| **店铺管理** | 1张 | `ml_shops` | 商家店铺信息 |
+| **订单管理** | 2张 | `ml_orders`, `ml_order_items` | 订单主表、订单商品明细 |
+| **购物车** | 1张 | `ml_shopping_cart` | 购物车商品 |
+| **营销系统** | 2张 | `ml_coupon_templates`, `ml_user_coupons` | 优惠券模板、用户优惠券 |
+| **配送管理** | 2张 | `ml_delivery_drivers`, `ml_delivery_tasks` | 配送员、配送任务 |
+| **评价系统** | 1张 | `ml_product_reviews` | 商品评价 |
+| **用户行为** | 3张 | `ml_user_favorites`, `ml_browse_history`, `ml_search_history` | 收藏、浏览历史、搜索记录 |
+| **系统配置** | 2张 | `ml_system_configs`, `ml_regions` | 系统配置、地区数据 |
+
+#### 2.2.3 核心设计特性
+
+**1. UUID 主键设计**
+```sql
+id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
+```
+- 所有表使用 UUID 作为主键
+- 支持分布式系统,避免ID冲突
+- 使用 `uuid-ossp` 扩展生成
+
+**2. SEO 友好的自增ID (cid)**
+```sql
+cid SERIAL UNIQUE NOT NULL -- SEO友好的自增ID
+```
+- 为主要表添加 `cid` 字段用于URL生成
+- 提供简洁、语义化的URL路径
+- 例如: `/product/123` 而不是 `/product/uuid-string`
+
+**3. JSONB 灵活数据存储**
+```sql
+image_urls JSONB DEFAULT '[]'
+preferences JSONB DEFAULT '{}'
+specifications JSONB DEFAULT '{}'
+```
+- 使用 JSONB 存储灵活的JSON数据
+- 支持高效查询和索引 (GIN索引)
+- 适合存储数组、对象等非结构化数据
+
+**4. 时间戳字段**
+```sql
+created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+```
+- 标准的时间戳字段
+- 自动记录创建和更新时间
+- 通过触发器自动更新 `updated_at`
+
+**5. 外键约束**
+```sql
+user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
+```
+- 完整的引用完整性约束
+- 级联删除保证数据一致性
+- 复用 `ak_users` 表实现单点登录
+
+### 2.3 索引优化策略
+
+#### 2.3.1 索引类型
+- **主键索引**: 自动创建 (UUID)
+- **唯一索引**: 防止数据重复 (`product_code`, `order_no` 等)
+- **外键索引**: 30+ 个优化查询索引
+- **复合索引**: 针对常用查询组合
+- **GIN 索引**: JSON 和数组字段的高效查询
+
+#### 2.3.2 索引示例
+```sql
+-- 商品表索引
+CREATE INDEX idx_ml_products_merchant ON public.ml_products(merchant_id);
+CREATE INDEX idx_ml_products_category ON public.ml_products(category_id);
+CREATE INDEX idx_ml_products_status ON public.ml_products(status);
+CREATE INDEX idx_ml_products_cid ON public.ml_products(cid); -- SEO查询
+
+-- 订单表索引
+CREATE INDEX idx_ml_orders_user ON public.ml_orders(user_id);
+CREATE INDEX idx_ml_orders_merchant ON public.ml_orders(merchant_id);
+CREATE INDEX idx_ml_orders_status ON public.ml_orders(order_status);
+CREATE INDEX idx_ml_orders_created ON public.ml_orders(created_at DESC);
+
+-- JSONB GIN索引
+CREATE INDEX idx_ml_products_images_gin ON public.ml_products USING GIN(image_urls);
+```
+
+### 2.4 数据安全策略 (RLS)
+
+#### 2.4.1 Row Level Security (行级安全)
+- **启用方式**: 所有表启用 RLS 策略
+- **认证方式**: 使用 Supabase `auth.uid()` 进行身份验证
+- **权限模型**: 基于用户角色的细粒度权限控制
+
+#### 2.4.2 RLS 策略示例
+```sql
+-- 用户只能访问自己的数据
+CREATE POLICY ml_user_profiles_select_policy ON public.ml_user_profiles
+ FOR SELECT USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ );
+
+-- 商品公开查看,商家管理
+CREATE POLICY ml_products_select_policy ON public.ml_products
+ FOR SELECT USING (status = 1); -- 所有人可查看已上架商品
+
+CREATE POLICY ml_products_update_policy ON public.ml_products
+ FOR UPDATE USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id)
+ ); -- 商家只能管理自己的商品
+
+-- 订单权限:用户和商家都可查看
+CREATE POLICY ml_orders_select_policy ON public.ml_orders
+ FOR SELECT USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ OR
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id)
+ );
+```
+
+### 2.5 触发器自动化
+
+#### 2.5.1 触发器功能
+| 触发器名称 | 功能 | 应用表 |
+| ------------------------------- | ---------------- | ------------------- |
+| `update_updated_at_column` | 自动更新时间戳 | 8张主要表 |
+| `ensure_single_default_address` | 确保唯一默认地址 | `ml_user_addresses` |
+| `update_product_stock` | 自动更新商品库存 | `ml_product_skus` |
+| `handle_order_status_change` | 订单状态变更处理 | `ml_orders` |
+
+#### 2.5.2 触发器示例
+```sql
+-- 自动更新 updated_at
+CREATE TRIGGER trigger_ml_user_profiles_updated_at
+ BEFORE UPDATE ON public.ml_user_profiles
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+-- 确保唯一默认地址
+CREATE TRIGGER trigger_ensure_single_default_address
+ BEFORE INSERT OR UPDATE ON public.ml_user_addresses
+ FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address();
+```
+
+### 2.6 数据库函数
+
+#### 2.6.1 业务函数
+| 函数名称 | 功能描述 | 返回类型 |
+| ------------------------------- | ---------------- | -------- |
+| `generate_order_no()` | 生成唯一订单号 | TEXT |
+| `generate_coupon_code()` | 生成优惠券码 | TEXT |
+| `get_user_default_address()` | 获取用户默认地址 | TABLE |
+| `is_verified_merchant()` | 检查是否认证商家 | BOOLEAN |
+| `calculate_cart_total()` | 计算购物车总金额 | DECIMAL |
+| `get_product_available_stock()` | 获取商品可用库存 | INTEGER |
+
+#### 2.6.2 函数示例
+```sql
+-- 生成订单号
+CREATE OR REPLACE FUNCTION public.generate_order_no()
+RETURNS TEXT AS $$
+BEGIN
+ RETURN 'ORD' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(NEXTVAL('order_no_seq')::TEXT, 6, '0');
+END;
+$$ LANGUAGE plpgsql;
+
+-- 计算购物车总金额
+CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID)
+RETURNS DECIMAL AS $$
+DECLARE
+ total DECIMAL(12,2);
+BEGIN
+ SELECT COALESCE(SUM(c.quantity * p.base_price), 0)
+ INTO total
+ FROM ml_shopping_cart c
+ JOIN ml_products p ON c.product_id = p.id
+ WHERE c.user_id = p_user_id AND c.selected = TRUE;
+ RETURN total;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+### 2.7 视图设计
+
+#### 2.7.1 业务视图
+| 视图名称 | 功能描述 |
+| ------------------------- | -------------------------------------- |
+| `ml_users_view` | 商城用户完整信息视图 |
+| `ml_products_detail_view` | 商品详情视图(含分类、品牌、店铺信息) |
+| `ml_orders_detail_view` | 订单详情视图(含客户、商家、状态信息) |
+
+#### 2.7.2 视图示例
+```sql
+-- 商品详情视图
+CREATE VIEW ml_products_detail_view AS
+SELECT
+ p.*,
+ c.name as category_name,
+ b.name as brand_name,
+ s.shop_name,
+ s.shop_logo
+FROM ml_products p
+LEFT JOIN ml_categories c ON p.category_id = c.id
+LEFT JOIN ml_brands b ON p.brand_id = b.id
+LEFT JOIN ml_shops s ON p.merchant_id = s.merchant_id;
+```
+
+---
+
+## 三、数据库交互方式
+
+### 3.1 API 架构
+
+#### 3.1.1 Supabase REST API
+- **基础URL**: `https://your-project.supabase.co/rest/v1/`
+- **认证方式**: JWT Token (Bearer Token)
+- **API Key**: `apikey` Header
+- **协议**: HTTP/HTTPS RESTful API
+
+#### 3.1.2 PostgREST 自动生成
+- Supabase 基于 PostgREST 自动生成 REST API
+- 每个表自动获得 CRUD 接口
+- 支持复杂查询、过滤、排序、分页
+
+### 3.2 前端交互方式
+
+#### 3.2.1 Supabase 客户端封装
+项目使用自定义的 Supabase 客户端封装 (`components/supadb/aksupa.uts`):
+
+```typescript
+// 客户端初始化
+const supaClient = new AkSupa({
+ baseUrl: 'https://your-project.supabase.co',
+ apikey: 'your-anon-key'
+});
+
+// 查询数据
+const response = await supaClient.select('ml_products', null, {
+ columns: 'id,name,base_price,main_image_url',
+ limit: 20,
+ order: 'created_at.desc'
+});
+
+// 插入数据
+const result = await supaClient.insert('ml_orders', {
+ user_id: userId,
+ merchant_id: merchantId,
+ total_amount: 100.00,
+ order_status: 1
+});
+
+// 更新数据
+await supaClient.update('ml_products', { id: productId }, {
+ status: 2,
+ updated_at: new Date().toISOString()
+});
+
+// 删除数据
+await supaClient.delete('ml_user_favorites', { id: favoriteId });
+```
+
+#### 3.2.2 查询选项支持
+```typescript
+type AkSupaSelectOptions = {
+ columns?: string; // 选择字段: 'id,name,price'
+ limit?: number; // 限制数量
+ order?: string; // 排序: 'created_at.desc'
+ rangeFrom?: number; // 分页起始
+ rangeTo?: number; // 分页结束
+ count?: string; // 计数方式: 'exact'|'planned'|'estimated'
+ single?: boolean; // 单条记录
+ head?: boolean; // 仅返回元数据
+}
+```
+
+#### 3.2.3 过滤条件支持
+```typescript
+// 简单过滤
+const filter = {
+ status: 1,
+ merchant_id: userId
+};
+
+// 复杂过滤 (PostgREST 操作符)
+const filter = {
+ base_price: { gte: 100, lte: 500 }, // 范围查询
+ name: { ilike: '%商品%' }, // 模糊查询
+ category_id: { in: [id1, id2, id3] }, // IN 查询
+ created_at: { gte: '2024-01-01' } // 时间范围
+};
+```
+
+### 3.3 实时数据同步
+
+#### 3.3.1 Supabase Realtime
+- 支持 WebSocket 实时数据同步
+- 表变更自动推送到客户端
+- 适用于订单状态更新、库存变化等场景
+
+```typescript
+// 实时订阅订单状态
+supaClient.realtime.subscribe('ml_orders', {
+ filter: `id=eq.${orderId}`,
+ event: 'UPDATE',
+ callback: (payload) => {
+ console.log('订单状态更新:', payload);
+ }
+});
+```
+
+### 3.4 存储过程调用 (RPC)
+
+#### 3.4.1 数据库函数调用
+```typescript
+// 调用数据库函数
+const result = await supaClient.rpc('calculate_cart_total', {
+ p_user_id: userId
+});
+
+// 调用生成订单号函数
+const orderNo = await supaClient.rpc('generate_order_no');
+```
+
+### 3.5 认证与权限
+
+#### 3.5.1 Supabase Auth 集成
+```typescript
+// 用户登录
+const { data, error } = await supaClient.auth.signInWithPassword({
+ email: 'user@example.com',
+ password: 'password'
+});
+
+// 获取当前用户
+const user = await supaClient.auth.getUser();
+
+// Token 自动附加到请求头
+// Authorization: Bearer
+```
+
+#### 3.5.2 RLS 自动生效
+- 前端请求自动携带 JWT Token
+- RLS 策略根据 `auth.uid()` 自动过滤数据
+- 用户只能访问被授权的数据
+
+### 3.6 数据迁移与初始化
+
+#### 3.6.1 数据库脚本执行
+```bash
+# PostgreSQL 直接执行
+psql -h localhost -U postgres -d your_database -f complete_mall_database.sql
+
+# Supabase Dashboard 执行
+# 1. 登录 Supabase Dashboard
+# 2. 进入 SQL Editor
+# 3. 复制粘贴 SQL 脚本
+# 4. 执行脚本
+```
+
+#### 3.6.2 模拟数据插入
+```bash
+# 先执行主数据库脚本
+psql -f complete_mall_database.sql
+
+# 再执行模拟数据
+psql -f mock_data_insert.sql
+```
+
+---
+
+## 四、开发模式
+
+### 4.1 架构模式
+
+#### 4.1.1 BaaS (Backend as a Service) 模式
+- **特点**: 使用 Supabase 作为后端服务
+- **优势**:
+ - 无需自建后端服务器
+ - 自动生成 REST API
+ - 内置认证、权限、实时同步
+ - 减少后端开发工作量
+
+#### 4.1.2 数据库优先 (Database-First) 模式
+- **流程**: 先设计数据库 → 自动生成 API → 前端调用
+- **优势**:
+ - 数据结构清晰
+ - API 自动生成,减少手写代码
+ - 类型安全 (通过 TypeScript/UTS 类型定义)
+
+### 4.2 前端开发模式
+
+#### 4.2.1 uni-app-x 框架
+- **平台**: uni-app-x (跨平台框架)
+- **语言**: UTS (UniApp TypeScript)
+- **兼容性**: 严格遵循 UTS Android 语法规范
+
+#### 4.2.2 类型定义驱动
+```typescript
+// types/mall-types.uts
+export type ProductType = {
+ id: string
+ merchant_id: string
+ category_id: string
+ name: string
+ description: string | null
+ images: Array
+ price: number
+ stock: number
+ status: number
+ created_at: string
+}
+```
+
+#### 4.2.3 组件化开发
+- 页面组件: `pages/mall/`
+- 业务组件: `components/`
+- 工具类: `utils/`
+- 类型定义: `types/`
+
+### 4.3 数据访问模式
+
+#### 4.3.1 服务层封装
+```typescript
+// 商品服务
+class ProductService {
+ async getProducts(filters: any) {
+ return await supaClient.select('ml_products', filters, {
+ limit: 20,
+ order: 'created_at.desc'
+ });
+ }
+
+ async getProductById(id: string) {
+ return await supaClient.select('ml_products', { id }, {
+ single: true
+ });
+ }
+}
+```
+
+#### 4.3.2 响应式数据绑定
+- 使用 uni-app-x 的数据绑定机制
+- 结合 Supabase Realtime 实现实时更新
+- 状态管理通过组件状态或全局状态
+
+### 4.4 安全模式
+
+#### 4.4.1 多层安全防护
+1. **网络层**: HTTPS 加密传输
+2. **认证层**: Supabase Auth JWT Token
+3. **权限层**: RLS 行级安全策略
+4. **应用层**: 前端数据验证
+
+#### 4.4.2 最小权限原则
+- 用户只能访问自己的数据
+- 商家只能管理自己的商品和订单
+- 公开数据 (商品列表) 所有人可查看
+
+---
+
+## 五、开发流程
+
+### 5.1 数据库设计流程
+
+#### 5.1.1 需求分析
+1. **业务需求梳理**
+ - 商品管理需求
+ - 订单流程需求
+ - 用户角色需求
+ - 营销功能需求
+
+2. **数据模型设计**
+ - 实体识别 (商品、订单、用户等)
+ - 关系设计 (一对多、多对多)
+ - 字段设计 (类型、约束、索引)
+
+#### 5.1.2 数据库脚本编写
+```sql
+-- 1. 创建表结构
+CREATE TABLE ml_products (...);
+
+-- 2. 创建索引
+CREATE INDEX idx_ml_products_merchant ON ml_products(merchant_id);
+
+-- 3. 创建触发器
+CREATE TRIGGER trigger_update_updated_at ...;
+
+-- 4. 创建 RLS 策略
+CREATE POLICY ml_products_select_policy ...;
+
+-- 5. 创建函数
+CREATE FUNCTION generate_order_no() ...;
+
+-- 6. 创建视图
+CREATE VIEW ml_products_detail_view AS ...;
+```
+
+#### 5.1.3 数据库部署
+1. **环境准备**
+ - PostgreSQL 13+ 或 Supabase 项目
+ - 数据库用户权限配置
+
+2. **脚本执行**
+ ```bash
+ # 执行主数据库脚本
+ psql -f complete_mall_database.sql
+
+ # 执行模拟数据 (可选)
+ psql -f mock_data_insert.sql
+ ```
+
+3. **验证测试**
+ ```bash
+ # 执行验证脚本
+ psql -f validation_test.sql
+ ```
+
+### 5.2 前端开发流程
+
+#### 5.2.1 类型定义
+```typescript
+// 1. 定义 TypeScript/UTS 类型
+export type ProductType = {
+ id: string
+ name: string
+ price: number
+ // ...
+}
+```
+
+#### 5.2.2 API 服务封装
+```typescript
+// 2. 封装 API 调用
+class MallAPI {
+ async getProducts() {
+ return await supaClient.select('ml_products', ...);
+ }
+}
+```
+
+#### 5.2.3 页面开发
+```vue
+
+
+
+
+ {{ product.name }}
+
+
+
+
+
+```
+
+#### 5.2.4 测试验证
+- 功能测试: 验证业务流程
+- 权限测试: 验证 RLS 策略
+- 性能测试: 验证查询性能
+
+### 5.3 迭代开发流程
+
+#### 5.3.1 功能迭代
+1. **需求变更** → 数据库迁移脚本
+2. **表结构更新** → `mall_alter_upgrade.sql`
+3. **数据迁移** → 迁移脚本执行
+4. **前端适配** → 类型定义更新 → 页面更新
+
+#### 5.3.2 版本管理
+- **数据库版本**: 通过迁移脚本管理
+- **代码版本**: Git 版本控制
+- **文档版本**: Markdown 文档同步更新
+
+### 5.4 部署流程
+
+#### 5.4.1 开发环境
+1. 本地 PostgreSQL 或 Supabase 本地实例
+2. 执行数据库脚本
+3. 配置环境变量
+4. 启动前端开发服务器
+
+#### 5.4.2 生产环境
+1. **Supabase 云服务部署**
+ - 创建 Supabase 项目
+ - 执行数据库脚本
+ - 配置环境变量
+ - 部署前端应用
+
+2. **自建 PostgreSQL 部署**
+ - 搭建 PostgreSQL 服务器
+ - 执行数据库脚本
+ - 配置 Nginx 反向代理 (如需要)
+ - 部署前端应用
+
+---
+
+## 六、技术架构总结
+
+### 6.1 技术栈总结
+
+| 层级 | 技术 | 说明 |
+| ------------ | -------------- | ----------------- |
+| **数据库** | PostgreSQL 13+ | 关系型数据库 |
+| **BaaS平台** | Supabase | 后端即服务 |
+| **API层** | PostgREST | 自动生成 REST API |
+| **认证** | Supabase Auth | JWT Token 认证 |
+| **前端框架** | uni-app-x | 跨平台框架 |
+| **开发语言** | UTS | UniApp TypeScript |
+| **类型系统** | TypeScript/UTS | 类型安全 |
+
+### 6.2 架构特点
+
+#### ✅ 优势
+1. **开发效率高**: BaaS 模式减少后端开发
+2. **类型安全**: 完整的类型定义系统
+3. **自动API**: PostgREST 自动生成 REST API
+4. **权限完善**: RLS 行级安全策略
+5. **实时同步**: Supabase Realtime 支持
+6. **扩展性强**: 数据库函数、触发器、视图支持
+
+#### ⚠️ 注意事项
+1. **Supabase 依赖**: 深度依赖 Supabase 生态
+2. **学习曲线**: 需要熟悉 PostgreSQL 和 Supabase
+3. **成本考虑**: Supabase 云服务有使用限制
+4. **迁移成本**: 如需迁移到其他平台,成本较高
+
+### 6.3 最佳实践
+
+1. **数据库设计优先**: 先设计好数据库结构
+2. **类型定义同步**: 保持数据库和类型定义同步
+3. **RLS 策略完善**: 确保数据安全
+4. **索引优化**: 针对查询场景优化索引
+5. **文档完善**: 保持文档与代码同步
+
+---
+
+## 📚 相关文档
+
+- [数据库创建报告](./database/database_creation_report.md)
+- [数据库语法修正报告](./database/database_syntax_fix_report.md)
+- [完整部署指南](./database/complete_deployment_guide.md)
+- [用户表复用方案](./user_reuse_summary.md)
+- [前后端联调指南](./FRONTEND_BACKEND_DEBUGGING.md) ⭐ **新增**
+- [模块README](./README.md)
+
+---
+
+**生成时间**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 完整分析报告
diff --git a/mall_sql/docs/QUICK_START_MIGRATION.md b/mall_sql/docs/QUICK_START_MIGRATION.md
new file mode 100644
index 00000000..e26d3078
--- /dev/null
+++ b/mall_sql/docs/QUICK_START_MIGRATION.md
@@ -0,0 +1,111 @@
+# ⚡ 快速开始迁移
+
+## 🚀 快速执行迁移
+
+### Windows (PowerShell)
+
+#### 1. 预览模式(推荐先执行)
+```powershell
+cd doc_mall
+.\migrate.ps1 -TargetPath "D:\path\to\new-repo" -DryRun
+```
+
+#### 2. 执行迁移
+```powershell
+cd doc_mall
+.\migrate.ps1 -TargetPath "D:\path\to\new-repo"
+```
+
+#### 3. 包含 Supabase 组件
+```powershell
+.\migrate.ps1 -TargetPath "D:\path\to\new-repo" -CopySupabaseComponents
+```
+
+#### 4. 包含工具函数
+```powershell
+.\migrate.ps1 -TargetPath "D:\path\to\new-repo" -CopyUtils
+```
+
+### Linux/Mac (Bash)
+
+#### 1. 预览模式
+```bash
+cd doc_mall
+chmod +x migrate.sh
+./migrate.sh /path/to/new-repo --dry-run
+```
+
+#### 2. 执行迁移
+```bash
+./migrate.sh /path/to/new-repo
+```
+
+#### 3. 包含可选组件
+```bash
+./migrate.sh /path/to/new-repo --copy-supabase --copy-utils
+```
+
+---
+
+## 📋 迁移后的必要步骤
+
+### 1. 检查迁移结果
+```powershell
+# 检查目标目录
+ls D:\path\to\new-repo\doc_mall
+ls D:\path\to\new-repo\pages\mall
+ls D:\path\to\new-repo\types
+```
+
+### 2. 配置 Supabase
+
+创建配置文件 `config/supabase.config.ts`:
+```typescript
+export const supabaseConfig = {
+ url: 'https://your-project.supabase.co',
+ anonKey: 'your-anon-key',
+}
+```
+
+### 3. 执行数据库脚本
+
+#### 方式 1: Supabase Dashboard
+1. 登录 Supabase Dashboard
+2. 进入 SQL Editor
+3. 复制 `doc_mall/database/complete_mall_database.sql` 内容
+4. 执行脚本
+
+#### 方式 2: psql 命令行
+```bash
+psql -h your-host -U postgres -d your_database -f doc_mall/database/complete_mall_database.sql
+```
+
+### 4. 验证数据库
+```bash
+psql -h your-host -U postgres -d your_database -f doc_mall/database/validation_test.sql
+```
+
+### 5. 插入测试数据(可选)
+```bash
+psql -h your-host -U postgres -d your_database -f doc_mall/database/mock_data_insert.sql
+```
+
+---
+
+## ✅ 验证清单
+
+- [ ] 文件已复制到目标目录
+- [ ] 目录结构正确
+- [ ] Supabase 配置已更新
+- [ ] 数据库脚本已执行
+- [ ] 数据库验证通过
+- [ ] 项目可以编译
+- [ ] 页面可以正常加载
+
+---
+
+## 📚 更多信息
+
+- 详细迁移指南: [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md)
+- 完整检查清单: [MIGRATION_CHECKLIST.md](./MIGRATION_CHECKLIST.md)
+- 技术实现文档: [TECHNICAL_IMPLEMENTATION.md](./TECHNICAL_IMPLEMENTATION.md)
diff --git a/mall_sql/docs/README.md b/mall_sql/docs/README.md
new file mode 100644
index 00000000..d3b85b94
--- /dev/null
+++ b/mall_sql/docs/README.md
@@ -0,0 +1,108 @@
+# 🛍️ 商城系统模块 (Mall System Module)
+
+本目录包含完整的商城系统模块,已从主项目中独立出来,可作为独立仓库使用。
+
+## 📁 目录结构
+
+```
+mall/
+├── doc_mall/ # 文档和数据库脚本
+│ ├── database/ # 数据库脚本目录
+│ ├── analysis/ # 分析文档目录
+│ ├── reports/ # 生成报告目录
+│ └── *.md # 各类文档和迁移指南
+├── pages/ # 前端页面代码
+│ └── mall/ # 商城页面
+│ ├── admin/ # 管理端页面
+│ ├── analytics/ # 数据分析端页面
+│ ├── consumer/ # 消费者端页面
+│ ├── delivery/ # 配送端页面
+│ ├── merchant/ # 商家端页面
+│ ├── service/ # 客服端页面
+│ └── nfc/ # NFC支付页面
+└── types/ # 类型定义
+ └── mall-types.uts # 商城系统类型定义
+```
+
+## 📊 迁移统计
+
+- **文档和数据库脚本**: 48+ 个文件 (`doc_mall/`)
+- **前端页面代码**: 45+ 个文件 (`pages/mall/`)
+- **类型定义**: 1 个文件 (`types/mall-types.uts`)
+
+## 🚀 快速开始
+
+### 1. 查看迁移指南
+
+- **完整迁移指南**: [doc_mall/MIGRATION_GUIDE.md](./doc_mall/MIGRATION_GUIDE.md)
+- **迁移检查清单**: [doc_mall/MIGRATION_CHECKLIST.md](./doc_mall/MIGRATION_CHECKLIST.md)
+- **快速开始**: [doc_mall/QUICK_START_MIGRATION.md](./doc_mall/QUICK_START_MIGRATION.md)
+
+### 2. 配置数据库
+
+执行数据库脚本创建表结构:
+
+```bash
+# 方式1: 通过 Supabase Dashboard SQL Editor
+# 打开 doc_mall/database/complete_mall_database.sql 并执行
+
+# 方式2: 通过 psql 命令行
+psql -h localhost -U postgres -d your_database -f doc_mall/database/complete_mall_database.sql
+```
+
+### 3. 配置 Supabase 连接
+
+创建配置文件,设置 Supabase 项目 URL 和 API Key。
+
+### 4. 更新导入路径
+
+检查并更新代码中的导入路径,确保指向正确的位置。
+
+## 📚 核心文档
+
+### 技术文档
+- [技术实现拆解](./doc_mall/TECHNICAL_IMPLEMENTATION.md) - 详细的技术实现说明
+- [模块深度分析](./doc_mall/MODULE_ANALYSIS.md) - 模块架构和设计理念
+- [前后端联调指南](./doc_mall/FRONTEND_BACKEND_DEBUGGING.md) - 开发调试指南
+
+### 数据库文档
+- [完整部署指南](./doc_mall/database/complete_deployment_guide.md) - 数据库部署步骤
+- [快速部署指南](./doc_mall/database/deployment_guide.md) - 快速部署方法
+- [数据库创建报告](./doc_mall/database/database_creation_report.md) - 数据库结构说明
+
+## 🔧 迁移到新仓库
+
+如果你需要将本模块迁移到一个完全独立的 Git 仓库,可以使用提供的迁移脚本:
+
+### Windows (PowerShell)
+```powershell
+cd doc_mall
+.\migrate.ps1 -TargetPath "D:\path\to\new-repo"
+```
+
+### Linux/Mac (Bash)
+```bash
+cd doc_mall
+chmod +x migrate.sh
+./migrate.sh /path/to/new-repo
+```
+
+详细步骤请参考 [MIGRATION_GUIDE.md](./doc_mall/MIGRATION_GUIDE.md)。
+
+## 📝 注意事项
+
+1. **用户表依赖**: 商城系统依赖 `ak_users` 用户表,迁移时需要确定处理方案(独立表/复用表/API服务)
+2. **Supabase 配置**: 需要配置 Supabase 项目连接信息
+3. **路径更新**: 迁移后需要更新代码中的导入路径
+4. **数据库脚本**: 需要按顺序执行数据库脚本
+
+## 📞 支持
+
+- 查看文档: 参考 `doc_mall/` 目录下的相关文档
+- 迁移问题: 参考 [MIGRATION_GUIDE.md](./doc_mall/MIGRATION_GUIDE.md) 中的常见问题部分
+
+---
+
+**迁移日期**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 已独立迁移到 mall/ 目录
diff --git a/mall_sql/docs/README_subscription_consumer.md b/mall_sql/docs/README_subscription_consumer.md
new file mode 100644
index 00000000..f7728a8e
--- /dev/null
+++ b/mall_sql/docs/README_subscription_consumer.md
@@ -0,0 +1,16 @@
+# 软件订阅(consumer)
+
+入口:
+- 用户中心 -> 软件订阅
+
+页面:
+- plan-list.uvue:展示可用订阅方案(ml_subscription_plans)
+- plan-detail.uvue:展示某个订阅方案详情
+- subscribe-checkout.uvue:确认支付并创建订阅(写入 ml_user_subscriptions)
+
+依赖表(示例名称,可按实际后端调整):
+- ml_subscription_plans(id, plan_code, name, description, features jsonb, price numeric, currency text, billing_period text, trial_days int, is_active bool, sort_order int, created_at, updated_at)
+- ml_user_subscriptions(id, user_id, plan_id, status text, start_date timestamptz, end_date timestamptz, next_billing_date timestamptz, auto_renew bool, cancel_at_period_end bool, metadata jsonb, created_at, updated_at)
+
+注意:
+- 本实现使用 uni-app-x 兼容组件与 supaClient。实际支付请替换为你们的支付网关,并在后端完成对账与签名校验。
diff --git a/mall_sql/docs/ROLE_FIELD_FIX_REPORT.md b/mall_sql/docs/ROLE_FIELD_FIX_REPORT.md
new file mode 100644
index 00000000..1faa1687
--- /dev/null
+++ b/mall_sql/docs/ROLE_FIELD_FIX_REPORT.md
@@ -0,0 +1,151 @@
+# 角色字段统一修复完成报告
+
+## 🔧 问题修复
+
+### 问题1:重复的角色字段
+**原问题**:`ml_user_profiles` 表中存在重复的 `role` 字段,与 `ak_users.role` 重复。
+**解决方案**:删除 `ml_user_profiles.role` 字段,统一使用 `ak_users.role`。
+
+### 问题2:变量类型错误
+**原问题**:订单生成代码中 `merchant_rec` 变量类型错误,导致数据类型不匹配。
+**解决方案**:将 `merchant_rec RECORD` 改为 `merchant_id UUID`。
+
+## ✅ 已修复的文件
+
+### 1. complete_mall_database.sql
+- ❌ 删除:`ml_user_profiles.role` 字段定义
+- ❌ 删除:相关约束 `chk_ml_user_role`
+- ❌ 删除:相关索引 `idx_ml_user_profiles_role`
+- ❌ 删除:相关注释
+- ✅ 更新:`is_verified_merchant()` 函数,从 `ak_users` 表获取角色
+- ✅ 更新:`ml_users_view` 视图,使用 `u.role` 替代 `p.role`
+- ✅ 更新:插入语句,移除 `role` 字段
+
+### 2. mock_data_insert.sql
+- ✅ 更新:用户档案插入语句,移除 `role` 字段
+- ✅ 更新:冲突处理语句,移除 `role` 字段
+- ✅ 修复:订单生成代码中的变量类型错误
+
+### 3. role_field_cleanup.sql (新增)
+- ✅ 创建:专门的角色字段清理脚本
+- ✅ 功能:检查并清理重复的角色字段
+- ✅ 功能:数据迁移和一致性检查
+- ✅ 功能:更新相关函数和视图
+
+## 📊 当前角色字段设计
+
+### 唯一的角色存储位置
+```sql
+-- ak_users 表 - 唯一的角色字段存储位置
+CREATE TABLE public.ak_users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ role TEXT DEFAULT 'customer' NOT NULL,
+ -- 其他字段...
+
+ CONSTRAINT chk_ak_users_role
+ CHECK (role IN ('customer', 'merchant', 'delivery', 'service', 'admin'))
+);
+```
+
+### 相关表关联
+```sql
+-- ml_user_profiles 表 - 不再包含 role 字段
+CREATE TABLE public.ml_user_profiles (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id),
+ status INTEGER DEFAULT 1 NOT NULL,
+ -- 其他扩展信息字段...
+);
+```
+
+### 获取用户角色
+```sql
+-- 通过关联查询获取角色信息
+SELECT u.role, p.real_name, p.credit_score
+FROM ak_users u
+LEFT JOIN ml_user_profiles p ON u.id = p.user_id
+WHERE u.id = 'user-uuid';
+```
+
+## 🔍 验证步骤
+
+### 1. 字段检查
+```sql
+-- 检查是否还有重复的 role 字段
+SELECT
+ table_name,
+ column_name,
+ data_type
+FROM information_schema.columns
+WHERE column_name = 'role'
+AND table_name IN ('ak_users', 'ml_user_profiles');
+
+-- 预期结果:只有 ak_users.role
+```
+
+### 2. 约束检查
+```sql
+-- 检查角色约束
+SELECT constraint_name, table_name
+FROM information_schema.check_constraints
+WHERE constraint_name LIKE '%role%';
+
+-- 预期结果:只有 chk_ak_users_role
+```
+
+### 3. 功能检查
+```sql
+-- 测试角色相关函数
+SELECT get_user_role('test-user-id');
+SELECT check_user_permission('test-user-id', ARRAY['admin']);
+SELECT * FROM vw_role_statistics;
+```
+
+## 🎯 优势总结
+
+### 1. 数据一致性
+- ✅ 单一数据源:角色信息只存储在一个地方
+- ✅ 避免同步问题:不会出现两个表角色不一致的情况
+- ✅ 数据完整性:通过外键约束保证关联关系
+
+### 2. 代码简洁性
+- ✅ 查询简化:直接从 `ak_users` 获取角色信息
+- ✅ 维护容易:只需要维护一个角色字段
+- ✅ 扩展性好:新增角色类型只需要修改一个约束
+
+### 3. 性能优化
+- ✅ 减少JOIN:在只需要角色信息时无需关联 `ml_user_profiles`
+- ✅ 索引优化:`ak_users.role` 上的索引直接支持角色查询
+- ✅ 存储节约:减少了重复数据的存储
+
+## 📋 迁移指南
+
+### 对于新项目
+直接使用修复后的 `complete_mall_database.sql` 脚本。
+
+### 对于现有项目
+1. 执行 `role_field_cleanup.sql` 脚本
+2. 验证数据迁移结果
+3. 测试相关功能是否正常
+
+### 脚本执行顺序
+```bash
+# 1. 主数据库结构
+psql -f complete_mall_database.sql
+
+# 2. 角色字段清理(如果是从旧版本升级)
+psql -f role_field_cleanup.sql
+
+# 3. 插入测试数据
+psql -f mock_data_insert.sql
+```
+
+## ✨ 结论
+
+角色字段统一修复已经完成,系统现在具有:
+- 🎯 **清晰的数据结构**:角色信息统一存储在 `ak_users.role`
+- 🔒 **数据一致性保证**:消除了数据重复和不一致的风险
+- 🚀 **更好的性能**:简化了查询逻辑,提高了查询效率
+- 🛠️ **易于维护**:减少了代码复杂度,便于后续维护和扩展
+
+所有相关文件已更新完毕,可以安全使用!
diff --git a/mall_sql/docs/ROLE_FIELD_SUMMARY.md b/mall_sql/docs/ROLE_FIELD_SUMMARY.md
new file mode 100644
index 00000000..e584ae50
--- /dev/null
+++ b/mall_sql/docs/ROLE_FIELD_SUMMARY.md
@@ -0,0 +1,172 @@
+# 角色字段统一方案总结
+
+## 📋 概述
+
+为了提高代码可读性和语义清晰度,我们将商城系统中的用户角色字段从 `user_type` (INTEGER) 统一为 `role` (TEXT)。
+
+## 🔄 修改内容
+
+### 1. 字段类型变更
+
+#### 原始设计 (已废弃)
+```sql
+-- ml_user_profiles 表
+user_type INTEGER DEFAULT 1 NOT NULL
+-- 约束:CHECK (user_type IN (1,2,3,4,5))
+-- 1:消费者 2:商家 3:配送员 4:客服 5:管理员
+```
+
+#### 新设计 (当前版本)
+```sql
+-- ml_user_profiles 表 + ak_users 表
+role TEXT DEFAULT 'customer' NOT NULL
+-- 约束:CHECK (role IN ('customer', 'merchant', 'delivery', 'service', 'admin'))
+-- customer:消费者, merchant:商家, delivery:配送员, service:客服, admin:管理员
+```
+
+### 2. 数据映射关系
+
+| 旧 user_type (INTEGER) | 新 role (TEXT) | 中文含义 |
+|------------------------|----------------|----------|
+| 1 | customer | 消费者 |
+| 2 | merchant | 商家 |
+| 3 | delivery | 配送员 |
+| 4 | service | 客服 |
+| 5 | admin | 管理员 |
+
+### 3. 统一后的优势
+
+1. **语义清晰**:`role` 比 `user_type` 更符合业务语义
+2. **代码可读**:字符串值比数字更易理解
+3. **扩展性好**:便于添加新角色类型
+4. **国际化友好**:角色名称可直接用于多语言映射
+5. **API友好**:前端可直接使用角色字符串
+
+## 📁 相关文件
+
+### 核心数据库文件
+- ✅ `complete_mall_database.sql` - 主数据库结构(已更新)
+- ✅ `mock_data_insert.sql` - 测试数据插入(已更新)
+
+### 迁移脚本
+- 🆕 `quick_role_migration.sql` - 快速迁移脚本(推荐)
+- 🆕 `role_field_unification.sql` - 完整统一方案
+
+### 其他升级脚本(自动兼容)
+- ✅ `mall_alter_upgrade.sql` - 增量升级脚本
+- ✅ `mall_fields_only_upgrade.sql` - 字段升级脚本
+- ✅ `mall_migration.sql` - 完整迁移脚本
+- ✅ `mall_seo_security.sql` - SEO和安全脚本
+
+### 文档
+- ✅ `UPGRADE_GUIDE.md` - 升级指南(已更新)
+
+## 🚀 执行步骤
+
+### 对于新项目
+直接使用最新的 `complete_mall_database.sql`,已包含 `role` 字段设计。
+
+### 对于现有项目
+如果您的数据库中存在 `user_type` 字段,请按以下步骤升级:
+
+#### 步骤 1:数据备份
+```bash
+pg_dump your_database > backup_before_role_migration.sql
+```
+
+#### 步骤 2:执行快速迁移
+```bash
+psql -d your_database -f quick_role_migration.sql
+```
+
+#### 步骤 3:验证迁移结果
+```sql
+-- 检查角色分布
+SELECT role, COUNT(*) as count
+FROM ml_user_profiles
+GROUP BY role;
+
+-- 检查数据一致性
+SELECT COUNT(*) as inconsistent_records
+FROM ak_users u
+JOIN ml_user_profiles p ON u.id = p.user_id
+WHERE u.role != p.role;
+```
+
+#### 步骤 4:(可选)清理旧字段
+迁移成功并确认无误后,可删除旧的 `user_type` 字段:
+```sql
+ALTER TABLE ml_user_profiles DROP COLUMN user_type;
+```
+
+## 🔧 技术细节
+
+### 更新的数据库对象
+
+1. **表结构**
+ - `ml_user_profiles.role` - 新增字段
+ - `ak_users.role` - 与之保持同步
+
+2. **约束**
+ - `chk_ml_user_role` - 角色值约束
+ - 移除:`chk_ml_user_type`
+
+3. **索引**
+ - `idx_ml_user_profiles_role` - 角色字段索引
+ - 移除:`idx_ml_user_profiles_type`
+
+4. **函数**
+ - `is_verified_merchant()` - 商家验证函数
+ - `get_user_role()` - 获取用户角色
+ - `check_user_permission()` - 权限检查
+ - `upgrade_user_role()` - 角色升级
+
+5. **视图**
+ - `ml_users_view` - 用户信息视图
+ - `vw_user_info` - 用户完整信息视图
+ - `vw_role_statistics` - 角色统计视图
+
+6. **RLS策略**
+ - 所有涉及角色检查的策略已更新
+
+### 兼容性说明
+
+- ✅ **向前兼容**:新脚本可在空数据库上运行
+- ✅ **向后兼容**:提供完整回滚方案
+- ✅ **增量升级**:支持现有数据的平滑迁移
+- ✅ **Supabase兼容**:完全支持Supabase环境
+
+## 🔍 测试验证
+
+### 测试用例
+```sql
+-- 1. 测试角色约束
+INSERT INTO ml_user_profiles (user_id, role)
+VALUES (uuid_generate_v4(), 'invalid_role'); -- 应该失败
+
+-- 2. 测试函数
+SELECT get_user_role('user-uuid-here');
+SELECT check_user_permission('user-uuid-here', ARRAY['admin', 'merchant']);
+
+-- 3. 测试视图
+SELECT * FROM vw_role_statistics;
+SELECT * FROM ml_users_view WHERE role = 'merchant';
+```
+
+### 性能影响
+- 角色查询性能:通过 `idx_ml_user_profiles_role` 索引优化
+- 存储开销:TEXT字段比INTEGER稍大,但差异微小
+- 查询兼容:所有现有查询逻辑已更新
+
+## 📞 支持
+
+如果在角色字段迁移过程中遇到问题,请:
+
+1. 检查错误日志
+2. 确认数据备份完整
+3. 运行 `mall_database_check.sql` 诊断问题
+4. 如需回滚,使用 `quick_role_migration.sql` 中的回滚脚本
+
+---
+
+**总结**:角色字段统一方案提供了更清晰、更语义化的用户角色管理,同时保持了完整的向后兼容性和迁移安全性。
diff --git a/mall_sql/docs/TECHNICAL_IMPLEMENTATION.md b/mall_sql/docs/TECHNICAL_IMPLEMENTATION.md
new file mode 100644
index 00000000..17b2546d
--- /dev/null
+++ b/mall_sql/docs/TECHNICAL_IMPLEMENTATION.md
@@ -0,0 +1,1431 @@
+# 🔨 doc_mall 模块技术实现拆解
+
+## 📋 目录
+1. [整体架构](#整体架构)
+2. [数据库层实现](#数据库层实现)
+3. [后端/API层实现](#后端api层实现)
+4. [前端实现](#前端实现)
+5. [数据流机制](#数据流机制)
+6. [业务逻辑实现](#业务逻辑实现)
+7. [安全机制](#安全机制)
+8. [性能优化](#性能优化)
+
+---
+
+## 一、整体架构
+
+### 1.1 技术栈架构图
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ 前端层 (uni-app-x) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
+│ │ 消费者端 │ │ 商家端 │ │ 配送端 │ │ 管理端 │ │
+│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
+│ │ │ │ │ │
+│ └──────────────┴──────────────┴────────────┘ │
+│ │ │
+│ ┌───────────▼───────────┐ │
+│ │ Supabase 客户端封装 │ │
+│ │ (AkSupa.uts) │ │
+│ └───────────┬───────────┘ │
+└──────────────────────────┼──────────────────────────────┘
+ │ HTTPS REST API
+┌───────────────────────────▼──────────────────────────────┐
+│ API 层 (PostgREST) │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ 自动生成 REST API │ │
+│ │ - GET /rest/v1/ml_products │ │
+│ │ - POST /rest/v1/ml_orders │ │
+│ │ - RPC /rest/v1/rpc/calculate_cart_total │ │
+│ └──────────────────────────────────────────────────┘ │
+└───────────────────────────┬──────────────────────────────┘
+ │
+┌───────────────────────────▼──────────────────────────────┐
+│ 认证层 (Supabase Auth) │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ JWT Token 认证 │ │
+│ │ - 用户登录/注册 │ │
+│ │ - Token 刷新 │ │
+│ │ - 权限验证 │ │
+│ └──────────────────────────────────────────────────┘ │
+└───────────────────────────┬──────────────────────────────┘
+ │
+┌───────────────────────────▼──────────────────────────────┐
+│ 权限层 (RLS - Row Level Security) │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ 行级安全策略 │ │
+│ │ - 用户数据隔离 │ │
+│ │ - 商家权限控制 │ │
+│ │ - 公开数据访问 │ │
+│ └──────────────────────────────────────────────────┘ │
+└───────────────────────────┬──────────────────────────────┘
+ │
+┌───────────────────────────▼──────────────────────────────┐
+│ 数据库层 (PostgreSQL) │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
+│ │ 表结构 │ │ 触发器 │ │ 函数 │ │ 视图 │ │
+│ │ 索引 │ │ RLS策略 │ │ 序列 │ │ 扩展 │ │
+│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 1.2 核心设计模式
+
+#### BaaS (Backend as a Service) 模式
+- **特点**: 使用 Supabase 作为后端服务,无需自建后端服务器
+- **优势**:
+ - 自动生成 REST API
+ - 内置认证和权限系统
+ - 实时数据同步
+ - 减少后端开发工作量
+
+#### 数据库优先 (Database-First) 模式
+- **流程**: 数据库设计 → 自动生成 API → 前端调用
+- **实现**:
+ 1. 设计数据库表结构
+ 2. PostgREST 自动生成 REST API
+ 3. 前端通过 Supabase 客户端调用
+
+#### 类型驱动开发 (Type-Driven Development)
+- **实现**: TypeScript/UTS 类型定义
+- **文件**: `types/mall-types.uts`
+- **优势**: 类型安全、代码提示、减少错误
+
+---
+
+## 二、数据库层实现
+
+### 2.1 表结构设计
+
+#### 2.1.1 核心表结构
+
+**用户扩展表** (`ml_user_profiles`)
+```sql
+CREATE TABLE public.ml_user_profiles (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ status INTEGER DEFAULT 1, -- 1:正常 2:冻结 3:注销 4:待审核
+ real_name VARCHAR(100), -- 真实姓名
+ id_card VARCHAR(32), -- 身份证号
+ credit_score INTEGER DEFAULT 100, -- 信用分数 0-1000
+ verification_status INTEGER DEFAULT 0, -- 认证状态
+ verification_data JSONB DEFAULT '{}', -- 认证相关数据
+ preferences JSONB DEFAULT '{}', -- 用户偏好设置
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+```
+
+**商品表** (`ml_products`)
+```sql
+CREATE TABLE public.ml_products (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
+ category_id UUID NOT NULL REFERENCES public.ml_categories(id),
+ brand_id UUID REFERENCES public.ml_brands(id),
+ product_code VARCHAR(100) UNIQUE NOT NULL,
+ name VARCHAR(500) NOT NULL,
+ description TEXT,
+ main_image_url TEXT,
+ image_urls JSONB DEFAULT '[]', -- 商品图片数组
+ base_price DECIMAL(12,2) NOT NULL,
+ total_stock INTEGER DEFAULT 0,
+ available_stock INTEGER DEFAULT 0,
+ status INTEGER DEFAULT 1, -- 1:上架 2:下架 3:草稿 4:删除
+ view_count INTEGER DEFAULT 0,
+ sale_count INTEGER DEFAULT 0,
+ rating_avg DECIMAL(3,2) DEFAULT 0.00,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+```
+
+**订单表** (`ml_orders`)
+```sql
+CREATE TABLE public.ml_orders (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ cid SERIAL UNIQUE NOT NULL,
+ order_no VARCHAR(50) UNIQUE NOT NULL,
+ user_id UUID NOT NULL REFERENCES public.ak_users(id),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
+ product_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
+ discount_amount DECIMAL(12,2) DEFAULT 0,
+ shipping_fee DECIMAL(12,2) DEFAULT 0,
+ total_amount DECIMAL(12,2) NOT NULL,
+ shipping_address JSONB NOT NULL, -- 收货地址JSON
+ order_status INTEGER DEFAULT 1, -- 1:待支付 2:待发货 3:待收货 4:已完成
+ payment_status INTEGER DEFAULT 1, -- 1:未支付 2:已支付 3:部分退款 4:全额退款
+ shipping_status INTEGER DEFAULT 1, -- 1:未发货 2:已发货 3:运输中 4:已送达
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+```
+
+### 2.2 索引设计
+
+#### 2.2.1 性能优化索引
+
+```sql
+-- 商品表索引
+CREATE INDEX idx_ml_products_merchant ON public.ml_products(merchant_id);
+CREATE INDEX idx_ml_products_category ON public.ml_products(category_id);
+CREATE INDEX idx_ml_products_status ON public.ml_products(status);
+CREATE INDEX idx_ml_products_cid ON public.ml_products(cid); -- SEO查询
+CREATE INDEX idx_ml_products_created ON public.ml_products(created_at DESC);
+
+-- 订单表索引
+CREATE INDEX idx_ml_orders_user ON public.ml_orders(user_id);
+CREATE INDEX idx_ml_orders_merchant ON public.ml_orders(merchant_id);
+CREATE INDEX idx_ml_orders_status ON public.ml_orders(order_status);
+CREATE INDEX idx_ml_orders_created ON public.ml_orders(created_at DESC);
+
+-- JSONB GIN索引(用于JSON字段查询)
+CREATE INDEX idx_ml_products_images_gin ON public.ml_products USING GIN(image_urls);
+CREATE INDEX idx_ml_orders_address_gin ON public.ml_orders USING GIN(shipping_address);
+```
+
+### 2.3 触发器实现
+
+#### 2.3.1 自动更新时间戳
+
+```sql
+-- 触发器函数
+CREATE OR REPLACE FUNCTION public.update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- 应用到表
+CREATE TRIGGER trigger_ml_user_profiles_updated_at
+ BEFORE UPDATE ON public.ml_user_profiles
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+```
+
+#### 2.3.2 确保唯一默认地址
+
+```sql
+CREATE OR REPLACE FUNCTION public.ensure_single_default_address()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.is_default = TRUE THEN
+ UPDATE public.ml_user_addresses
+ SET is_default = FALSE
+ WHERE user_id = NEW.user_id AND id != NEW.id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_ensure_single_default_address
+ BEFORE INSERT OR UPDATE ON public.ml_user_addresses
+ FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address();
+```
+
+#### 2.3.3 自动更新商品库存
+
+```sql
+CREATE OR REPLACE FUNCTION public.update_product_stock()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ -- 插入订单商品时,减少库存
+ UPDATE public.ml_products
+ SET available_stock = available_stock - NEW.quantity
+ WHERE id = NEW.product_id;
+ ELSIF TG_OP = 'DELETE' THEN
+ -- 删除订单商品时,恢复库存
+ UPDATE public.ml_products
+ SET available_stock = available_stock + OLD.quantity
+ WHERE id = OLD.product_id;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_update_product_stock
+ AFTER INSERT OR DELETE ON public.ml_order_items
+ FOR EACH ROW EXECUTE FUNCTION public.update_product_stock();
+```
+
+### 2.4 数据库函数实现
+
+#### 2.4.1 业务函数
+
+**生成订单号**
+```sql
+CREATE OR REPLACE FUNCTION public.generate_order_no()
+RETURNS TEXT AS $$
+DECLARE
+ order_no TEXT;
+BEGIN
+ order_no := 'ML' || TO_CHAR(NOW(), 'YYYYMMDD') ||
+ LPAD(NEXTVAL('ml_order_seq')::TEXT, 6, '0');
+ RETURN order_no;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+**计算购物车总金额**
+```sql
+CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID)
+RETURNS DECIMAL AS $$
+DECLARE
+ total_amount DECIMAL(12,2) := 0;
+BEGIN
+ SELECT COALESCE(SUM(
+ CASE
+ WHEN c.sku_id IS NOT NULL THEN s.price * c.quantity
+ ELSE p.base_price * c.quantity
+ END
+ ), 0) INTO total_amount
+ FROM public.ml_shopping_cart c
+ LEFT JOIN public.ml_product_skus s ON c.sku_id = s.id
+ LEFT JOIN public.ml_products p ON c.product_id = p.id
+ WHERE c.user_id = p_user_id
+ AND c.selected = TRUE
+ AND p.status = 1
+ AND (s.id IS NULL OR s.status = 1);
+
+ RETURN total_amount;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+**获取用户默认地址**
+```sql
+CREATE OR REPLACE FUNCTION public.get_user_default_address(p_user_id UUID)
+RETURNS TABLE (
+ id UUID,
+ receiver_name VARCHAR,
+ receiver_phone VARCHAR,
+ address_detail TEXT
+) AS $$
+BEGIN
+ RETURN QUERY
+ SELECT
+ a.id,
+ a.receiver_name,
+ a.receiver_phone,
+ a.address_detail
+ FROM public.ml_user_addresses a
+ WHERE a.user_id = p_user_id
+ AND a.is_default = TRUE
+ AND a.status = 1
+ LIMIT 1;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+### 2.5 RLS 行级安全策略
+
+#### 2.5.1 用户数据隔离
+
+```sql
+-- 用户只能访问自己的数据
+CREATE POLICY ml_user_profiles_select_policy ON public.ml_user_profiles
+ FOR SELECT USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ );
+
+CREATE POLICY ml_user_profiles_update_policy ON public.ml_user_profiles
+ FOR UPDATE USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ );
+```
+
+#### 2.5.2 商品权限控制
+
+```sql
+-- 所有人可查看已上架商品
+CREATE POLICY ml_products_select_policy ON public.ml_products
+ FOR SELECT USING (status = 1);
+
+-- 商家只能管理自己的商品
+CREATE POLICY ml_products_insert_policy ON public.ml_products
+ FOR INSERT WITH CHECK (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id)
+ );
+
+CREATE POLICY ml_products_update_policy ON public.ml_products
+ FOR UPDATE USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id)
+ );
+```
+
+#### 2.5.3 订单权限控制
+
+```sql
+-- 用户和商家都可查看相关订单
+CREATE POLICY ml_orders_select_policy ON public.ml_orders
+ FOR SELECT USING (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ OR
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = merchant_id)
+ );
+
+-- 只有用户能创建订单
+CREATE POLICY ml_orders_insert_policy ON public.ml_orders
+ FOR INSERT WITH CHECK (
+ auth.uid()::text = (SELECT auth_id::text FROM public.ak_users WHERE id = user_id)
+ );
+```
+
+---
+
+## 三、后端/API层实现
+
+### 3.1 PostgREST 自动生成 API
+
+#### 3.1.1 REST API 自动生成机制
+
+Supabase 基于 PostgREST 自动为每个表生成 REST API:
+
+**查询 API**
+```
+GET /rest/v1/ml_products
+GET /rest/v1/ml_products?id=eq.{uuid}
+GET /rest/v1/ml_products?status=eq.1&limit=20
+GET /rest/v1/ml_products?base_price=gte.100&base_price=lte.500
+```
+
+**插入 API**
+```
+POST /rest/v1/ml_orders
+Content-Type: application/json
+{
+ "user_id": "uuid",
+ "merchant_id": "uuid",
+ "total_amount": 100.00
+}
+```
+
+**更新 API**
+```
+PATCH /rest/v1/ml_products?id=eq.{uuid}
+Content-Type: application/json
+{
+ "status": 2
+}
+```
+
+**删除 API**
+```
+DELETE /rest/v1/ml_user_favorites?id=eq.{uuid}
+```
+
+**RPC 函数调用**
+```
+POST /rest/v1/rpc/calculate_cart_total
+Content-Type: application/json
+{
+ "p_user_id": "uuid"
+}
+```
+
+### 3.2 查询操作符支持
+
+PostgREST 支持丰富的查询操作符:
+
+```typescript
+// 等于
+?status=eq.1
+
+// 不等于
+?status=neq.2
+
+// 大于/小于
+?base_price=gte.100&base_price=lte.500
+
+// 模糊查询
+?name=ilike.%商品%
+
+// IN 查询
+?category_id=in.(id1,id2,id3)
+
+// IS NULL
+?deleted_at=is.null
+
+// JSONB 查询
+?preferences->theme=eq.dark
+```
+
+### 3.3 分页和排序
+
+```typescript
+// 分页
+?limit=20&offset=0
+// 或使用 Range 头
+Range: 0-19
+
+// 排序
+?order=created_at.desc
+?order=base_price.asc,created_at.desc
+
+// 计数
+Prefer: count=exact
+```
+
+---
+
+## 四、前端实现
+
+### 4.1 项目结构
+
+```
+pages/mall/
+├── consumer/ # 消费者端
+│ ├── index.uvue # 首页
+│ ├── product-detail.uvue # 商品详情
+│ ├── cart.uvue # 购物车
+│ ├── checkout.uvue # 结算页
+│ ├── orders.uvue # 订单列表
+│ └── subscription/ # 订阅相关
+├── merchant/ # 商家端
+│ ├── index.uvue # 商家首页
+│ └── product-detail.uvue
+├── delivery/ # 配送端
+├── admin/ # 管理端
+└── service/ # 客服端
+
+components/supadb/
+├── aksupa.uts # Supabase 客户端封装
+├── aksupainstance.uts # 全局单例
+└── aksuparealtime.uts # 实时订阅
+
+types/
+└── mall-types.uts # 类型定义
+```
+
+### 4.2 Supabase 客户端封装
+
+#### 4.2.1 客户端初始化
+
+**文件**: `components/supadb/aksupainstance.uts`
+
+```typescript
+import AkSupa from './aksupa.uts'
+import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
+
+// 创建全局 Supabase 客户端实例
+const supa = new AkSupa(SUPA_URL, SUPA_KEY)
+
+// 自动登录 (开发环境)
+const supaReady: Promise = (async () => {
+ try {
+ await supa.signIn('test@example.com', 'password')
+ return true
+ } catch (err) {
+ console.error('Supabase auto sign-in failed', err)
+ return false
+ }
+})()
+
+export { supaReady }
+export default supa
+```
+
+#### 4.2.2 API 调用封装
+
+**文件**: `components/supadb/aksupa.uts`
+
+```typescript
+export class AkSupa {
+ baseUrl: string
+ apikey: string
+
+ constructor(baseUrl: string, apikey: string) {
+ this.baseUrl = baseUrl
+ this.apikey = apikey
+ }
+
+ // 查询数据
+ async select(
+ table: string,
+ filter?: string | null,
+ options?: AkSupaSelectOptions
+ ): Promise> {
+ let url = this.baseUrl + '/rest/v1/' + table
+ let headers = {
+ apikey: this.apikey,
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${AkReq.getToken() ?? ''}`
+ } as UTSJSONObject
+
+ // 构建查询参数
+ let params: string[] = []
+ if (options?.limit) params.push(`limit=${options.limit}`)
+ if (options?.order) params.push(`order=${encodeURIComponent(options.order)}`)
+ if (filter) params.push(filter)
+
+ if (params.length > 0) {
+ url += '?' + params.join('&')
+ }
+
+ return await this.requestWithAutoRefresh({
+ url,
+ method: 'GET',
+ headers
+ })
+ }
+
+ // 插入数据
+ async insert(
+ table: string,
+ row: UTSJSONObject | Array
+ ): Promise> {
+ const url = this.baseUrl + '/rest/v1/' + table
+ const headers = {
+ apikey: this.apikey,
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${AkReq.getToken() ?? ''}`,
+ Prefer: 'return=representation'
+ } as UTSJSONObject
+
+ return await this.requestWithAutoRefresh({
+ url,
+ method: 'POST',
+ headers,
+ data: row
+ })
+ }
+
+ // 更新数据
+ async update(
+ table: string,
+ filter: UTSJSONObject,
+ data: UTSJSONObject
+ ): Promise> {
+ const filterStr = buildSupabaseFilterQuery(filter)
+ const url = this.baseUrl + '/rest/v1/' + table + '?' + filterStr
+ const headers = {
+ apikey: this.apikey,
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${AkReq.getToken() ?? ''}`,
+ Prefer: 'return=representation'
+ } as UTSJSONObject
+
+ return await this.requestWithAutoRefresh({
+ url,
+ method: 'PATCH',
+ headers,
+ data
+ })
+ }
+
+ // 调用 RPC 函数
+ async rpc(
+ functionName: string,
+ params: UTSJSONObject
+ ): Promise> {
+ const url = `${this.baseUrl}/rest/v1/rpc/${functionName}`
+ const headers = {
+ apikey: this.apikey,
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${AkReq.getToken() ?? ''}`
+ } as UTSJSONObject
+
+ return await this.requestWithAutoRefresh({
+ url,
+ method: 'POST',
+ headers,
+ data: params
+ })
+ }
+}
+```
+
+### 4.3 页面实现示例
+
+#### 4.3.1 商品列表页面
+
+**文件**: `pages/mall/consumer/index.uvue`
+
+```typescript
+
+```
+
+#### 4.3.2 商品详情页面
+
+```typescript
+// 加载商品详情
+const loadProductDetail = async (productId: string) => {
+ try {
+ // 查询商品信息
+ const productRes = await supa.select('ml_products',
+ `id=eq.${productId}`,
+ { single: true }
+ )
+
+ if (productRes.success && productRes.data) {
+ product.value = productRes.data as ProductType
+ }
+
+ // 查询商品SKU
+ const skuRes = await supa.select('ml_product_skus', {
+ product_id: productId
+ })
+
+ if (skuRes.success && skuRes.data) {
+ productSkus.value = skuRes.data as Array
+ }
+
+ // 查询商家信息
+ const merchantRes = await supa.select('ml_shops', {
+ merchant_id: product.value.merchant_id
+ }, { single: true })
+
+ if (merchantRes.success && merchantRes.data) {
+ merchant.value = merchantRes.data as MerchantType
+ }
+ } catch (err) {
+ console.error('加载商品详情失败:', err)
+ }
+}
+```
+
+#### 4.3.3 购物车操作
+
+```typescript
+// 添加到购物车
+const addToCart = async (product: ProductType, skuId?: string, quantity: number = 1) => {
+ try {
+ const userId = getCurrentUserId()
+ if (!userId) {
+ uni.showToast({ title: '请先登录', icon: 'error' })
+ return
+ }
+
+ // 检查购物车中是否已存在
+ const existingRes = await supa.select('ml_shopping_cart', {
+ user_id: userId,
+ product_id: product.id,
+ sku_id: skuId || null
+ }, { single: true })
+
+ if (existingRes.success && existingRes.data) {
+ // 更新数量
+ await supa.update('ml_shopping_cart',
+ { id: existingRes.data.id },
+ { quantity: existingRes.data.quantity + quantity }
+ )
+ } else {
+ // 新增购物车项
+ await supa.insert('ml_shopping_cart', {
+ user_id: userId,
+ product_id: product.id,
+ sku_id: skuId || null,
+ quantity: quantity,
+ selected: true
+ })
+ }
+
+ uni.showToast({ title: '已添加到购物车', icon: 'success' })
+ loadCartCount()
+ } catch (err) {
+ console.error('添加到购物车失败:', err)
+ uni.showToast({ title: '操作失败', icon: 'error' })
+ }
+}
+```
+
+#### 4.3.4 创建订单
+
+```typescript
+// 创建订单
+const createOrder = async (cartItems: Array, address: UserAddressType) => {
+ try {
+ // 1. 计算订单总金额(使用数据库函数)
+ const totalRes = await supa.rpc('calculate_cart_total', {
+ p_user_id: getCurrentUserId()
+ })
+
+ if (!totalRes.success) {
+ throw new Error('计算订单金额失败')
+ }
+
+ const totalAmount = totalRes.data as number
+
+ // 2. 生成订单号
+ const orderNoRes = await supa.rpc('generate_order_no')
+ if (!orderNoRes.success) {
+ throw new Error('生成订单号失败')
+ }
+ const orderNo = orderNoRes.data as string
+
+ // 3. 创建订单
+ const orderRes = await supa.insert('ml_orders', {
+ order_no: orderNo,
+ user_id: getCurrentUserId(),
+ merchant_id: cartItems[0].product.merchant_id,
+ product_amount: totalAmount,
+ discount_amount: 0,
+ shipping_fee: 0,
+ total_amount: totalAmount,
+ shipping_address: {
+ receiver_name: address.receiver_name,
+ receiver_phone: address.receiver_phone,
+ province: address.province,
+ city: address.city,
+ district: address.district,
+ address_detail: address.address_detail
+ },
+ order_status: 1, // 待支付
+ payment_status: 1,
+ shipping_status: 1
+ })
+
+ if (!orderRes.success) {
+ throw new Error('创建订单失败')
+ }
+
+ const orderId = orderRes.data.id
+
+ // 4. 创建订单商品项
+ const orderItems = cartItems.map(item => ({
+ order_id: orderId,
+ product_id: item.product_id,
+ sku_id: item.sku_id || null,
+ product_name: item.product.name,
+ price: item.sku?.price || item.product.price,
+ quantity: item.quantity,
+ total_amount: (item.sku?.price || item.product.price) * item.quantity
+ }))
+
+ await supa.insert('ml_order_items', orderItems)
+
+ // 5. 清空购物车
+ for (const item of cartItems) {
+ await supa.delete('ml_shopping_cart', { id: item.id })
+ }
+
+ uni.showToast({ title: '订单创建成功', icon: 'success' })
+
+ // 跳转到订单详情
+ uni.navigateTo({
+ url: `/pages/mall/consumer/order-detail?orderId=${orderId}`
+ })
+ } catch (err) {
+ console.error('创建订单失败:', err)
+ uni.showToast({ title: '创建订单失败', icon: 'error' })
+ }
+}
+```
+
+### 4.4 实时数据同步
+
+#### 4.4.1 订阅订单状态更新
+
+```typescript
+import { AkSupaRealtime } from '@/components/supadb/aksuparealtime.uts'
+
+// 订阅订单状态更新
+const subscribeOrderStatus = (orderId: string, callback: (payload: any) => void) => {
+ const realtime = new AkSupaRealtime(WS_URL, SUPA_KEY)
+
+ realtime.subscribe('ml_orders', {
+ filter: `id=eq.${orderId}`,
+ event: 'UPDATE',
+ callback: (payload) => {
+ console.log('订单状态更新:', payload)
+ callback(payload)
+ }
+ })
+}
+
+// 使用
+onMounted(() => {
+ subscribeOrderStatus(orderId, (payload) => {
+ // 更新订单状态
+ order.value.order_status = payload.new.order_status
+ })
+})
+```
+
+---
+
+## 五、数据流机制
+
+### 5.1 数据流向图
+
+```
+用户操作
+ │
+ ▼
+前端页面 (uni-app-x)
+ │
+ ▼
+Supabase 客户端 (AkSupa)
+ │
+ ▼ HTTP Request
+ │
+ ├─ Headers: apikey, Authorization (JWT Token)
+ │
+ ▼
+PostgREST API
+ │
+ ├─ 解析请求
+ ├─ 验证 JWT Token
+ ├─ 应用 RLS 策略
+ │
+ ▼
+PostgreSQL 数据库
+ │
+ ├─ 执行 SQL 查询
+ ├─ 触发触发器
+ ├─ 执行函数
+ │
+ ▼
+返回数据
+ │
+ ▼
+PostgREST 格式化响应
+ │
+ ▼ HTTP Response
+ │
+ ├─ JSON 数据
+ ├─ 状态码
+ │
+ ▼
+Supabase 客户端处理
+ │
+ ├─ 解析响应
+ ├─ 错误处理
+ │
+ ▼
+前端页面更新 UI
+```
+
+### 5.2 认证流程
+
+```
+1. 用户登录
+ │
+ ▼
+2. Supabase Auth 验证
+ │
+ ├─ 验证邮箱/密码
+ ├─ 生成 JWT Token
+ │
+ ▼
+3. 存储 Token
+ │
+ ├─ 本地存储 (uni.setStorageSync)
+ │
+ ▼
+4. 后续请求自动携带 Token
+ │
+ ├─ Authorization: Bearer
+ │
+ ▼
+5. PostgREST 验证 Token
+ │
+ ├─ 解析 JWT
+ ├─ 获取 auth.uid()
+ │
+ ▼
+6. RLS 策略应用
+ │
+ ├─ 根据 auth.uid() 过滤数据
+```
+
+### 5.3 权限控制流程
+
+```
+请求到达 PostgREST
+ │
+ ▼
+解析 JWT Token
+ │
+ ├─ 获取 auth.uid()
+ │
+ ▼
+查找 RLS 策略
+ │
+ ├─ SELECT 策略 → USING 子句
+ ├─ INSERT 策略 → WITH CHECK 子句
+ ├─ UPDATE 策略 → USING + WITH CHECK
+ ├─ DELETE 策略 → USING 子句
+ │
+ ▼
+应用策略条件
+ │
+ ├─ 用户只能访问自己的数据
+ ├─ 商家只能管理自己的商品
+ ├─ 公开数据所有人可查看
+ │
+ ▼
+执行 SQL 查询
+ │
+ ├─ 自动添加 WHERE 条件
+ │
+ ▼
+返回过滤后的数据
+```
+
+---
+
+## 六、业务逻辑实现
+
+### 6.1 商品管理流程
+
+#### 6.1.1 商品上架流程
+
+```typescript
+// 商家上架商品
+const publishProduct = async (productData: any) => {
+ // 1. 验证商家权限
+ const merchantRes = await supa.select('ml_user_profiles', {
+ user_id: getCurrentUserId(),
+ user_type: 2, // 商家
+ verification_status: 1 // 已认证
+ }, { single: true })
+
+ if (!merchantRes.success || !merchantRes.data) {
+ throw new Error('您还不是认证商家')
+ }
+
+ // 2. 创建商品
+ const productRes = await supa.insert('ml_products', {
+ merchant_id: getCurrentUserId(),
+ category_id: productData.category_id,
+ name: productData.name,
+ description: productData.description,
+ base_price: productData.price,
+ total_stock: productData.stock,
+ available_stock: productData.stock,
+ status: 1, // 上架
+ ...productData
+ })
+
+ // 3. 创建商品SKU
+ if (productData.skus && productData.skus.length > 0) {
+ const skus = productData.skus.map((sku: any) => ({
+ product_id: productRes.data.id,
+ sku_code: sku.sku_code,
+ specifications: sku.specifications,
+ price: sku.price,
+ stock: sku.stock
+ }))
+
+ await supa.insert('ml_product_skus', skus)
+ }
+
+ return productRes.data
+}
+```
+
+### 6.2 订单处理流程
+
+#### 6.2.1 订单状态流转
+
+```typescript
+// 订单状态枚举
+const ORDER_STATUS = {
+ PENDING_PAYMENT: 1, // 待支付
+ PAID: 2, // 已支付
+ SHIPPED: 3, // 已发货
+ DELIVERED: 4, // 已送达
+ COMPLETED: 5, // 已完成
+ CANCELLED: 6, // 已取消
+ REFUNDING: 7, // 退款中
+ REFUNDED: 8 // 已退款
+}
+
+// 更新订单状态
+const updateOrderStatus = async (orderId: string, newStatus: number) => {
+ const updateData: any = {
+ order_status: newStatus,
+ updated_at: new Date().toISOString()
+ }
+
+ // 根据状态设置相应时间戳
+ switch (newStatus) {
+ case ORDER_STATUS.PAID:
+ updateData.paid_at = new Date().toISOString()
+ updateData.payment_status = 2 // 已支付
+ break
+ case ORDER_STATUS.SHIPPED:
+ updateData.shipped_at = new Date().toISOString()
+ updateData.shipping_status = 2 // 已发货
+ break
+ case ORDER_STATUS.DELIVERED:
+ updateData.delivered_at = new Date().toISOString()
+ updateData.shipping_status = 4 // 已送达
+ break
+ case ORDER_STATUS.COMPLETED:
+ updateData.completed_at = new Date().toISOString()
+ break
+ }
+
+ await supa.update('ml_orders', { id: orderId }, updateData)
+}
+```
+
+### 6.3 库存管理机制
+
+#### 6.3.1 库存扣减流程
+
+```typescript
+// 下单时扣减库存
+const deductStock = async (orderItems: Array) => {
+ for (const item of orderItems) {
+ if (item.sku_id) {
+ // 扣减 SKU 库存
+ const skuRes = await supa.select('ml_product_skus', {
+ id: item.sku_id
+ }, { single: true })
+
+ if (skuRes.data.stock < item.quantity) {
+ throw new Error(`商品 ${item.product_name} 库存不足`)
+ }
+
+ await supa.update('ml_product_skus',
+ { id: item.sku_id },
+ { stock: skuRes.data.stock - item.quantity }
+ )
+ } else {
+ // 扣减商品总库存
+ const productRes = await supa.select('ml_products', {
+ id: item.product_id
+ }, { single: true })
+
+ if (productRes.data.available_stock < item.quantity) {
+ throw new Error(`商品 ${item.product_name} 库存不足`)
+ }
+
+ await supa.update('ml_products',
+ { id: item.product_id },
+ {
+ available_stock: productRes.data.available_stock - item.quantity,
+ sale_count: productRes.data.sale_count + item.quantity
+ }
+ )
+ }
+ }
+}
+```
+
+---
+
+## 七、安全机制
+
+### 7.1 多层安全防护
+
+```
+┌─────────────────────────────────────┐
+│ 1. 网络层安全 │
+│ - HTTPS 加密传输 │
+│ - SSL/TLS 证书 │
+└─────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────┐
+│ 2. 认证层安全 │
+│ - JWT Token 验证 │
+│ - Token 过期机制 │
+│ - 自动刷新 Token │
+└─────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────┐
+│ 3. 权限层安全 (RLS) │
+│ - 行级安全策略 │
+│ - 基于用户角色的权限控制 │
+│ - 数据隔离 │
+└─────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────┐
+│ 4. 应用层安全 │
+│ - 前端数据验证 │
+│ - 输入过滤和转义 │
+│ - 防止 SQL 注入 │
+└─────────────────────────────────────┘
+```
+
+### 7.2 数据验证机制
+
+#### 7.2.1 数据库约束
+
+```sql
+-- CHECK 约束
+CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4))
+CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7))
+CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)
+
+-- 外键约束
+REFERENCES public.ak_users(id) ON DELETE CASCADE
+REFERENCES public.ml_products(id) ON DELETE CASCADE
+
+-- 唯一约束
+UNIQUE(product_code)
+UNIQUE(order_no)
+UNIQUE(user_id, product_id, sku_id) -- 购物车唯一性
+```
+
+#### 7.2.2 前端验证
+
+```typescript
+// 表单验证
+const validateOrderData = (orderData: any): boolean => {
+ if (!orderData.user_id) {
+ uni.showToast({ title: '用户ID不能为空', icon: 'error' })
+ return false
+ }
+
+ if (!orderData.total_amount || orderData.total_amount <= 0) {
+ uni.showToast({ title: '订单金额无效', icon: 'error' })
+ return false
+ }
+
+ if (!orderData.shipping_address) {
+ uni.showToast({ title: '请选择收货地址', icon: 'error' })
+ return false
+ }
+
+ return true
+}
+```
+
+---
+
+## 八、性能优化
+
+### 8.1 数据库优化
+
+#### 8.1.1 索引优化
+
+```sql
+-- 复合索引(针对常用查询组合)
+CREATE INDEX idx_ml_products_status_created
+ON ml_products(status, created_at DESC);
+
+CREATE INDEX idx_ml_orders_user_status
+ON ml_orders(user_id, order_status);
+
+-- 部分索引(只索引活跃数据)
+CREATE INDEX idx_ml_products_active
+ON ml_products(merchant_id, category_id)
+WHERE status = 1;
+```
+
+#### 8.1.2 查询优化
+
+```typescript
+// 只查询需要的字段
+const res = await supa.select('ml_products', null, {
+ columns: 'id,name,base_price,main_image_url', // 只查询必要字段
+ limit: 20
+})
+
+// 使用视图简化复杂查询
+const res = await supa.select('ml_products_detail_view', {
+ status: 1
+}, {
+ limit: 20
+})
+```
+
+### 8.2 前端优化
+
+#### 8.2.1 分页加载
+
+```typescript
+// 分页加载商品列表
+const loadProducts = async (loadMore: boolean = false) => {
+ const currentPage = loadMore ? page.value + 1 : 1
+
+ const res = await supa
+ .from('ml_products')
+ .select('*')
+ .eq('status', 1)
+ .order('created_at', { ascending: false })
+ .range((currentPage - 1) * pageSize.value, currentPage * pageSize.value - 1)
+
+ // 追加或替换数据
+ if (loadMore) {
+ productList.value.push(...res.data)
+ } else {
+ productList.value = res.data
+ }
+}
+```
+
+#### 8.2.2 数据缓存
+
+```typescript
+// 使用本地缓存减少请求
+const getCachedProducts = async (categoryId: string) => {
+ const cacheKey = `products_${categoryId}`
+ const cached = uni.getStorageSync(cacheKey)
+
+ if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
+ // 5分钟内使用缓存
+ return cached.data
+ }
+
+ // 从服务器获取
+ const res = await supa.select('ml_products', {
+ category_id: categoryId,
+ status: 1
+ })
+
+ // 更新缓存
+ uni.setStorageSync(cacheKey, {
+ data: res.data,
+ timestamp: Date.now()
+ })
+
+ return res.data
+}
+```
+
+### 8.3 实时同步优化
+
+```typescript
+// 只订阅必要的数据变更
+const subscribeOrderUpdates = (orderId: string) => {
+ realtime.subscribe('ml_orders', {
+ filter: `id=eq.${orderId}`, // 只订阅特定订单
+ event: 'UPDATE', // 只监听更新事件
+ callback: handleOrderUpdate
+ })
+}
+
+// 及时取消订阅
+onUnmounted(() => {
+ realtime.unsubscribe('ml_orders')
+})
+```
+
+---
+
+## 九、开发工作流
+
+### 9.1 开发流程
+
+```
+1. 数据库设计
+ │
+ ├─ 设计表结构
+ ├─ 设计索引
+ ├─ 设计 RLS 策略
+ ├─ 设计触发器
+ └─ 设计函数
+ │
+ ▼
+2. 执行数据库脚本
+ │
+ ├─ complete_mall_database.sql
+ │
+ ▼
+3. 定义类型
+ │
+ ├─ types/mall-types.uts
+ │
+ ▼
+4. 开发前端页面
+ │
+ ├─ 页面组件
+ ├─ API 调用
+ └─ 业务逻辑
+ │
+ ▼
+5. 测试验证
+ │
+ ├─ 功能测试
+ ├─ 权限测试
+ └─ 性能测试
+```
+
+### 9.2 调试技巧
+
+```typescript
+// 1. 启用详细日志
+console.log('请求参数:', {
+ table: 'ml_products',
+ filter: filter,
+ options: options
+})
+
+// 2. 检查响应
+console.log('API 响应:', {
+ success: res.success,
+ status: res.status,
+ data: res.data,
+ error: res.error
+})
+
+// 3. 验证权限
+const session = await supa.getSession()
+console.log('当前用户:', session.user)
+console.log('JWT Token:', AkReq.getToken())
+```
+
+---
+
+## 📚 相关文档
+
+- [模块分析报告](./MODULE_ANALYSIS.md)
+- [前后端联调指南](./FRONTEND_BACKEND_DEBUGGING.md)
+- [数据库创建报告](./database/database_creation_report.md)
+- [完整部署指南](./database/complete_deployment_guide.md)
+
+---
+
+**生成时间**: 2025年1月
+**版本**: v1.0
+**状态**: ✅ 完整技术实现拆解
diff --git a/mall_sql/docs/UNI_APP_X_MIGRATION.md b/mall_sql/docs/UNI_APP_X_MIGRATION.md
new file mode 100644
index 00000000..7b847032
--- /dev/null
+++ b/mall_sql/docs/UNI_APP_X_MIGRATION.md
@@ -0,0 +1,198 @@
+# uni-app X 迁移操作总结
+
+## 操作日期
+2024年(具体日期根据实际情况填写)
+
+## 操作背景
+项目需要从传统的 uni-app 迁移到 **uni-app X**,以支持 `.uvue` 文件在 H5 浏览器中正确渲染。
+
+## 问题分析
+对比根项目(`akmon`)和 `mall` 项目,发现以下关键差异:
+
+1. **缺少 uni-app X 配置**:`mall/manifest.json` 中缺少 `"uni-app-x": {}` 配置项
+2. **存在 .vue 文件**:项目中有 23 个 `.vue` 文件,这些文件在 uni-app X 中无法被正确编译到 H5
+3. **编译器配置**:需要确保 HBuilderX 使用 uni-app X 编译器
+
+## 执行的操作
+
+### 1. 添加 uni-app X 配置
+
+在 `manifest.json` 中添加了 `"uni-app-x": {}` 配置项:
+
+```json
+{
+ "vueVersion": "3",
+ "uni-app-x": {},
+ "h5": {
+ "title": "mall",
+ "router": {
+ "mode": "hash",
+ "base": "./"
+ }
+ }
+}
+```
+
+**位置**:`manifest.json` 第 66 行
+
+**作用**:
+- 告诉 HBuilderX 这是一个 uni-app X 项目
+- 启用 uni-app X 编译链,支持 `.uvue` 文件编译到 H5
+- 确保 `.uvue` 文件能够被正确编译和渲染
+
+### 2. 删除所有 .vue 文件
+
+删除了项目中所有 `.vue` 文件,共 23 个文件:
+
+**删除的文件列表**:
+- `pages/user/boot.vue`
+- `pages/user/login.vue`
+- `pages/user/register.vue`
+- `pages/user/forgot-password.vue`
+- `pages/user/profile.vue`
+- `pages/user/center.vue`
+- `pages/user/terms.vue`
+- `pages/mall/consumer/index.vue`
+- `pages/mall/consumer/product-detail.vue`
+- `pages/mall/consumer/order-detail.vue`
+- `pages/mall/consumer/profile.vue`
+- `pages/mall/consumer/subscription/plan-list.vue`
+- `pages/mall/consumer/subscription/plan-detail.vue`
+- `pages/mall/consumer/subscription/subscribe-checkout.vue`
+- `pages/mall/consumer/subscription/my-subscriptions.vue`
+- `pages/mall/merchant/index.vue`
+- `pages/mall/delivery/index.vue`
+- `pages/mall/admin/index.vue`
+- `pages/mall/admin/subscription/plan-management.vue`
+- `pages/mall/admin/subscription/user-subscriptions.vue`
+- `pages/mall/service/index.vue`
+- `pages/mall/analytics/index.vue`
+- `pages/mall/nfc/security/index.vue`
+
+**删除命令**:
+```powershell
+Set-Location -Path 'd:\datas\hfkj\akmon\mall'
+Get-ChildItem -Recurse -Filter *.vue | Remove-Item -Force
+```
+
+**原因**:
+- `.vue` 文件在 uni-app X 中无法被正确编译到 H5 浏览器
+- 所有页面和组件应使用 `.uvue` 格式
+- 导入语句会自动识别 `.uvue` 扩展名(导入时无需显式指定扩展名)
+
+## 技术说明
+
+### uni-app X vs 传统 uni-app
+
+| 特性 | 传统 uni-app | uni-app X |
+| ------------- | --------------------- | ---------------------- |
+| 文件格式 | `.vue` | `.uvue` |
+| 脚本语言 | JavaScript/TypeScript | UTS (TypeScript 扩展) |
+| 编译器 | uni-app 编译器 | uni-app X 编译器 |
+| H5 渲染 | 需要编译 | 需要编译(但支持更好) |
+| manifest.json | 不需要 `uni-app-x` | 需要 `"uni-app-x": {}` |
+
+### 导入语句说明
+
+删除 `.vue` 文件后,所有导入语句会自动使用对应的 `.uvue` 文件:
+
+```typescript
+// 之前(.vue)
+import LoginPage from './pages/user/login.vue'
+
+// 现在(.uvue,扩展名可省略)
+import LoginPage from './pages/user/login.uvue'
+// 或者
+import LoginPage from './pages/user/login' // 自动识别 .uvue
+```
+
+## 后续操作
+
+### 1. 验证配置
+
+1. 打开 HBuilderX
+2. 打开 `mall` 项目
+3. 检查编译器:**工具** → **切换编译器** → 确认选择 **uni-app X**
+4. 如果未选择,请切换到 uni-app X 编译器
+
+### 2. 运行到 H5
+
+1. 在 HBuilderX 中,点击菜单:**运行** → **运行到浏览器** → **Chrome**(或内置浏览器)
+2. 等待编译完成
+3. 浏览器会自动打开并显示应用
+
+### 3. 发行 H5
+
+如果需要打包发布:
+
+1. 点击菜单:**发行** → **网站-H5**
+2. 等待编译完成
+3. 编译产物在 `unpackage/dist/build/h5` 目录
+4. 将整个 `h5` 目录部署到 Web 服务器
+
+### 4. 检查页面
+
+确保所有页面都有对应的 `.uvue` 文件:
+
+- 检查 `pages.json` 中配置的所有页面路径
+- 确认每个页面都有对应的 `.uvue` 文件
+- 如果缺少,需要从备份或版本控制中恢复并转换为 `.uvue` 格式
+
+## 注意事项
+
+1. **编译器版本**:必须使用支持 uni-app X 的 HBuilderX 版本
+2. **文件格式**:所有页面和组件必须使用 `.uvue` 格式,不能混用 `.vue`
+3. **UTS 语法**:`.uvue` 文件中的 `
+
+
diff --git a/pages/mall/consumer/address-list.uvue b/pages/mall/consumer/address-list.uvue
index 1b6aeefa..9938dd2a 100644
--- a/pages/mall/consumer/address-list.uvue
+++ b/pages/mall/consumer/address-list.uvue
@@ -36,6 +36,7 @@
+
+
diff --git a/pages/mall/consumer/checkout.uvue b/pages/mall/consumer/checkout.uvue
index 3c744516..d96e6e2a 100644
--- a/pages/mall/consumer/checkout.uvue
+++ b/pages/mall/consumer/checkout.uvue
@@ -120,8 +120,42 @@
+
+
+ 🔒
+ 您尚未登录,点击登录以同步服务器地址
+ ›
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ 本地地址(未同步)
-
-
+
+
+
+
+
+
+
+ 规格
+ {{ product.specification }}
+
+
+ 功能主治
+ {{ product.usage }}
+
+
+ 副作用
+ {{ product.side_effects }}
+
+
+ 注意事项
+ {{ product.precautions }}
+
+
+ 有效期
+ {{ product.expiry_date }}
+
+
+ 储存条件
+ {{ product.storage_conditions }}
+
+
+ 批准文号
+ {{ product.approval_number }}
+
+
+ 标签
+ {{ product.tags.join(', ') }}
+
+
+
+
@@ -148,7 +218,8 @@ export default {
selectedSkuId: '',
selectedSpec: '',
quantity: 1,
- isFavorite: false
+ isFavorite: false,
+ showParams: false
}
},
onLoad(options: any) {
@@ -241,42 +312,186 @@ export default {
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
- let dbProduct = null
+ let dbProductRaw = null
try {
console.log('正在尝试从数据库加载商品详情:', productId)
- dbProduct = await supabaseService.getProductById(productId)
- console.log('数据库返回的商品详情:', dbProduct)
+ dbProductRaw = await supabaseService.getProductById(productId)
+ console.log('数据库返回的商品详情 (原始数据):', dbProductRaw)
+
+ // 调试:打印数据库返回的所有字段
+ if (dbProductRaw) {
+ console.log('数据库返回字段详情:')
+ if (Array.isArray(dbProductRaw)) {
+ console.log('返回数据是数组,长度:', dbProductRaw.length)
+ if (dbProductRaw.length > 0) {
+ const firstItem = dbProductRaw[0]
+ console.log('数组第一个元素:', firstItem)
+ for (const key in firstItem) {
+ console.log(` ${key}:`, firstItem[key], typeof firstItem[key])
+ }
+ }
+ } else {
+ console.log('返回数据是对象')
+ for (const key in dbProductRaw) {
+ console.log(` ${key}:`, dbProductRaw[key], typeof dbProductRaw[key])
+ }
+ }
+ }
} catch (e) {
console.error('Failed to load product from DB', e)
}
+ // 处理数据库返回数据:可能是数组或对象
+ let dbProduct = null
+ if (dbProductRaw) {
+ if (Array.isArray(dbProductRaw)) {
+ if (dbProductRaw.length > 0) {
+ dbProduct = dbProductRaw[0] // 取数组第一个元素
+ } else {
+ console.warn('数据库返回空数组')
+ }
+ } else {
+ dbProduct = dbProductRaw // 已经是对象
+ }
+ }
+
if (dbProduct) {
console.log('使用数据库数据渲染页面')
- // 使用数据库数据
- const images = [] as Array
- if (dbProduct.image) images.push(dbProduct.image)
- else if (options.image) images.push(decodeURIComponent(options.image as string))
- else images.push('/static/product1.jpg')
- // 补充模拟图片
- images.push('/static/product2.jpg')
- images.push('/static/product3.jpg')
+ // 调试:打印dbProduct的详细结构和类型
+ console.log('dbProduct类型:', typeof dbProduct)
+ console.log('dbProduct原型:', Object.getPrototypeOf(dbProduct))
+ console.log('dbProduct的键:')
+ for (let key in dbProduct) {
+ console.log(' ', key, ':', dbProduct[key], '类型:', typeof dbProduct[key])
+ }
+
+ // 验证必要字段,如果关键字段缺失则使用模拟数据
+ // 注意:数据库返回的字段可能与本地ProductType不完全匹配
+ console.log('验证必要字段,dbProduct:', dbProduct)
+
+ // 尝试多种方式访问属性
+ const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
+ const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
+ const priceValue = dbProduct.price !== undefined ? dbProduct.price : (dbProduct['price'] !== undefined ? dbProduct['price'] : undefined)
+
+ const hasId = idValue !== undefined && idValue !== null
+ const hasName = nameValue !== undefined && nameValue !== null
+ const hasPrice = priceValue !== undefined && priceValue !== null
+
+ const hasRequiredFields = dbProduct && hasId && hasName && hasPrice
+ console.log('字段检查 - id:', idValue, 'hasId:', hasId, 'name:', nameValue, 'hasName:', hasName, 'price:', priceValue, 'hasPrice:', hasPrice)
+ console.log('hasRequiredFields:', hasRequiredFields)
+
+ if (!hasRequiredFields) {
+ console.warn('数据库返回数据缺少必要字段,使用模拟数据')
+ // 继续执行,会进入下面的else分支
+ dbProduct = null
+ } else {
+ // 更新dbProduct的字段为实际值,确保后续使用正确的属性访问
+ if (dbProduct.id === undefined && idValue !== undefined) dbProduct.id = idValue
+ if (dbProduct.name === undefined && nameValue !== undefined) dbProduct.name = nameValue
+ if (dbProduct.price === undefined && priceValue !== undefined) dbProduct.price = priceValue
+ // 使用数据库数据 - 处理字段映射
+ // 数据库Product接口和本地ProductType接口字段可能不同
+ const images = [] as Array
+
+ // 处理图片字段:优先使用images字段,其次使用image字段
+ console.log('处理数据库图片字段:')
+ console.log('dbProduct.images:', dbProduct.images, '类型:', typeof dbProduct.images)
+ console.log('dbProduct.image:', dbProduct.image, '类型:', typeof dbProduct.image)
+
+ // 尝试从数据库的images字段获取图片(可能是字符串或数组)
+ if (dbProduct.images) {
+ let imagesArray: any[] = []
+ if (typeof dbProduct.images === 'string') {
+ try {
+ imagesArray = JSON.parse(dbProduct.images)
+ console.log('解析images字符串成功:', imagesArray)
+ } catch (e) {
+ console.error('解析images字段失败:', e, dbProduct.images)
+ // 如果不是JSON,尝试按逗号分割
+ if (dbProduct.images.includes(',')) {
+ imagesArray = dbProduct.images.split(',').map((img: string) => img.trim())
+ } else if (dbProduct.images) {
+ imagesArray = [dbProduct.images]
+ }
+ }
+ } else if (Array.isArray(dbProduct.images)) {
+ imagesArray = dbProduct.images
+ }
+
+ if (imagesArray.length > 0) {
+ console.log('从数据库images字段获取图片数组:', imagesArray)
+ for (const img of imagesArray) {
+ if (typeof img === 'string' && img) {
+ images.push(img)
+ }
+ }
+ }
+ }
+
+ // 如果没有从images字段获取到图片,尝试使用image字段
+ if (images.length === 0 && dbProduct.image) {
+ console.log('使用单张图片字段:', dbProduct.image)
+ images.push(dbProduct.image)
+ }
+
+ // 如果仍然没有图片,使用传入的图片或默认图片
+ if (images.length === 0) {
+ if (options.image) {
+ images.push(decodeURIComponent(options.image as string))
+ } else {
+ images.push('/static/product1.jpg')
+ }
+ }
+
+ // 补充模拟图片(如果图片数量不足3张)
+ const needSupplementCount = 3 - images.length
+ if (needSupplementCount > 0) {
+ const supplementalImages = ['/static/product2.jpg', '/static/product3.jpg']
+ for (let i = 0; i < needSupplementCount && i < supplementalImages.length; i++) {
+ images.push(supplementalImages[i])
+ }
+ }
+
+ console.log('最终图片数组:', images)
+
+ // 映射字段:数据库shop_id对应本地merchant_id
+ const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
+
+ // 确保数值字段有效
+ const price = typeof dbProduct.price === 'number' ? dbProduct.price : 0
+ const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : 100
+ const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : 50
- this.product = {
- id: dbProduct.id,
- merchant_id: dbProduct.shop_id || 'merchant_001',
- category_id: dbProduct.category_id,
- name: dbProduct.name,
- description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
- images: images,
- price: dbProduct.price,
- original_price: dbProduct.original_price || null,
- stock: dbProduct.stock !== undefined ? dbProduct.stock : 0, // 确保使用数据库中的库存
- sales: dbProduct.sales !== undefined ? dbProduct.sales : 0, // 确保使用数据库中的销量
- status: 1,
- created_at: dbProduct.created_at || '2024-01-01'
- } as ProductType
- console.log('页面 product 对象已更新:', this.product)
+ this.product = {
+ id: dbProduct.id || productId,
+ merchant_id: merchantId,
+ category_id: dbProduct.category_id || 'cat_001',
+ name: dbProduct.name || '商品名称',
+ description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
+ images: images,
+ price: price,
+ original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : null,
+ stock: stock,
+ sales: sales,
+ status: 1,
+ created_at: dbProduct.created_at || '2024-01-01',
+ // 药品相关字段
+ specification: dbProduct.specification || null,
+ usage: dbProduct.usage || null,
+ side_effects: dbProduct.side_effects || null,
+ precautions: dbProduct.precautions || null,
+ expiry_date: dbProduct.expiry_date || null,
+ storage_conditions: dbProduct.storage_conditions || null,
+ approval_number: dbProduct.approval_number || null,
+ tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
+ } as ProductType
+ console.log('页面 product 对象已更新:', this.product)
+ console.log('商品图片数组:', this.product.images)
+ console.log('商品价格:', this.product.price, '库存:', this.product.stock, '销量:', this.product.sales)
+ }
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
@@ -378,6 +593,10 @@ export default {
}
]
},
+
+ onSwiperChange(e: any) {
+ this.currentImageIndex = e.detail.current
+ },
showSpecModal() {
this.showSpec = true
@@ -626,6 +845,21 @@ export default {
getAvailableStock() {
return this.getMaxQuantity()
+ },
+
+ previewImage(index: number) {
+ uni.previewImage({
+ current: index,
+ urls: this.product.images
+ })
+ },
+
+ showParamsModal() {
+ this.showParams = true
+ },
+
+ hideParamsModal() {
+ this.showParams = false
}
}
}
@@ -993,4 +1227,166 @@ export default {
width: 100rpx;
text-align: right;
}
+
+/* 功能主治样式 */
+.function-section {
+ background-color: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+}
+
+.function-title {
+ font-size: 30rpx;
+ color: #333;
+ font-weight: bold;
+ margin-bottom: 15rpx;
+ display: block;
+}
+
+.function-content {
+ font-size: 28rpx;
+ color: #666;
+ line-height: 1.5;
+}
+
+/* 商品参数样式 */
+.params-section {
+ background-color: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ display: flex;
+ align-items: center;
+}
+
+.params-title {
+ font-size: 30rpx;
+ color: #333;
+ width: 120rpx;
+ flex-shrink: 0;
+}
+
+.params-summary {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.params-item {
+ font-size: 26rpx;
+ color: #666;
+ line-height: 1.5;
+ margin-right: 20rpx;
+ margin-bottom: 5rpx;
+ white-space: nowrap;
+}
+
+.params-arrow {
+ font-size: 28rpx;
+ color: #999;
+ flex-shrink: 0;
+ margin-left: 10rpx;
+}
+
+/* 商品参数弹窗样式 */
+.params-modal {
+ 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;
+}
+
+.params-content {
+ background-color: #fff;
+ width: 100%;
+ max-height: 80vh;
+ border-radius: 20rpx 20rpx 0 0;
+ padding: 30rpx;
+}
+
+.params-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30rpx;
+ padding-bottom: 20rpx;
+ border-bottom: 1rpx solid #eee;
+}
+
+.params-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ width: auto;
+}
+
+.params-list {
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.params-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f5f5f5;
+}
+
+.params-label {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: bold;
+ width: 150rpx;
+ flex-shrink: 0;
+}
+
+.params-value {
+ flex: 1;
+ font-size: 28rpx;
+ color: #666;
+ line-height: 1.5;
+}
+
+/* 商品详情图片样式 */
+.detail-images {
+ margin-top: 30rpx;
+}
+
+.detail-image {
+ width: 100%;
+ margin-bottom: 20rpx;
+ border-radius: 10rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+}
+
+/* 电脑端适配 */
+@media (min-width: 768px) {
+ .params-section {
+ padding: 20rpx 30rpx;
+ }
+
+ .params-summary {
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ }
+
+ .params-item {
+ flex: 1;
+ margin-right: 0;
+ text-align: center;
+ white-space: normal;
+ word-break: break-word;
+ padding: 0 10rpx;
+ }
+
+ .params-arrow {
+ margin-left: 20rpx;
+ }
+}
diff --git a/types/mall-types.uts b/types/mall-types.uts
index 6a54f7ee..de9460f9 100644
--- a/types/mall-types.uts
+++ b/types/mall-types.uts
@@ -85,6 +85,15 @@ export type ProductType = {
sales: number
status: number
created_at: string
+ // 药品相关字段
+ specification?: string | null // 规格说明
+ usage?: string | null // 用法用量
+ side_effects?: string | null // 副作用
+ precautions?: string | null // 注意事项
+ expiry_date?: string | null // 有效期
+ storage_conditions?: string | null // 储存条件
+ approval_number?: string | null // 批准文号
+ tags?: Array | null // 商品标签
}
// 商品SKU类型
diff --git a/utils/supabaseService.uts b/utils/supabaseService.uts
index bac53939..3ee23527 100644
--- a/utils/supabaseService.uts
+++ b/utils/supabaseService.uts
@@ -25,14 +25,46 @@ export interface Product {
original_price?: number
image?: string
manufacturer: string
- sales: number
- stock: number
+ sales?: number
+ stock?: number
badge?: string
shop_id?: string
shop_name?: string
created_at?: string
}
+export interface CartItem {
+ id: string
+ user_id: string
+ product_id: string
+ sku_id?: string
+ quantity: number
+ selected: boolean
+ product_name?: string
+ product_image?: string
+ product_price?: number
+ product_specification?: string
+ shop_id?: string
+ shop_name?: string
+ created_at?: string
+ updated_at?: string
+}
+
+export interface UserAddress {
+ id: string
+ user_id: string
+ recipient_name: string
+ phone: string
+ province: string
+ city: string
+ district: string
+ detail_address: string
+ postal_code?: string
+ is_default: boolean
+ created_at?: string
+ updated_at?: string
+}
+
export interface PaginatedResponse {
data: T[]
total: number
@@ -42,6 +74,17 @@ export interface PaginatedResponse {
}
class SupabaseService {
+ // 获取当前用户ID
+ private getCurrentUserId(): string | null {
+ try {
+ const userId = uni.getStorageSync('user_id')
+ return userId ? userId as string : null
+ } catch (e) {
+ console.error('获取用户ID失败:', e)
+ return null
+ }
+ }
+
// 获取所有分类
async getCategories(): Promise {
try {
@@ -176,7 +219,7 @@ class SupabaseService {
.select('*')
.eq('id', productId)
.single()
- .execute()
+ .executeAs()
if (response.error) {
console.error('获取商品详情失败:', response.error)
@@ -304,6 +347,561 @@ class SupabaseService {
return []
}
}
+
+ // 获取当前用户的购物车商品(关联商品和店铺信息)
+ async getCartItems(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.warn('用户未登录,无法获取购物车')
+ return []
+ }
+
+ // 查询购物车表,并关联商品表(使用内联关联)
+ const response = await supa
+ .from('ml_shopping_cart')
+ .select(`
+ id,
+ user_id,
+ product_id,
+ sku_id,
+ quantity,
+ selected,
+ created_at,
+ updated_at,
+ products!inner (
+ id,
+ name,
+ image,
+ price,
+ specification,
+ shop_id,
+ shop_name
+ )
+ `)
+ .eq('user_id', userId)
+ .order('created_at', { ascending: false })
+ .execute()
+
+ if (response.error) {
+ console.error('获取购物车失败:', response.error)
+ return []
+ }
+
+ // 处理返回数据,构建CartItem数组
+ const cartItems: CartItem[] = []
+ if (response.data && Array.isArray(response.data)) {
+ for (const item of response.data) {
+ const product = item.products as any
+
+ cartItems.push({
+ id: item.id as string,
+ user_id: item.user_id as string,
+ product_id: item.product_id as string,
+ sku_id: item.sku_id as string,
+ quantity: item.quantity as number,
+ selected: item.selected as boolean,
+ product_name: product?.name as string,
+ product_image: product?.image as string,
+ product_price: product?.price as number,
+ product_specification: product?.specification as string,
+ shop_id: product?.shop_id as string,
+ shop_name: product?.shop_name as string,
+ created_at: item.created_at as string,
+ updated_at: item.updated_at as string
+ })
+ }
+ }
+
+ return cartItems
+ } catch (error) {
+ console.error('获取购物车异常:', error)
+ return []
+ }
+ }
+
+ // 添加商品到购物车
+ async addToCart(productId: string, quantity: number = 1, skuId?: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法添加商品到购物车')
+ return false
+ }
+
+ // 检查商品是否已在购物车中
+ const existingResponse = await supa
+ .from('ml_shopping_cart')
+ .select('*')
+ .eq('user_id', userId)
+ .eq('product_id', productId)
+ .eq('sku_id', skuId || '')
+ .single()
+ .execute()
+
+ let response
+ if (existingResponse.data) {
+ // 商品已存在,更新数量
+ const existingItem = existingResponse.data as any
+ response = await supa
+ .from('ml_shopping_cart')
+ .update({
+ quantity: (existingItem.quantity || 0) + quantity,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', existingItem.id)
+ .execute()
+ } else {
+ // 商品不存在,添加新记录
+ response = await supa
+ .from('ml_shopping_cart')
+ .insert({
+ user_id: userId,
+ product_id: productId,
+ sku_id: skuId || null,
+ quantity: quantity,
+ selected: true,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+ }
+
+ if (response.error) {
+ console.error('添加商品到购物车失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('添加商品到购物车异常:', error)
+ return false
+ }
+ }
+
+ // 更新购物车商品数量
+ async updateCartItemQuantity(cartItemId: string, quantity: number): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法更新购物车')
+ return false
+ }
+
+ if (quantity < 1) {
+ // 数量小于1时删除商品
+ return await this.deleteCartItem(cartItemId)
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .update({
+ quantity: quantity,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', cartItemId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('更新购物车商品数量失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('更新购物车商品数量异常:', error)
+ return false
+ }
+ }
+
+ // 更新购物车商品选中状态
+ async updateCartItemSelection(cartItemId: string, selected: boolean): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法更新购物车')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .update({
+ selected: selected,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', cartItemId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('更新购物车商品选中状态失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('更新购物车商品选中状态异常:', error)
+ return false
+ }
+ }
+
+ // 批量更新购物车商品选中状态
+ async batchUpdateCartItemSelection(cartItemIds: string[], selected: boolean): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法更新购物车')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .update({
+ selected: selected,
+ updated_at: new Date().toISOString()
+ })
+ .eq('user_id', userId)
+ .in('id', cartItemIds)
+ .execute()
+
+ if (response.error) {
+ console.error('批量更新购物车商品选中状态失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('批量更新购物车商品选中状态异常:', error)
+ return false
+ }
+ }
+
+ // 删除购物车商品
+ async deleteCartItem(cartItemId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法删除购物车商品')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .delete()
+ .eq('id', cartItemId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('删除购物车商品失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('删除购物车商品异常:', error)
+ return false
+ }
+ }
+
+ // 批量删除购物车商品
+ async batchDeleteCartItems(cartItemIds: string[]): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法删除购物车商品')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .delete()
+ .eq('user_id', userId)
+ .in('id', cartItemIds)
+ .execute()
+
+ if (response.error) {
+ console.error('批量删除购物车商品失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('批量删除购物车商品异常:', error)
+ return false
+ }
+ }
+
+ // 清空购物车
+ async clearCart(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法清空购物车')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_shopping_cart')
+ .delete()
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('清空购物车失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('清空购物车异常:', error)
+ return false
+ }
+ }
+
+ // 获取当前用户的所有地址
+ async getAddresses(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.warn('用户未登录,无法获取地址')
+ return []
+ }
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .select('*')
+ .eq('user_id', userId)
+ .order('is_default', { ascending: false })
+ .order('created_at', { ascending: false })
+ .execute()
+
+ if (response.error) {
+ console.error('获取地址失败:', response.error)
+ return []
+ }
+
+ return response.data as UserAddress[]
+ } catch (error) {
+ console.error('获取地址异常:', error)
+ return []
+ }
+ }
+
+ // 根据ID获取地址详情
+ async getAddressById(addressId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.warn('用户未登录,无法获取地址')
+ return null
+ }
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .select('*')
+ .eq('id', addressId)
+ .eq('user_id', userId)
+ .single()
+ .execute()
+
+ if (response.error) {
+ console.error('获取地址详情失败:', response.error)
+ return null
+ }
+
+ return response.data as UserAddress
+ } catch (error) {
+ console.error('获取地址详情异常:', error)
+ return null
+ }
+ }
+
+ // 添加新地址
+ async addAddress(address: {
+ recipient_name: string
+ phone: string
+ province: string
+ city: string
+ district: string
+ detail_address: string
+ postal_code?: string
+ is_default?: boolean
+ }): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法添加地址')
+ return false
+ }
+
+ // 如果设置为默认地址,需要先取消其他默认地址
+ if (address.is_default) {
+ await this.clearDefaultAddress(userId)
+ }
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .insert({
+ user_id: userId,
+ recipient_name: address.recipient_name,
+ phone: address.phone,
+ province: address.province,
+ city: address.city,
+ district: address.district,
+ detail_address: address.detail_address,
+ postal_code: address.postal_code || null,
+ is_default: address.is_default || false,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+
+ if (response.error) {
+ console.error('添加地址失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('添加地址异常:', error)
+ return false
+ }
+ }
+
+ // 更新地址
+ async updateAddress(addressId: string, address: {
+ recipient_name?: string
+ phone?: string
+ province?: string
+ city?: string
+ district?: string
+ detail_address?: string
+ postal_code?: string
+ is_default?: boolean
+ }): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法更新地址')
+ return false
+ }
+
+ // 如果设置为默认地址,需要先取消其他默认地址
+ if (address.is_default) {
+ await this.clearDefaultAddress(userId)
+ }
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .update({
+ ...address,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', addressId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('更新地址失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('更新地址异常:', error)
+ return false
+ }
+ }
+
+ // 删除地址
+ async deleteAddress(addressId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法删除地址')
+ return false
+ }
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .delete()
+ .eq('id', addressId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('删除地址失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('删除地址异常:', error)
+ return false
+ }
+ }
+
+ // 设置默认地址
+ async setDefaultAddress(addressId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) {
+ console.error('用户未登录,无法设置默认地址')
+ return false
+ }
+
+ // 先取消所有默认地址
+ await this.clearDefaultAddress(userId)
+
+ // 设置新的默认地址
+ const response = await supa
+ .from('ml_user_addresses')
+ .update({
+ is_default: true,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', addressId)
+ .eq('user_id', userId)
+ .execute()
+
+ if (response.error) {
+ console.error('设置默认地址失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('设置默认地址异常:', error)
+ return false
+ }
+ }
+
+ // 清除用户的默认地址(内部方法)
+ private async clearDefaultAddress(userId: string): Promise {
+ try {
+ const response = await supa
+ .from('ml_user_addresses')
+ .update({
+ is_default: false,
+ updated_at: new Date().toISOString()
+ })
+ .eq('user_id', userId)
+ .eq('is_default', true)
+ .execute()
+
+ if (response.error) {
+ console.error('清除默认地址失败:', response.error)
+ return false
+ }
+
+ return true
+ } catch (error) {
+ console.error('清除默认地址异常:', error)
+ return false
+ }
+ }
}
// 导出单例实例
From 5f856a96c908172136f1e3135837c3433aa79111 Mon Sep 17 00:00:00 2001
From: not-like-juvenile <16056107+not-like-juvenile@user.noreply.gitee.com>
Date: Fri, 30 Jan 2026 21:11:17 +0800
Subject: [PATCH 11/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=BA=93=E5=B9=B6=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2=E6=A0=B7?=
=?UTF-8?q?=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ak/config.uts | 4 +-
pages/mall/delivery/index.uvue | 22 +++---
pages/mall/delivery/order-detail.uvue | 99 ++++++++-------------------
3 files changed, 41 insertions(+), 84 deletions(-)
diff --git a/ak/config.uts b/ak/config.uts
index a24c488a..2b48e5da 100644
--- a/ak/config.uts
+++ b/ak/config.uts
@@ -2,11 +2,11 @@
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.62
// Kong HTTP Port: 8000
-export const SUPA_URL: string = 'http://192.168.1.62:8000'
+export const SUPA_URL: string = 'http://192.168.1.62:13000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://)
-export const WS_URL: string = 'ws://192.168.1.62:8000/realtime/v1/websocket'
+export const WS_URL: string = 'ws://192.168.1.62:13000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)
// 开发环境 - 其他内网地址
diff --git a/pages/mall/delivery/index.uvue b/pages/mall/delivery/index.uvue
index 7e785649..0b5f98fe 100644
--- a/pages/mall/delivery/index.uvue
+++ b/pages/mall/delivery/index.uvue
@@ -454,7 +454,7 @@
confirmPickup() {
// TODO: 调用API确认取货
if (this.currentTask) {
- this.currentTask.status = 4 // 更新状态为“已取货”
+ this.currentTask.status = 5 // 更新状态为“已取货”
}
uni.showToast({
title: '取货完成',
@@ -462,16 +462,16 @@
})
},
- startDelivery() {
- // TODO: 调用API开始配送
- if (this.currentTask) {
- this.currentTask.status = 5 // 更新状态为“配送中”
- }
- uni.showToast({
- title: '开始配送',
- icon: 'success'
- })
- },
+ // startDelivery() {
+ // // TODO: 调用API开始配送
+ // if (this.currentTask) {
+ // this.currentTask.status = 5 // 更新状态为“配送中”
+ // }
+ // uni.showToast({
+ // title: '开始配送',
+ // icon: 'success'
+ // })
+ // },
// 显示确认送达弹框
showConfirmDeliveryDialog() {
diff --git a/pages/mall/delivery/order-detail.uvue b/pages/mall/delivery/order-detail.uvue
index 68bc94d4..68a50b34 100644
--- a/pages/mall/delivery/order-detail.uvue
+++ b/pages/mall/delivery/order-detail.uvue
@@ -1,4 +1,3 @@
-
@@ -180,16 +179,6 @@
diff --git a/pages/mall/analytics/delivery-analysis.uvue b/pages/mall/analytics/delivery-analysis.uvue
index a267dead..36625af4 100644
--- a/pages/mall/analytics/delivery-analysis.uvue
+++ b/pages/mall/analytics/delivery-analysis.uvue
@@ -106,7 +106,6 @@
-
@@ -115,338 +114,326 @@
-
diff --git a/pages/mall/analytics/market-trends.uvue b/pages/mall/analytics/market-trends.uvue
index 03e0ee5a..cca780fb 100644
--- a/pages/mall/analytics/market-trends.uvue
+++ b/pages/mall/analytics/market-trends.uvue
@@ -93,298 +93,305 @@
-
diff --git a/pages/mall/analytics/product-insights.uvue b/pages/mall/analytics/product-insights.uvue
index c98eb294..1c362575 100644
--- a/pages/mall/analytics/product-insights.uvue
+++ b/pages/mall/analytics/product-insights.uvue
@@ -50,7 +50,7 @@
热销商品
{{ formatInt(productData.hot_products) }}
- 销量 > 100
+ 销量 > 100
库存周转率
@@ -70,7 +70,7 @@
商品销售分析
@@ -167,274 +167,307 @@
-
diff --git a/pages/mall/analytics/sales-report.uvue b/pages/mall/analytics/sales-report.uvue
index a5780af2..d7a974ca 100644
--- a/pages/mall/analytics/sales-report.uvue
+++ b/pages/mall/analytics/sales-report.uvue
@@ -148,242 +148,189 @@
-
diff --git a/pages/mall/analytics/user-analysis.uvue b/pages/mall/analytics/user-analysis.uvue
index 569da41f..3d5c3794 100644
--- a/pages/mall/analytics/user-analysis.uvue
+++ b/pages/mall/analytics/user-analysis.uvue
@@ -27,36 +27,55 @@
-
-
-
- {{ p.label }}
+
+
+
+ 时间范围
+
+
+ {{ p.label }}
+
+
+
+
+
+ 渠道/终端/会员/新老:待接入数据后开放
-
-
+
+
- 总用户数
- {{ formatInt(userData.total_users) }}
- 较上期:{{ formatPct(userData.user_growth) }}
-
-
- 新用户
+ 新增用户
{{ formatInt(userData.new_users) }}
较上期:{{ formatPct(userData.new_user_growth) }}
- 用户活跃度
- {{ formatPct(userData.active_rate) }}
+ 活跃用户(DAU)
+ {{ formatInt(userData.active_users) }}
较上期:{{ formatPct(userData.active_growth) }}
+
+ 下单用户数
+ {{ formatInt(userData.ordering_users) }}
+ 较上期:{{ formatPct(userData.ordering_growth) }}
+
+
+ 支付用户数
+ {{ formatInt(userData.paid_users) }}
+ 较上期:{{ formatPct(userData.paid_growth) }}
+
+
+ 新客转化率
+ {{ formatPct(userData.new_user_conversion_rate) }}
+ 新客 → 下单/支付
+
复购率
{{ formatPct(userData.repurchase_rate) }}
@@ -64,11 +83,11 @@
-
+
- 用户增长趋势
- {{ selectedPeriodText }} · 新用户 vs 总用户
+ 增长与活跃趋势
+ {{ selectedPeriodText }} · 新增 vs 活跃(DAU)
{{ loading ? '加载中...' : '暂无数据' }}
@@ -76,60 +95,110 @@
-
-
-
- 用户洞察
- 留存率 / 新老对比 / 活跃度 / 画像
+
+
+
+ 新客转化趋势
+ 新客 → 下单/支付(待接入)
+
+
+ 暂无数据 / 待接入
+
-
-
-
- 用户留存率
- 按留存天数统计
-
-
+
+
+
+ 回访 / 复购趋势
+ 复购人数 / 复购率(待接入)
-
-
- 新老用户对比
- GMV、订单数、客单价
-
-
-
-
-
- 用户活跃度
- 日活/周活/月活
-
-
-
-
-
- 用户画像
- 性别/年龄/地域
-
-
+
+ 暂无数据 / 待接入
-
+
- 用户分群 & 流量来源
- {{ selectedPeriodText }} · 分群占比 & 来源分布
+ 转化漏斗
+ 拉新 → 激活 → 转化(待接入埋点/事件)
+
+
+
+
+
+ {{ idx + 1 }}
+ {{ s.step }}
+
+
+ {{ formatInt(s.value) }}
+ 转化:{{ formatPct(calcFunnelRate(idx)) }}
+ —
+
+
+
+
+
+ 暂无漏斗数据 / 待接入:UV、PDP、加购、下单、支付
+
+
+
+
+
+
+
+ 留存与回访
+ {{ selectedPeriodText }} · 1/3/7/14/30日留存(Cohort 后续补)
+
+
+
+
+ 留存曲线
+ 留存率趋势(待接入)
+
+
+
+
+
+ 流失用户占比
+ 7/14天未活跃(待接入)
+
+
+ 暂无数据 / 待接入
+
+
+
+
+
+
+
+
+ 用户分群(运营可用)
+ RFM / LTV / 新客分层(后续补) · 当前为基础结构占比
-
+
+ 用户画像(基础)
+ 性别/年龄/地域(待接入)
+
+
+
+
+
+ 渠道来源
+ {{ selectedPeriodText }} · 渠道占比(后续可扩展渠道质量表)
+
+
+
+
@@ -138,272 +207,321 @@
-
diff --git a/services/analytics/couponAnalysisService.uts b/services/analytics/couponAnalysisService.uts
index d7cae687..960195b0 100644
--- a/services/analytics/couponAnalysisService.uts
+++ b/services/analytics/couponAnalysisService.uts
@@ -1,4 +1,4 @@
-import { computeDateRange } from './dateRange.uts'
+import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
export type CouponAnalysisData = {
@@ -11,31 +11,21 @@ export type CouponAnalysisData = {
export async function fetchCouponAnalysis(period: string): Promise {
const { startIso, endIso } = computeDateRange(period)
+ const p_start_date = toDateOnly(startIso)
+ const p_end_date = toDateOnly(endIso)
- const overviewRow = await rpcOrNull('rpc_coupon_effectiveness_overview', {
- p_start: startIso,
- p_end: endIso
- } as UTSJSONObject)
+ const params = {
+ p_start_date,
+ p_end_date
+ } as any
- const typeList = await rpcOrEmptyArray('rpc_coupon_type_stats', {
- p_start: startIso,
- p_end: endIso
- } as UTSJSONObject)
-
- const channelList = await rpcOrEmptyArray('rpc_coupon_channel_stats', {
- p_start: startIso,
- p_end: endIso
- } as UTSJSONObject)
-
- const trendList = await rpcOrEmptyArray('rpc_coupon_trend_daily', {
- p_start: startIso,
- p_end: endIso
- } as UTSJSONObject)
-
- const conversionList = await rpcOrEmptyArray('rpc_coupon_conversion_effect', {
- p_start: startIso,
- p_end: endIso
- } as UTSJSONObject)
+ const [overviewRow, typeList, channelList, trendList, conversionList] = await Promise.all([
+ rpcOrNull('rpc_analytics_coupon_overview', params),
+ rpcOrEmptyArray('rpc_analytics_coupon_by_type', params),
+ rpcOrEmptyArray('rpc_analytics_coupon_by_channel', params),
+ rpcOrEmptyArray('rpc_analytics_coupon_trend', params),
+ rpcOrEmptyArray('rpc_analytics_coupon_conversion', params)
+ ])
return { overviewRow, typeList, channelList, trendList, conversionList }
}
diff --git a/services/analytics/customReportService.uts b/services/analytics/customReportService.uts
index 0e8efcc9..b5ddc587 100644
--- a/services/analytics/customReportService.uts
+++ b/services/analytics/customReportService.uts
@@ -1,5 +1,4 @@
-import supa from '@/components/supadb/aksupainstance.uts'
-import { rpcOrValue } from './rpc.uts'
+import { rpcOrEmptyArray, rpcOrValue } from './rpc.uts'
export type CustomReportListItem = {
id: string
@@ -24,41 +23,41 @@ export type UpdateCustomReportParams = {
period: string | null
}
+function safeString(v: any): string {
+ return v != null ? `${v}` : ''
+}
+
+// 改造:不再直查 analytics_reports 表,统一通过 RPC 获取当前用户的报表列表
export async function listCustomReports(ownerUserId: string): Promise> {
- const res: any = await supa
- .from('analytics_reports')
- .select('id, title, description, period, updated_at')
- .eq('type', 'custom')
- .eq('owner_user_id', ownerUserId)
- .order('updated_at', { ascending: false } as any)
-
- if (res?.error != null) {
- throw res.error
- }
-
- const rows: Array = Array.isArray(res.data) ? (res.data as Array) : []
+ const rows = await rpcOrEmptyArray('rpc_get_custom_reports', {} as any)
const list: Array = []
for (let i = 0; i < rows.length; i++) {
- const r = rows[i]
+ const r: any = rows[i]
list.push({
- id: `${r.id}`,
- title: `${r.title}`,
- description: `${r.description || ''}`,
- period: `${r.period || ''}`,
- updated_at: `${r.updated_at || ''}`
+ id: safeString(r.getAny?.('id') ?? r.getString?.('id')),
+ title: safeString(r.getAny?.('title') ?? r.getString?.('title')),
+ description: safeString(r.getAny?.('description') ?? r.getString?.('description')),
+ // 兼容旧 UI 字段:custom-report 页面里可能还在用 period 字段
+ period: '',
+ updated_at: safeString(r.getAny?.('updated_at') ?? r.getString?.('updated_at'))
})
}
return list
}
+// 改造:RPC 参数改为 p_definition(JSONB),承载 period/metrics/chartType
export async function createCustomReport(params: CreateCustomReportParams): Promise {
+ const definition = {
+ period: params.period,
+ metrics: params.metrics,
+ chartType: params.chartType || 'line'
+ }
+
const data = await rpcOrValue('rpc_create_custom_report', {
p_title: params.title,
p_description: params.description || '',
- p_period: params.period,
- p_metrics: params.metrics,
- p_chart_type: params.chartType || 'line'
- } as UTSJSONObject)
+ p_definition: definition
+ } as any)
if (data == null) {
throw new Error('保存失败:未返回报表ID')
@@ -68,12 +67,17 @@ export async function createCustomReport(params: CreateCustomReportParams): Prom
}
export async function updateCustomReport(params: UpdateCustomReportParams): Promise {
+ // 注意:旧 UI 只传 title/description/period,这里把 period 合并进 definition
+ const definition = {
+ period: params.period
+ }
+
await rpcOrValue('rpc_update_custom_report', {
p_report_id: params.reportId,
p_title: params.title,
p_description: params.description,
- p_period: params.period
- } as UTSJSONObject)
+ p_definition: definition
+ } as any)
return true
}
@@ -81,7 +85,7 @@ export async function updateCustomReport(params: UpdateCustomReportParams): Prom
export async function deleteCustomReport(reportId: string): Promise {
await rpcOrValue('rpc_delete_custom_report', {
p_report_id: reportId
- } as UTSJSONObject)
+ } as any)
return true
}
diff --git a/services/analytics/dashboardService.uts b/services/analytics/dashboardService.uts
index c8ece9ee..453e212b 100644
--- a/services/analytics/dashboardService.uts
+++ b/services/analytics/dashboardService.uts
@@ -1,5 +1,5 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
-import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
+import { rpcOrEmptyArray, rpcOrNull, rpcOrValue } from './rpc.uts'
export type TrendData = { x: Array; gmv: Array; orders: Array }
export type SegmentItem = { name: string; value: number }
@@ -17,10 +17,9 @@ export async function fetchDashboardTrend(period: string): Promise {
const p_start_date = toDateOnly(startIso)
const p_end_date = toDateOnly(endIso)
- const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', {
+ const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', {
p_start_date,
- p_end_date,
- p_merchant_id: null
+ p_end_date
} as any)
const x: Array = []
@@ -28,45 +27,30 @@ export async function fetchDashboardTrend(period: string): Promise {
const orders: Array = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
- const d = `${row.getString?.('date') ?? row.getString?.('day') ?? row.getString?.('date_key') ?? ''}`
- if (d && d.length >= 10) x.push(d.slice(5))
- else x.push(`${i + 1}`)
- gmv.push(safeNumber(row.getAny?.('gmv') ?? row.getAny?.('total_amount') ?? 0))
- orders.push(safeNumber(row.getAny?.('orders') ?? row.getAny?.('order_count') ?? 0))
+ const d = `${row.getAny?.('date') ?? ''}`
+ x.push(d.length >= 10 ? d.slice(5) : d)
+ gmv.push(safeNumber(row.getAny?.('gmv') ?? 0))
+ orders.push(safeNumber(row.getAny?.('orders') ?? 0))
}
return { x, gmv, orders }
}
export async function fetchDashboardRealtime(): Promise {
- const now = new Date()
- const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- const todayISO = today0.toISOString()
+ const [kpiRow, onlineUsersVal] = await Promise.all([
+ rpcOrNull('rpc_analytics_realtime_kpis', {} as any),
+ rpcOrValue('rpc_analytics_online_users', {} as any)
+ ])
- const ySame = new Date(now.getTime() - 24 * 60 * 60 * 1000)
- const y0 = new Date(ySame.getFullYear(), ySame.getMonth(), ySame.getDate())
+ const obj: any = kpiRow != null ? kpiRow : ({} as any)
- const row = await rpcOrNull('rpc_analytics_realtime_kpis', {
- p_start: todayISO,
- p_end: now.toISOString(),
- p_compare_start: y0.toISOString(),
- p_compare_end: ySame.toISOString(),
- p_merchant_id: null
- } as any)
-
- const safe = (v: any): number => {
- const n = Number(v)
- return isFinite(n) ? n : 0
- }
-
- const obj: any = row != null ? row : ({} as any)
return {
- gmv: Math.round(safe(obj.getAny?.('gmv') ?? obj.getAny?.('total_gmv') ?? obj.getAny?.('revenue') ?? 0)),
- gmv_growth: safe(obj.getAny?.('gmv_growth') ?? obj.getAny?.('gmv_growth_rate') ?? obj.getAny?.('revenue_growth') ?? 0),
- orders: Math.round(safe(obj.getAny?.('orders') ?? obj.getAny?.('order_count') ?? obj.getAny?.('total_orders') ?? 0)),
- order_growth: safe(obj.getAny?.('order_growth') ?? obj.getAny?.('order_growth_rate') ?? 0),
- online_users: Math.round(safe(obj.getAny?.('online_users') ?? obj.getAny?.('active_users') ?? obj.getAny?.('current_users') ?? 0)),
- conversion_rate: safe(obj.getAny?.('conversion_rate') ?? obj.getAny?.('conversion') ?? 0),
- conversion_growth: safe(obj.getAny?.('conversion_growth') ?? obj.getAny?.('conversion_growth_rate') ?? 0)
+ gmv: Math.round(safeNumber(obj.getAny?.('gmv') ?? 0)),
+ gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0),
+ orders: Math.round(safeNumber(obj.getAny?.('orders') ?? 0)),
+ order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0),
+ online_users: Math.round(safeNumber(onlineUsersVal ?? 0)),
+ conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0),
+ conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0)
}
}
@@ -75,8 +59,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number =
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
- p_limit: limit,
- p_merchant_id: null
+ p_limit: limit
} as any)
const list: Array = []
@@ -86,7 +69,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number =
id: `${row.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${row.getAny?.('name') ?? '未知商品'}`,
- sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0)
+ sales: safeNumber(row.getAny?.('sales') ?? 0)
})
}
return list
@@ -107,8 +90,8 @@ export async function fetchDashboardTopMerchants(period: string, limit: number =
id: `${row.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${row.getAny?.('name') ?? row.getAny?.('shop_name') ?? '未知商家'}`,
- sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0),
- growth: safeNumber(row.getAny?.('growth') ?? row.getAny?.('growth_rate') ?? 0)
+ sales: safeNumber(row.getAny?.('sales') ?? 0),
+ growth: safeNumber(row.getAny?.('growth') ?? 0)
})
}
return list
diff --git a/services/analytics/dataDetailService.uts b/services/analytics/dataDetailService.uts
index 579c4fe9..6a3065cf 100644
--- a/services/analytics/dataDetailService.uts
+++ b/services/analytics/dataDetailService.uts
@@ -1,70 +1,58 @@
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
-export type DataDetailReportInfo = {
- period: string
-}
-
-export type DataDetailRow = {
+export type ReportInfo = {
id: string
- date: string
- gmv: number
- orders: number
- users: number
+ title: string
+ description: string
+ definition: any
+ updated_at: string
}
-export type DataDetailDrillItem = {
- id: string
- label: string
- value: string
- type: string
+function safeString(v: any): string {
+ return v != null ? `${v}` : ''
}
-export async function fetchDataDetailReportInfo(reportId: string): Promise {
- const info = await rpcOrNull('rpc_data_detail_report_info', {
+// 改造:调用 rpc_data_detail_report_info
+export async function fetchReportInfo(reportId: string): Promise {
+ const row = await rpcOrNull('rpc_data_detail_report_info', {
p_report_id: reportId
- } as UTSJSONObject)
- if (info == null) return null
- return { period: info.getString('period') ?? '' }
+ } as any)
+
+ if (row == null) return null
+
+ return {
+ id: safeString(row.getAny?.('id')),
+ title: safeString(row.getAny?.('title')),
+ description: safeString(row.getAny?.('description')),
+ definition: row.getAny?.('definition'),
+ updated_at: safeString(row.getAny?.('updated_at'))
+ }
}
-export async function fetchDataDetailRows(reportId: string, sortBy: string, sortDir: string, limit: number, offset: number): Promise> {
- const rows = await rpcOrEmptyArray('rpc_data_detail_rows', {
+// 改造:调用 rpc_data_detail_rows
+export async function fetchReportRows(reportId: string, params: any): Promise> {
+ const result = await rpcOrNull('rpc_data_detail_rows', {
p_report_id: reportId,
- p_sort_by: sortBy,
- p_sort_dir: sortDir,
- p_limit: limit,
- p_offset: offset
- } as UTSJSONObject)
+ p_params: params
+ } as any)
- const out: Array = []
- for (let i = 0; i < rows.length; i++) {
- const r = rows[i]
- const dayStr = r.getString('row_date') ?? ''
- out.push({
- id: dayStr + '_' + i.toString(),
- date: dayStr,
- gmv: r.getNumber('gmv') ?? 0,
- orders: r.getNumber('orders') ?? 0,
- users: r.getNumber('users') ?? 0
- })
- }
- return out
+ if (result == null) return []
+ const anyData = result as any
+ return Array.isArray(anyData) ? (anyData as Array) : ([] as Array)
}
-export async function fetchDataDetailDrillItems(reportId: string): Promise> {
- const rows = await rpcOrEmptyArray('rpc_data_detail_drill_items', {
- p_report_id: reportId
- } as UTSJSONObject)
-
- const out: Array = []
- for (let i = 0; i < rows.length; i++) {
- const r = rows[i]
- out.push({
- id: `${r.getAny('id') ?? i}`,
- label: `${r.getString('label') ?? ''}`,
- value: `${r.getAny('value') ?? ''}`,
- type: `${r.getString('type') ?? ''}`
- })
- }
- return out
+// 保留调用,但 RPC 是模拟数据
+export async function fetchDrilldown(reportId: string, itemId: string): Promise> {
+ return await rpcOrEmptyArray('rpc_data_detail_drill_items', {
+ p_report_id: reportId,
+ p_item_id: itemId
+ } as any)
+}
+
+// 保留调用,但 RPC 是模拟数据
+export async function fetchComparison(itemId: string, period: string): Promise> {
+ return await rpcOrEmptyArray('rpc_data_detail_compare_gmv', {
+ p_item_id: itemId,
+ p_period: period
+ } as any)
}
diff --git a/services/analytics/deliveryAnalysisService.uts b/services/analytics/deliveryAnalysisService.uts
index 548c7adc..4e4f9fe5 100644
--- a/services/analytics/deliveryAnalysisService.uts
+++ b/services/analytics/deliveryAnalysisService.uts
@@ -1,5 +1,5 @@
-import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import { computeDateRange } from './dateRange.uts'
+import { rpcOrEmptyArray } from './rpc.uts'
export type DeliveryAnalysisData = {
trendList: Array
@@ -11,122 +11,16 @@ export type DeliveryAnalysisData = {
export async function fetchDeliveryAnalysis(period: string): Promise {
const { startIso, endIso } = computeDateRange(period)
- await ensureSupabaseReady()
-
- // 优先走 RPC(需要在 Supabase 执行 DELIVERY_ANALYSIS_RPCS.sql 创建函数)
- let trendList: Array = []
- let topList: Array = []
-
- const trendRes: any = await supa.rpc('rpc_delivery_efficiency_daily', {
+ const trendList = await rpcOrEmptyArray('rpc_delivery_efficiency_daily', {
p_start: startIso,
p_end: endIso
- } as UTSJSONObject)
+ } as any)
- if (trendRes.status === 404) {
- // RPC 不存在:降级到直查表聚合(测试阶段兜底)
- const taskRes: any = await supa
- .from('ml_delivery_tasks')
- .select('id,driver_id,assigned_at,delivered_at,delivery_fee', {})
- .eq('status', 5)
- .gte('assigned_at', startIso)
- .order('assigned_at', { ascending: true } as any)
- .execute()
-
- if (taskRes?.error != null) throw taskRes.error
-
- const rowsAny = (taskRes.data != null ? taskRes.data : []) as any
- const tasks = Array.isArray(rowsAny) ? (rowsAny as Array) : []
-
- const dayAgg = new Map()
- const driverAgg = new Map()
- const driverFeeAgg = new Map()
- const driverTimeAgg = new Map()
-
- for (let i = 0; i < tasks.length; i++) {
- const t = tasks[i]
- const assignedAt = t.getString('assigned_at') ?? ''
- const deliveredAt = t.getString('delivered_at') ?? ''
- const driverId = t.getString('driver_id') ?? ''
- if (assignedAt.trim() === '' || deliveredAt.trim() === '') continue
-
- const day = assignedAt.length >= 10 ? assignedAt.substring(0, 10) : assignedAt
- const a = new Date(assignedAt)
- const d = new Date(deliveredAt)
- const diffMin = Math.max(0, (d.getTime() - a.getTime()) / 60000)
- const fee = t.getNumber('delivery_fee') ?? 0
-
- const old = dayAgg.get(day)
- if (old == null) {
- const obj = new UTSJSONObject()
- obj.set('day', day)
- obj.set('completed_orders', 1)
- obj.set('sum_minutes', diffMin)
- obj.set('total_fee', fee)
- dayAgg.set(day, obj)
- } else {
- old.set('completed_orders', (old.getNumber('completed_orders') ?? 0) + 1)
- old.set('sum_minutes', (old.getNumber('sum_minutes') ?? 0) + diffMin)
- old.set('total_fee', (old.getNumber('total_fee') ?? 0) + fee)
- }
-
- if (driverId.trim() !== '') {
- driverAgg.set(driverId, (driverAgg.get(driverId) ?? 0) + 1)
- driverFeeAgg.set(driverId, (driverFeeAgg.get(driverId) ?? 0) + fee)
- driverTimeAgg.set(driverId, (driverTimeAgg.get(driverId) ?? 0) + diffMin)
- }
- }
-
- // dayAgg -> trendList
- const days = Array.from(dayAgg.keys()).sort()
- for (let i = 0; i < days.length; i++) {
- const day = days[i]
- const obj = dayAgg.get(day)
- if (obj != null) {
- const completed = obj.getNumber('completed_orders') ?? 0
- const sumMin = obj.getNumber('sum_minutes') ?? 0
- const totalFee = obj.getNumber('total_fee') ?? 0
- const out = new UTSJSONObject()
- out.set('day', day)
- out.set('avg_delivery_time', completed > 0 ? sumMin / completed : 0)
- out.set('total_fee', totalFee)
- out.set('completed_orders', completed)
- trendList.push(out)
- }
- }
-
- // driverAgg -> topList (Top10)
- const drivers = Array.from(driverAgg.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10)
- for (let i = 0; i < drivers.length; i++) {
- const [driverId, orders] = drivers[i]
- const out = new UTSJSONObject()
- out.set('driver_id', driverId)
- out.set('orders', orders)
- out.set('total_fee', driverFeeAgg.get(driverId) ?? 0)
- out.set('total_minutes', driverTimeAgg.get(driverId) ?? 0)
- topList.push(out)
- }
- } else if (trendRes.error != null) {
- throw trendRes.error
- } else {
- const anyData = trendRes.data as any
- trendList = Array.isArray(anyData) ? (anyData as Array) : []
-
- // Top drivers
- const topRes = await supa.rpc('rpc_delivery_efficiency_top_drivers', {
- p_start: startIso,
- p_end: endIso,
- p_limit: 10
- })
-
- if (topRes.status === 404) {
- console.warn('rpc_delivery_efficiency_top_drivers not found, top drivers will be empty')
- } else if (topRes.error != null) {
- throw topRes.error
- } else {
- const topAny = topRes.data as any
- topList = Array.isArray(topAny) ? (topAny as Array) : []
- }
- }
+ const topList = await rpcOrEmptyArray('rpc_delivery_efficiency_top_drivers', {
+ p_start: startIso,
+ p_end: endIso,
+ p_limit: 10
+ } as any)
return { trendList, topList, startIso, endIso }
}
diff --git a/services/analytics/productInsightsService.uts b/services/analytics/productInsightsService.uts
index 29bdbeaf..34c13b19 100644
--- a/services/analytics/productInsightsService.uts
+++ b/services/analytics/productInsightsService.uts
@@ -23,8 +23,8 @@ function safeNumber(v: any): number {
export async function fetchProductOverview(period: string): Promise {
const { startIso, endIso } = computeDateRange(period)
const row = await rpcOrNull('rpc_product_insights_overview', {
- p_start: startIso,
- p_end: endIso
+ p_start: toDateOnly(startIso),
+ p_end: toDateOnly(endIso)
} as any)
const obj: any = row != null ? row : ({} as any)
@@ -44,8 +44,7 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
- p_limit: limit,
- p_merchant_id: null
+ p_limit: limit
} as any)
const list: Array = []
@@ -55,8 +54,8 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom
id: `${r.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${r.getAny?.('name') ?? '未知商品'}`,
- sales: safeNumber(r.getAny?.('sales') ?? r.getAny?.('total_amount') ?? 0),
- growth: safeNumber(r.getAny?.('growth') ?? r.getAny?.('growth_rate') ?? 0)
+ sales: safeNumber(r.getAny?.('sales') ?? 0),
+ growth: safeNumber(r.getAny?.('growth') ?? 0)
})
}
return list
@@ -73,12 +72,12 @@ export async function fetchProductTrend(period: string, productId: string): Prom
const out: Array = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
- const date = `${r.getAny?.('date') ?? r.getAny?.('day') ?? r.getAny?.('date_key') ?? ''}`
+ const date = `${r.getAny?.('date') ?? ''}`
out.push({
date,
- gmv: safeNumber(r.getAny?.('gmv') ?? r.getAny?.('total_amount') ?? 0),
- qty: safeNumber(r.getAny?.('qty') ?? r.getAny?.('sales_qty') ?? 0),
- orders: safeNumber(r.getAny?.('orders') ?? r.getAny?.('order_count') ?? 0)
+ gmv: safeNumber(r.getAny?.('gmv') ?? 0),
+ qty: safeNumber(r.getAny?.('qty') ?? 0),
+ orders: safeNumber(r.getAny?.('orders') ?? 0)
})
}
return out
@@ -93,11 +92,7 @@ export async function fetchCategorySales(period: string): Promise> {
- const { startIso, endIso } = computeDateRange(period)
- return await rpcOrEmptyArray('rpc_product_insights_stock', {
- p_start: startIso,
- p_end: endIso
- } as any)
+ return await rpcOrEmptyArray('rpc_product_insights_stock', {} as any)
}
export async function fetchPriceTrend(period: string): Promise> {
diff --git a/services/analytics/salesReportService.uts b/services/analytics/salesReportService.uts
index c14f3a21..b8749f1b 100644
--- a/services/analytics/salesReportService.uts
+++ b/services/analytics/salesReportService.uts
@@ -24,22 +24,9 @@ function safeNumber(v: any): number {
export async function fetchSalesKpis(period: string): Promise {
const { startIso, endIso } = computeDateRange(period)
- const days = period === '7d' ? 7 : period === '30d' ? 30 : period === '90d' ? 90 : 365
-
- const startDateObj = new Date(startIso)
- const endDateObj = new Date(endIso)
-
- const periodStart = new Date(startDateObj.getFullYear(), startDateObj.getMonth(), startDateObj.getDate())
- const periodEnd = new Date(endDateObj.getFullYear(), endDateObj.getMonth(), endDateObj.getDate() + 1)
- const prevStart = new Date(periodStart.getTime() - days * 24 * 60 * 60 * 1000)
- const prevEnd = new Date(periodStart.getTime())
-
- const row = await rpcOrNull('rpc_analytics_realtime_kpis', {
- p_start: periodStart.toISOString(),
- p_end: periodEnd.toISOString(),
- p_compare_start: prevStart.toISOString(),
- p_compare_end: prevEnd.toISOString(),
- p_merchant_id: null
+ const row = await rpcOrNull('rpc_analytics_sales_kpis', {
+ p_start_date: toDateOnly(startIso),
+ p_end_date: toDateOnly(endIso)
} as any)
const obj: any = row != null ? row : ({} as any)
@@ -61,10 +48,9 @@ export async function fetchSalesKpis(period: string): Promise {
export async function fetchSalesTrend(period: string): Promise {
const { startIso, endIso } = computeDateRange(period)
- const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', {
+ const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', {
p_start_date: toDateOnly(startIso),
- p_end_date: toDateOnly(endIso),
- p_merchant_id: null
+ p_end_date: toDateOnly(endIso)
} as any)
const x: Array = []
@@ -87,8 +73,7 @@ export async function fetchSalesTopProducts(period: string, limit: number = 50):
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
- p_limit: limit,
- p_merchant_id: null
+ p_limit: limit
} as any)
const list: Array = []
diff --git a/types/analytics.uts b/types/analytics.uts
new file mode 100644
index 00000000..b3891595
--- /dev/null
+++ b/types/analytics.uts
@@ -0,0 +1,3 @@
+// types/analytics.uts
+
+export type AnalyticsTypesMigrated = true
diff --git a/types/analytics/common.uts b/types/analytics/common.uts
new file mode 100644
index 00000000..d6e186f6
--- /dev/null
+++ b/types/analytics/common.uts
@@ -0,0 +1,5 @@
+// types/analytics/common.uts
+
+export type TimePeriod = { value: string; label: string }
+export type ChartType = { value: string; label: string }
+export type Metric = { key: string; label: string }
diff --git a/types/analytics/coupon.uts b/types/analytics/coupon.uts
new file mode 100644
index 00000000..985cab5d
--- /dev/null
+++ b/types/analytics/coupon.uts
@@ -0,0 +1,11 @@
+// types/analytics/coupon.uts
+
+export type CouponData = {
+ total_issued: number
+ issued_growth: number
+ total_used: number
+ usage_rate: number
+ gmv_increase: number
+ gmv_growth: number
+ roi: number
+}
diff --git a/types/analytics/custom-report.uts b/types/analytics/custom-report.uts
new file mode 100644
index 00000000..c52f4298
--- /dev/null
+++ b/types/analytics/custom-report.uts
@@ -0,0 +1,29 @@
+// types/analytics/custom-report.uts
+
+import type { Metric, TimePeriod, ChartType } from './common.uts'
+
+export type CustomReport = {
+ id: string
+ name: string
+ description: string
+ metrics: Array
+ charts: Array
+ updated_at: string
+ period?: string
+}
+
+export type ReportForm = {
+ name: string
+ description: string
+ metrics: Array
+ period: string
+ chartType: string
+}
+
+export type ReportFormErrors = {
+ name: string
+ description: string
+ metrics: string
+ period: string
+ chartType: string
+}
diff --git a/types/analytics/dashboard.uts b/types/analytics/dashboard.uts
new file mode 100644
index 00000000..f73c7c96
--- /dev/null
+++ b/types/analytics/dashboard.uts
@@ -0,0 +1,7 @@
+// types/analytics/dashboard.uts
+
+export type TrendData = { x: Array; gmv: Array; orders: Array }
+export type SegmentItem = { name: string; value: number }
+export type TrafficItem = { name: string; value: number }
+export type TopProductItem = { id: string; rank: number; name: string; sales: number }
+export type TopMerchantItem = { id: string; rank: number; name: string; sales: number; growth: number }
diff --git a/types/analytics/data-detail.uts b/types/analytics/data-detail.uts
new file mode 100644
index 00000000..76d09564
--- /dev/null
+++ b/types/analytics/data-detail.uts
@@ -0,0 +1,4 @@
+// types/analytics/data-detail.uts
+
+export type TableColumn = { key: string; label: string; type: string; sortable: boolean }
+export type DrillDownItem = { id: string; label: string; value: string; type: string }
diff --git a/types/analytics/delivery.uts b/types/analytics/delivery.uts
new file mode 100644
index 00000000..203451dd
--- /dev/null
+++ b/types/analytics/delivery.uts
@@ -0,0 +1,25 @@
+// types/analytics/delivery.uts
+
+/**
+ * Key Performance Indicators for the Delivery Analysis page.
+ */
+export type DeliveryData = {
+ avg_delivery_time: number;
+ time_growth: number;
+ total_fee: number;
+ avg_fee: number;
+ avg_orders_per_driver: number;
+ satisfaction_rate: number;
+ satisfaction_growth: number;
+};
+
+/**
+ * Represents a driver's ranking based on performance.
+ */
+export type DriverRank = {
+ id: string;
+ rank: number;
+ name: string;
+ orders: number;
+ rating: number
+};
diff --git a/types/analytics/insight.uts b/types/analytics/insight.uts
new file mode 100644
index 00000000..f86175a3
--- /dev/null
+++ b/types/analytics/insight.uts
@@ -0,0 +1,19 @@
+// types/analytics/insight.uts
+
+export type InsightDetail = {
+ id: string
+ report_id: string
+ type: string
+ impact: string
+ title: string
+ content: string
+ created_at: string
+}
+
+export type RelatedReport = {
+ id: string
+ title: string
+ type: string
+ period: string
+ generated_at: string
+}
diff --git a/types/analytics/market.uts b/types/analytics/market.uts
new file mode 100644
index 00000000..8ea60a5c
--- /dev/null
+++ b/types/analytics/market.uts
@@ -0,0 +1,9 @@
+// types/analytics/market.uts
+
+export type MarketTrendsResponse = {
+ trendRows: any
+ categoryRows: any
+ seasonalRows: any
+ priceRows: any
+ competitionRows: any
+}
diff --git a/types/analytics/product.uts b/types/analytics/product.uts
new file mode 100644
index 00000000..46286048
--- /dev/null
+++ b/types/analytics/product.uts
@@ -0,0 +1,14 @@
+// types/analytics/product.uts
+
+export type ProductData = {
+ total_products: number
+ product_growth: number
+ hot_products: number
+ turnover_rate: number
+ turnover_growth: number
+ avg_stock: number
+ stock_growth: number
+}
+
+export type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number }
+export type ProductTrendRow = { date: string; gmv: number; qty: number; orders: number }
diff --git a/types/analytics/profile.uts b/types/analytics/profile.uts
new file mode 100644
index 00000000..610d0221
--- /dev/null
+++ b/types/analytics/profile.uts
@@ -0,0 +1,38 @@
+// types/analytics/profile.uts
+
+export type ReportStatus = 'pending' | 'ready' | 'failed' | 'scheduled' | 'shared' | string
+
+export type RecentReport = {
+ id: string
+ title: string
+ description: string
+ status: ReportStatus
+ created_at: string
+}
+
+export type OverviewData = {
+ totalSales: string
+ salesGrowth: number
+ totalUsers: string
+ userGrowth: number
+ totalOrders: string
+ orderGrowth: number
+ conversionRate: number
+ conversionGrowth: number
+}
+
+export type ReportCounts = {
+ total: number
+ pending: number
+ scheduled: number
+ shared: number
+}
+
+export type TodayInsights = {
+ hotProduct: string
+ peakTraffic: string
+ conversionAnomaly: string
+ mobileRatio: number
+}
+
+export type TrendDatum = { label: string; sales: number; orders: number }
diff --git a/types/analytics/report-detail.uts b/types/analytics/report-detail.uts
new file mode 100644
index 00000000..3cdc1786
--- /dev/null
+++ b/types/analytics/report-detail.uts
@@ -0,0 +1,46 @@
+// types/analytics/report-detail.uts
+
+export type ReportType = {
+ id: string;
+ title: string;
+ type: string;
+ period: string;
+ generated_at: string;
+ description: string;
+};
+
+export type MetricType = {
+ key: string;
+ label: string;
+ value: number;
+ format: string;
+ icon: string;
+ color: string;
+ change: number;
+};
+
+export type ChartTabType = {
+ key: string;
+ label: string;
+};
+
+export type ChartLegendType = {
+ key: string;
+ label: string;
+ color: string;
+};
+
+export type TableColumnType = {
+ key: string;
+ title: string;
+ width: string;
+ type: string;
+};
+
+export type InsightType = {
+ id: string;
+ type: string;
+ title: string;
+ content: string;
+ impact: string;
+};
diff --git a/types/analytics/sales.uts b/types/analytics/sales.uts
new file mode 100644
index 00000000..55e4757b
--- /dev/null
+++ b/types/analytics/sales.uts
@@ -0,0 +1,22 @@
+// types/analytics/sales.uts
+
+// Re-exporting shared types from dashboard for semantic clarity in the sales context.
+import type { TrendData, TopProductItem, TopMerchantItem } from './dashboard.uts'
+
+export type SalesTrendData = TrendData
+export type ProductRank = TopProductItem
+export type MerchantRank = TopMerchantItem
+
+/**
+ * Key Performance Indicators for the Sales Report page.
+ */
+export type SalesData = {
+ gmv: number
+ gmv_growth: number
+ orders: number
+ order_growth: number
+ conversion_rate: number
+ conversion_growth: number
+ avg_order_amount: number
+ avg_order_growth: number
+}
diff --git a/types/analytics/user.uts b/types/analytics/user.uts
new file mode 100644
index 00000000..216404cb
--- /dev/null
+++ b/types/analytics/user.uts
@@ -0,0 +1,25 @@
+// types/analytics/user.uts
+
+/**
+ * Key Performance Indicators for the User Analysis page.
+ */
+export type UserData = {
+ total_users: number;
+ user_growth: number;
+ new_users: number;
+ new_user_growth: number;
+ active_users: number;
+ active_growth: number;
+ ordering_users: number;
+ ordering_growth: number;
+ paid_users: number;
+ paid_growth: number;
+ new_user_conversion_rate: number;
+ repurchase_rate: number;
+ repurchase_growth: number;
+}
+
+/**
+ * Represents a single step in a conversion funnel.
+ */
+export type FunnelStep = { step: string; value: number };
From 19970db2889644bce3834839d06d039e8905a4a1 Mon Sep 17 00:00:00 2001
From: comlibmb <1844410276@qq.com>
Date: Sun, 1 Feb 2026 20:17:37 +0800
Subject: [PATCH 13/18] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90ui?=
=?UTF-8?q?=E8=A1=A5=E5=85=85=E5=AE=8C=E5=96=84=EF=BC=8C=E6=8E=A5=E5=85=A5?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ak/config.uts | 6 +-
.../analytics/AnalyticsDateRangePicker.uvue | 129 +++++++
pages.json | 6 -
pages/mall/analytics/coupon-analysis.uvue | 3 +-
pages/mall/analytics/custom-report.uvue | 359 +-----------------
pages/mall/analytics/data-detail.uvue | 3 +-
pages/mall/analytics/delivery-analysis.uvue | 3 +-
pages/mall/analytics/index.uvue | 76 +++-
pages/mall/analytics/insight-detail.uvue | 3 +-
pages/mall/analytics/market-trends.uvue | 3 +-
pages/mall/analytics/product-insights.uvue | 3 +-
pages/mall/analytics/profile.uvue | 16 +-
pages/mall/analytics/report-detail.uvue | 25 +-
pages/mall/analytics/sales-report.uvue | 57 ++-
pages/mall/analytics/user-analysis.uvue | 11 +-
services/analytics/dashboardService.uts | 15 +-
services/analytics/salesReportService.uts | 56 ++-
utils/sapi.uts | 28 +-
utils/utils.uts | 26 ++
19 files changed, 393 insertions(+), 435 deletions(-)
create mode 100644 components/analytics/AnalyticsDateRangePicker.uvue
diff --git a/ak/config.uts b/ak/config.uts
index e235d9a1..b9ac3206 100644
--- a/ak/config.uts
+++ b/ak/config.uts
@@ -2,11 +2,11 @@
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.63
// Kong HTTP Port: 8000
-export const SUPA_URL: string = 'http://192.168.1.63:8000'
-export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
+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.63:8000/realtime/v1/websocket'
+export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket'
// 备用配置(已注释,如需切换可取消注释)
// 开发环境 - 其他内网地址
diff --git a/components/analytics/AnalyticsDateRangePicker.uvue b/components/analytics/AnalyticsDateRangePicker.uvue
new file mode 100644
index 00000000..57e5e91c
--- /dev/null
+++ b/components/analytics/AnalyticsDateRangePicker.uvue
@@ -0,0 +1,129 @@
+
+
+
+ 开始日期
+
+ {{ startDate || '请选择' }}
+
+
+
+ 结束日期
+
+ {{ endDate || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages.json b/pages.json
index 314253fb..4a62865b 100644
--- a/pages.json
+++ b/pages.json
@@ -411,12 +411,6 @@
"navigationBarTitleText": "数据洞察详情",
"enablePullDownRefresh": false
}
- },
- {
- "path": "test/test-connection",
- "style": {
- "navigationBarTitleText": "Supabase 连接测试"
- }
}
]
},
diff --git a/pages/mall/analytics/coupon-analysis.uvue b/pages/mall/analytics/coupon-analysis.uvue
index 8eddf244..11b03e5e 100644
--- a/pages/mall/analytics/coupon-analysis.uvue
+++ b/pages/mall/analytics/coupon-analysis.uvue
@@ -109,7 +109,8 @@
diff --git a/pages/mall/consumer/category.uvue b/pages/mall/consumer/category.uvue
index ec734cdf..a26780fb 100644
--- a/pages/mall/consumer/category.uvue
+++ b/pages/mall/consumer/category.uvue
@@ -122,6 +122,7 @@ const productList = ref([])
const activePrimary = ref('')
const cartCount = ref(3)
const hasMore = ref(true)
+const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
// 获取当前分类信息
const currentCategoryName = ref('')
@@ -134,34 +135,67 @@ const pageParams = ref({})
// 生命周期
onMounted(async() => {
await loadCategories()
- await loadProducts()
+ // 等待分类加载完成后,再检查是否需要加载默认分类的商品
+ // 延迟一点时间,确保页面参数处理完成
+ setTimeout(async () => {
+ if (!hasLoadedFromParams.value && activePrimary.value) {
+ await loadProducts()
+ }
+ }, 300)
})
// 添加加载分类的方法
const loadCategories = async () => {
- const categories = await supabaseService.getCategories()
- if (categories.length > 0) {
- primaryCategories.value = categories
- // 设置默认选中第一个分类
- if (!activePrimary.value && categories[0]) {
- activePrimary.value = categories[0].id
+ try {
+ const categories = await supabaseService.getCategories()
+ console.log('加载分类数据成功,数量:', categories.length)
+ 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)
+ }
+ } else {
+ console.warn('从Supabase获取的分类数据为空')
}
+ } catch (error) {
+ console.error('加载分类数据失败:', error)
}
}
// 加载商品数据
const loadProducts = async () => {
- if (activePrimary.value) {
- const response = await supabaseService.getProductsByCategory(activePrimary.value)
- productList.value = response.data
- hasMore.value = response.hasmore
-
- // 更新当前分类信息
- const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
- if (category) {
- currentCategoryName.value = category.name
- currentCategoryDesc.value = category.description
+ try {
+ if (activePrimary.value) {
+ console.log('开始加载商品,分类ID:', activePrimary.value)
+ const response = await supabaseService.getProductsByCategory(activePrimary.value)
+ console.log('商品加载结果:', {
+ dataCount: response.data.length,
+ total: response.total,
+ hasmore: response.hasmore
+ })
+
+ productList.value = response.data
+ hasMore.value = response.hasmore
+
+ // 更新当前分类信息
+ const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
+ if (category) {
+ currentCategoryName.value = category.name
+ currentCategoryDesc.value = category.description || ''
+ console.log('当前分类信息:', category.name, '描述:', category.description)
+ } else {
+ console.warn('未找到对应的分类信息,分类ID:', activePrimary.value)
+ }
+
+ console.log('商品列表加载完成,数量:', productList.value.length)
+ } else {
+ console.warn('activePrimary为空,无法加载商品')
}
+ } catch (error) {
+ console.error('加载商品数据失败:', error)
+ productList.value = []
}
}
@@ -200,6 +234,7 @@ onLoad((options: any) => {
// 如果有找到分类ID,则选中对应的分类
if (categoryId) {
+ hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName || '未指定')
@@ -244,6 +279,7 @@ onShow(() => {
// 检查是否有分类参数
if (pageOptions.categoryId) {
+ hasLoadedFromParams.value = true
const categoryId = pageOptions.categoryId
const categoryName = pageOptions.name || ''
@@ -288,6 +324,7 @@ onShow(() => {
const params = new URLSearchParams(queryString)
const urlCategoryId = params.get('categoryId')
if (urlCategoryId) {
+ hasLoadedFromParams.value = true
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
selectPrimaryCategory(urlCategoryId)
}
diff --git a/pages/mall/consumer/category药品.uvue b/pages/mall/consumer/category药品.uvue
new file mode 100644
index 00000000..ec734cdf
--- /dev/null
+++ b/pages/mall/consumer/category药品.uvue
@@ -0,0 +1,1131 @@
+
+
+
+
+
+
+
+ 请输入药品名称、症状或品牌
+
+
+
+ 🔳
+
+
+
+
+ 📷
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+ {{ item.icon }}
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ product.badge }}
+
+
+ {{ product.name }}
+ {{ product.specification }}
+
+
+
+ ¥
+ {{ product.price }}
+
+
+ ¥{{ product.originalPrice }}
+
+
+
+
+ {{ product.manufacturer }}
+
+ 已售{{ product.sales }}
+
+
+
+
+
+
+
+
+ 💊
+ 暂无相关药品
+ 该分类下暂无商品,敬请期待
+
+
+
+
+ 上拉加载更多
+
+
+
+
+
+
+
+
+
diff --git a/pages/mall/consumer/checkout.uvue b/pages/mall/consumer/checkout.uvue
index d96e6e2a..f68dfd6f 100644
--- a/pages/mall/consumer/checkout.uvue
+++ b/pages/mall/consumer/checkout.uvue
@@ -772,64 +772,10 @@ const loadDefaultAddress = async () => {
// 获取当前用户ID
const getCurrentUserId = (): string => {
- // 尝试从多个可能的键名获取用户ID
- const possibleKeys = ['user_id', 'userId', 'uid', 'user_uuid', 'userID', 'user.id']
-
- for (const key of possibleKeys) {
- const value = uni.getStorageSync(key)
- console.log(`getCurrentUserId: 尝试键名 ${key}:`, value)
- if (value) {
- console.log(`getCurrentUserId: 从 ${key} 获取到用户ID:`, value)
- return value as string
- }
- }
-
- // 尝试从userInfo对象获取
- const userInfo = uni.getStorageSync('userInfo')
- console.log('getCurrentUserId: 从userInfo获取:', userInfo)
- if (userInfo) {
- // userInfo可能是字符串(需要解析)或对象
- let userInfoObj: any = userInfo
- if (typeof userInfo === 'string') {
- try {
- userInfoObj = JSON.parse(userInfo)
- } catch (e) {
- console.error('解析userInfo失败:', e)
- }
- }
-
- // 尝试多个可能的属性名
- const possibleProps = ['id', 'userId', 'uid', 'user_id', 'uuid', 'user_uuid']
- for (const prop of possibleProps) {
- if (userInfoObj && userInfoObj[prop]) {
- console.log(`getCurrentUserId: 从userInfo.${prop} 获取到用户ID:`, userInfoObj[prop])
- return userInfoObj[prop] as string
- }
- }
- }
-
- // 尝试从auth获取(如果使用Supabase Auth)
- const authData = uni.getStorageSync('supabase.auth.token')
- if (authData) {
- console.log('getCurrentUserId: 从supabase.auth.token获取:', authData)
- try {
- const authObj = typeof authData === 'string' ? JSON.parse(authData) : authData
- if (authObj.currentSession && authObj.currentSession.user && authObj.currentSession.user.id) {
- console.log('getCurrentUserId: 从auth session获取用户ID:', authObj.currentSession.user.id)
- return authObj.currentSession.user.id as string
- }
- } catch (e) {
- console.error('解析auth数据失败:', e)
- }
- }
-
- // 打印所有存储键,用于调试
- console.log('getCurrentUserId: 所有Storage键:')
- const allKeys = uni.getStorageInfoSync().keys
- console.log('Storage keys:', allKeys)
-
- console.log('getCurrentUserId: 未找到用户ID')
- return ''
+ // 使用 SupabaseService 获取当前用户ID
+ const userId = supabaseService.getCurrentUserId()
+ console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId)
+ return userId ?? ''
}
// 用户登录状态
@@ -1280,65 +1226,89 @@ const selectCoupon = () => {
// 提交订单
const submitOrder = async () => {
- if (!selectedAddress.value) {
- uni.showToast({
- title: '请选择收货地址',
- icon: 'none'
- })
- return
- }
+ // 校验地址
+ if (!selectedAddress.value) {
+ uni.showToast({
+ title: '请选择收货地址',
+ icon: 'none'
+ })
+ return
+ }
+
+ // 校验商品
+ if (checkoutItems.value.length === 0) {
+ uni.showToast({
+ title: '订单中没有商品',
+ icon: 'none'
+ })
+ return
+ }
+
+ uni.showLoading({ title: '提交中...' })
- // MOCK ORDER SUBMISSION
- // 模拟创建成功
try {
- const mockOrderId = `order_${Date.now()}`
+ const userId = getCurrentUserId()
+ // 确保使用当前登录用户ID (如果本地存储为空,可能需要处理)
+ if (!userId) {
+ uni.hideLoading()
+ uni.showToast({
+ title: '请先登录',
+ icon: 'none'
+ })
+ return
+ }
- // 创建订单对象
- const newOrder = {
- id: mockOrderId,
- order_no: generateOrderNo(),
- user_id: getCurrentUserId() || 'user_001',
- merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理,取第一个商品的merchant
- status: 1, // 待支付
- total_amount: totalAmount.value,
- discount_amount: discountAmount.value,
- delivery_fee: deliveryFee.value,
- actual_amount: actualAmount.value,
- payment_method: 0,
- payment_status: 0,
- delivery_address: selectedAddress.value,
- items: checkoutItems.value,
- created_at: new Date().toISOString()
- }
+ // 准备订单项数据
+ // 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构
+ // 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段
+ const orderItems = checkoutItems.value.map((item: any): any => ({
+ id: item.id || '', // 这是一个临时ID或者购物车ID,createOrder 中会使用 product_id
+ product_id: item.product_id || item.id, // 确保有 product_id
+ quantity: item.quantity,
+ price: item.price,
+ product_name: item.name,
+ product_image: item.image,
+ spec: item.spec,
+ checked: true
+ }))
- // 保存到本地存储
- const storedOrders = uni.getStorageSync('orders')
- let orders: any[] = []
- if (storedOrders) {
- try {
- orders = JSON.parse(storedOrders as string) as any[]
- } catch (e) {
- console.error('解析订单数据失败', e)
- }
- }
- orders.unshift(newOrder)
- uni.setStorageSync('orders', JSON.stringify(orders))
-
- uni.showLoading({ title: '提交中...' })
- await new Promise(resolve => setTimeout(resolve, 500))
+ // 调用 Supabase 服务创建订单
+ const result = await supabaseService.createOrder(
+ userId,
+ selectedAddress.value!.id, // 地址ID
+ actualAmount.value, // 实付金额
+ orderItems
+ )
+
uni.hideLoading()
-
- // 携带价格详情跳转
- uni.navigateTo({
- url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
- })
- } catch (err) {
- console.error('创建订单失败:', err)
- uni.showToast({
- title: '订单创建失败',
- icon: 'none'
- })
- }
+
+ if (result.success) {
+ // 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储)
+ // 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地
+ try {
+ uni.removeStorageSync('checkout_items')
+ } catch(e) {
+ console.error('清除结算商品失败', e)
+ }
+
+ const activeOrderId = result.data as string
+
+ // 跳转支付页面
+ uni.navigateTo({
+ url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
+ })
+ } else {
+ throw new Error(result.error)
+ }
+
+ } catch (err: any) {
+ uni.hideLoading()
+ console.error('创建订单失败:', err)
+ uni.showToast({
+ title: err.message || '订单创建失败',
+ icon: 'none'
+ })
+ }
}
// 生成订单号
diff --git a/pages/mall/consumer/favorites.uvue b/pages/mall/consumer/favorites.uvue
index 693a201a..ad1c8d8a 100644
--- a/pages/mall/consumer/favorites.uvue
+++ b/pages/mall/consumer/favorites.uvue
@@ -31,6 +31,7 @@
+
+
diff --git a/pages/mall/consumer/orders.uvue b/pages/mall/consumer/orders.uvue
index f33fd102..19de501a 100644
--- a/pages/mall/consumer/orders.uvue
+++ b/pages/mall/consumer/orders.uvue
@@ -156,9 +156,11 @@
-
-
diff --git a/pages/mall/consumer/product-detail.uvue b/pages/mall/consumer/product-detail.uvue
index a1da08fa..9e240061 100644
--- a/pages/mall/consumer/product-detail.uvue
+++ b/pages/mall/consumer/product-detail.uvue
@@ -373,7 +373,18 @@ export default {
// 尝试多种方式访问属性
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
- const priceValue = dbProduct.price !== undefined ? dbProduct.price : (dbProduct['price'] !== undefined ? dbProduct['price'] : undefined)
+
+ // 价格字段兼容性处理:优先查找 price,其次查找 base_price
+ let priceValue = dbProduct.price
+ if (priceValue === undefined || priceValue === null) {
+ priceValue = dbProduct.base_price
+ }
+ if (priceValue === undefined || priceValue === null) {
+ priceValue = dbProduct['price']
+ }
+ if (priceValue === undefined || priceValue === null) {
+ priceValue = dbProduct['base_price']
+ }
const hasId = idValue !== undefined && idValue !== null
const hasName = nameValue !== undefined && nameValue !== null
@@ -396,33 +407,27 @@ export default {
// 数据库Product接口和本地ProductType接口字段可能不同
const images = [] as Array
- // 处理图片字段:优先使用images字段,其次使用image字段
- console.log('处理数据库图片字段:')
- console.log('dbProduct.images:', dbProduct.images, '类型:', typeof dbProduct.images)
- console.log('dbProduct.image:', dbProduct.image, '类型:', typeof dbProduct.image)
+ // 处理图片字段:优先使用image_urls字段,其次使用main_image_url
+ console.log('处理数据库图片字段')
- // 尝试从数据库的images字段获取图片(可能是字符串或数组)
- if (dbProduct.images) {
+ // 尝试从数据库的image_urls字段获取图片(JSON字符串或对象)
+ if (dbProduct.image_urls) {
let imagesArray: any[] = []
- if (typeof dbProduct.images === 'string') {
+ if (typeof dbProduct.image_urls === 'string') {
try {
- imagesArray = JSON.parse(dbProduct.images)
- console.log('解析images字符串成功:', imagesArray)
+ imagesArray = JSON.parse(dbProduct.image_urls)
} catch (e) {
- console.error('解析images字段失败:', e, dbProduct.images)
- // 如果不是JSON,尝试按逗号分割
- if (dbProduct.images.includes(',')) {
- imagesArray = dbProduct.images.split(',').map((img: string) => img.trim())
- } else if (dbProduct.images) {
- imagesArray = [dbProduct.images]
+ console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
+ // 尝试逗号分割
+ if (dbProduct.image_urls.includes(',')) {
+ imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
}
}
- } else if (Array.isArray(dbProduct.images)) {
- imagesArray = dbProduct.images
+ } else if (Array.isArray(dbProduct.image_urls)) {
+ imagesArray = dbProduct.image_urls
}
if (imagesArray.length > 0) {
- console.log('从数据库images字段获取图片数组:', imagesArray)
for (const img of imagesArray) {
if (typeof img === 'string' && img) {
images.push(img)
@@ -430,11 +435,18 @@ export default {
}
}
}
-
- // 如果没有从images字段获取到图片,尝试使用image字段
+
+ // 如果没有获取到相册图,但有主图,放入相册
+ if (dbProduct.main_image_url) {
+ // 如果相册里没有这张图,把它加到第一位
+ if (!images.includes(dbProduct.main_image_url)) {
+ images.unshift(dbProduct.main_image_url)
+ }
+ }
+
+ // 兼容旧字段 image
if (images.length === 0 && dbProduct.image) {
- console.log('使用单张图片字段:', dbProduct.image)
- images.push(dbProduct.image)
+ images.push(dbProduct.image)
}
// 如果仍然没有图片,使用传入的图片或默认图片
@@ -461,9 +473,33 @@ export default {
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
// 确保数值字段有效
- const price = typeof dbProduct.price === 'number' ? dbProduct.price : 0
- const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : 100
- const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : 50
+ // 优先使用 price,不存在则使用 base_price
+ let productPrice = 0
+ if (typeof dbProduct.price === 'number') {
+ productPrice = dbProduct.price
+ } else if (typeof dbProduct.base_price === 'number') {
+ productPrice = dbProduct.base_price
+ } else if (priceValue !== undefined) {
+ // 使用上面校验时获取到的 priceValue
+ productPrice = Number(priceValue)
+ }
+
+ const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
+ const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
+
+ // 解析 attributes
+ let attributes: any = {}
+ if (dbProduct.attributes) {
+ try {
+ if (typeof dbProduct.attributes === 'string') {
+ attributes = JSON.parse(dbProduct.attributes)
+ } else {
+ attributes = dbProduct.attributes
+ }
+ } catch (e) {
+ console.error('解析 attributes 失败', e)
+ }
+ }
this.product = {
id: dbProduct.id || productId,
@@ -472,20 +508,20 @@ export default {
name: dbProduct.name || '商品名称',
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
- price: price,
- original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : null,
+ price: productPrice,
+ original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
stock: stock,
sales: sales,
status: 1,
created_at: dbProduct.created_at || '2024-01-01',
// 药品相关字段
- specification: dbProduct.specification || null,
- usage: dbProduct.usage || null,
- side_effects: dbProduct.side_effects || null,
- precautions: dbProduct.precautions || null,
- expiry_date: dbProduct.expiry_date || null,
- storage_conditions: dbProduct.storage_conditions || null,
- approval_number: dbProduct.approval_number || null,
+ specification: attributes.specification || dbProduct.specification || null,
+ usage: attributes.usage || dbProduct.usage || null,
+ side_effects: attributes.side_effects || dbProduct.side_effects || null,
+ precautions: attributes.precautions || dbProduct.precautions || null,
+ expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
+ storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
+ approval_number: attributes.approval_number || dbProduct.approval_number || null,
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
} as ProductType
console.log('页面 product 对象已更新:', this.product)
@@ -535,37 +571,109 @@ export default {
}
}
- // 根据商家ID生成不同的商家信息
- const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
- const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
- const shopDescriptions = [
- '专注品质生活',
- '品牌官方直营,正品保障',
- '厂家直销,价格优惠',
- '专注本领域十年老店',
- '用心服务每一位顾客'
- ]
- const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
-
- this.merchant = {
- id: this.product.merchant_id,
- user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
- shop_name: shopNames[merchantIndex],
- shop_logo: '/static/shop-logo.png',
- shop_banner: '/static/shop-banner.png',
- shop_description: shopDescriptions[merchantIndex],
- contact_name: contactNames[merchantIndex],
- contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
- shop_status: 1,
- rating: 4.5 + (merchantIndex * 0.1),
- total_sales: 10000 + merchantIndex * 5000,
- created_at: '2023-06-01'
+ // 尝试加载真实商户信息
+ let realMerchantLoaded = false
+ // 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
+ if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
+ console.log('尝试加载商户信息:', this.product.merchant_id)
+ try {
+ const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
+ if (shop) {
+ console.log('加载到商户信息:', shop.shop_name)
+
+ // 确保字段存在,避免 undefined 导致构造失败
+ this.merchant = {
+ id: shop.id || '',
+ user_id: shop.merchant_id || '',
+ shop_name: shop.shop_name || '未命名店铺',
+ shop_logo: shop.shop_logo || '/static/default-shop.png',
+ shop_banner: shop.shop_banner || '/static/default-banner.png',
+ shop_description: shop.description || '',
+ contact_name: shop.contact_name || '店主',
+ contact_phone: shop.contact_phone || '',
+ shop_status: 1,
+ // 优先使用 avg_rating,没有则使用默认值
+ rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
+ // 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
+ total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
+ created_at: shop.created_at || new Date().toISOString()
+ } as MerchantType
+ realMerchantLoaded = true
+ }
+ } catch (e) {
+ console.error('加载商户信息失败', e)
+ }
+ }
+
+ if (!realMerchantLoaded) {
+ // 根据商家ID生成不同的商家信息
+ const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
+ const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
+ const shopDescriptions = [
+ '专注品质生活',
+ '品牌官方直营,正品保障',
+ '厂家直销,价格优惠',
+ '专注本领域十年老店',
+ '用心服务每一位顾客'
+ ]
+ const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
+
+ this.merchant = {
+ id: this.product.merchant_id,
+ user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
+ shop_name: shopNames[merchantIndex],
+ shop_logo: '/static/shop-logo.png',
+ shop_banner: '/static/shop-banner.png',
+ shop_description: shopDescriptions[merchantIndex],
+ contact_name: contactNames[merchantIndex],
+ contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
+ shop_status: 1,
+ rating: 4.5 + (merchantIndex * 0.1),
+ total_sales: 10000 + merchantIndex * 5000,
+ created_at: '2023-06-01'
+ }
}
this.loadProductSkus(productId)
},
- loadProductSkus(productId: string) {
+ async loadProductSkus(productId: string) {
+ // 尝试从数据库加载SKU
+ try {
+ const skus = await supabaseService.getProductSkus(productId)
+ if (skus.length > 0) {
+ console.log('加载到商品SKU:', skus.length)
+ this.productSkus = skus.map((sku): ProductSkuType => {
+ let specs: UTSJSONObject = {}
+ if (sku.specifications) {
+ try {
+ if (typeof sku.specifications === 'string') {
+ specs = JSON.parse(sku.specifications) as UTSJSONObject
+ } else {
+ // 假设已经是对象
+ specs = sku.specifications as unknown as UTSJSONObject
+ }
+ } catch(e) {
+ console.error('解析SKU规格失败', e)
+ }
+ }
+ return {
+ id: sku.id,
+ product_id: sku.product_id,
+ sku_code: sku.sku_code,
+ specifications: specs,
+ price: sku.price,
+ stock: sku.stock !== undefined ? sku.stock : 0,
+ image_url: sku.image_url || '',
+ status: sku.status !== undefined ? sku.status : 1
+ } as ProductSkuType
+ })
+ return
+ }
+ } catch (e) {
+ console.error('Fetch SKUs error', e)
+ }
+
// 模拟加载商品SKU数据
const basePrice = this.product.price
@@ -620,7 +728,7 @@ export default {
return sku.sku_code
},
- addToCart() {
+ async addToCart() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
@@ -629,50 +737,42 @@ export default {
return
}
- // 获取现有购物车数据
- const cartData = uni.getStorageSync('cart')
- let cartItems: any[] = []
-
- if (cartData) {
- try {
- cartItems = JSON.parse(cartData as string) as any[]
- } catch (e) {
- console.error('解析购物车数据失败', e)
- }
- }
-
- // 检查商品是否已存在 (同一SKU)
- const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
-
- if (existingItem) {
- existingItem.quantity += this.quantity
- } else {
- // 查找SKU信息
- const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
-
- // 添加新商品
- cartItems.push({
- id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
- productId: this.product.id,
- shopId: this.merchant.id,
- shopName: this.merchant.shop_name,
- name: this.product.name,
- price: sku ? sku.price : this.product.price,
- image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
- spec: this.selectedSpec,
- quantity: this.quantity,
- selected: true
- })
- }
-
- // 保存回存储
- uni.setStorageSync('cart', JSON.stringify(cartItems))
-
- // 模拟添加到购物车
- uni.showToast({
- title: '已添加到购物车',
- icon: 'success'
+ // 显示加载中
+ 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'
+ })
+ } else {
+ console.error('添加购物车返回失败')
+ uni.showToast({
+ title: '添加失败,请登录重试',
+ icon: 'none'
+ })
+ }
+ } catch (e) {
+ uni.hideLoading()
+ console.error('添加购物车异常', e)
+ uni.showToast({
+ title: '添加异常',
+ icon: 'none'
+ })
+ }
},
buyNow() {
@@ -787,6 +887,14 @@ export default {
})
},
+ goToShop() {
+ if (this.merchant.user_id) {
+ uni.navigateTo({
+ url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.user_id}`
+ })
+ }
+ },
+
goToCart() {
uni.switchTab({
url: '/pages/mall/consumer/cart'
diff --git a/pages/mall/consumer/shop-detail.uvue b/pages/mall/consumer/shop-detail.uvue
index c2bf505e..cb4d5aa2 100644
--- a/pages/mall/consumer/shop-detail.uvue
+++ b/pages/mall/consumer/shop-detail.uvue
@@ -44,6 +44,7 @@
+
+
\ No newline at end of file
diff --git a/uni_modules/ak-req/ak-req.uts b/uni_modules/ak-req/ak-req.uts
index 1c8b5a11..a33c28cb 100644
--- a/uni_modules/ak-req/ak-req.uts
+++ b/uni_modules/ak-req/ak-req.uts
@@ -1,4 +1,5 @@
import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts';
+import { SUPA_URL } from '@/ak/config.uts';
// token 持久化 key
const ACCESS_TOKEN_KEY = 'akreq_access_token';
@@ -75,7 +76,7 @@ export class AkReq {
headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject;
} try {
const res = await this.request({
- url: 'https://ak3.oulog.com/auth/v1/token?grant_type=refresh_token',
+ url: SUPA_URL + '/auth/v1/token?grant_type=refresh_token',
method: 'POST',
data: ({ refresh_token: refreshToken } as UTSJSONObject),
headers: headers,
diff --git a/utils/supabaseService.uts b/utils/supabaseService.uts
index 3ee23527..beecb3bb 100644
--- a/utils/supabaseService.uts
+++ b/utils/supabaseService.uts
@@ -1,9 +1,8 @@
-import { createClient } from '@/components/supadb/aksupa.uts'
-import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
+import supa from '@/components/supadb/aksupainstance.uts'
import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
-// 创建 Supabase 客户端
-const supa = createClient(SUPA_URL, SUPA_KEY)
+// 使用单例 Supabase 客户端
+// const supa = createClient(SUPA_URL, SUPA_KEY)
// 类型定义
export interface Category {
@@ -18,18 +17,46 @@ export interface Category {
export interface Product {
id: string
category_id: string
+ merchant_id: string
name: string
description?: string
- specification: string
+ specification?: string
price: number
+ base_price?: number
original_price?: number
+ market_price?: number
image?: string
- manufacturer: string
+ main_image_url?: string
+ image_urls?: string // JSON string
+ manufacturer?: string
sales?: number
+ sale_count?: number
stock?: number
+ available_stock?: number
badge?: string
shop_id?: string
shop_name?: string
+ attributes?: string // JSON string
+ created_at?: string
+ expiry_date?: string
+ approval_number?: string
+ usage?: string
+ side_effects?: string
+}
+
+export interface Shop {
+ id: string
+ merchant_id: string
+ shop_name: string
+ shop_logo?: string
+ shop_banner?: string
+ description?: string
+ contact_name?: string
+ contact_phone?: string
+ rating_avg?: number
+ total_sales?: number
+ product_count?: number
+ total_sales_count?: number
created_at?: string
}
@@ -73,10 +100,33 @@ export interface PaginatedResponse {
hasmore: boolean
}
+export interface ProductSku {
+ id: string
+ product_id: string
+ sku_code: string
+ specifications: string // JSON string
+ price: number
+ market_price?: number
+ cost_price?: number
+ stock?: number
+ warning_stock?: number
+ image_url?: string
+ weight?: number
+ status?: number
+ created_at?: string
+}
+
class SupabaseService {
// 获取当前用户ID
- private getCurrentUserId(): string | null {
+ public getCurrentUserId(): string | null {
try {
+ // 优先从 Supabase 会话获取
+ const session = supa.getSession()
+ if (session && session.user) {
+ return session.user.getString('id')
+ }
+
+ // 后备:尝试从本地存储获取 (兼容旧逻辑)
const userId = uni.getStorageSync('user_id')
return userId ? userId as string : null
} catch (e) {
@@ -89,7 +139,7 @@ class SupabaseService {
async getCategories(): Promise {
try {
const response = await supa
- .from('categories')
+ .from('ml_categories')
.select('*')
.order('name', { ascending: true })
.execute()
@@ -114,10 +164,10 @@ class SupabaseService {
): Promise> {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*', { count: 'exact' })
.eq('category_id', categoryId)
- .order('sales', { ascending: false })
+ .order('sale_count', { ascending: false })
.page(page)
.limit(limit)
.execute()
@@ -152,6 +202,28 @@ class SupabaseService {
}
}
+ // 根据商品ID获取SKU列表
+ async getProductSkus(productId: string): Promise {
+ try {
+ const response = await supa
+ .from('ml_product_skus')
+ .select('*')
+ .eq('product_id', productId)
+ .eq('status', 1)
+ .execute()
+
+ if (response.error) {
+ console.error('获取商品SKU失败:', response.error)
+ return []
+ }
+
+ return response.data as ProductSku[]
+ } catch (error) {
+ console.error('获取商品SKU异常:', error)
+ return []
+ }
+ }
+
// 搜索商品
async searchProducts(
keyword: string,
@@ -162,18 +234,18 @@ class SupabaseService {
): Promise> {
try {
let query = supa
- .from('products')
+ .from('ml_products')
.select('*', { count: 'exact' })
.or(`name.ilike.%${keyword}%,manufacturer.ilike.%${keyword}%,specification.ilike.%${keyword}%`)
// 根据sortBy和ascending设置排序
if (sortBy === 'price') {
- query = query.order('price', { ascending })
- } else if (sortBy === 'sales') {
- query = query.order('sales', { ascending: false }) // 销量总是降序
+ query = query.order('base_price', { ascending })
+ } else if (sortBy === 'sales' || sortBy === 'sale_count') {
+ query = query.order('sale_count', { ascending: false }) // 销量总是降序
} else {
// 默认按销量降序
- query = query.order('sales', { ascending: false })
+ query = query.order('sale_count', { ascending: false })
}
const response = await query
@@ -215,7 +287,7 @@ class SupabaseService {
async getProductById(productId: string): Promise {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.eq('id', productId)
.single()
@@ -233,11 +305,80 @@ class SupabaseService {
}
}
+ // 根据商户ID获取店铺信息
+ async getShopByMerchantId(merchantId: string): Promise {
+ try {
+ const response = await supa
+ .from('ml_shops')
+ .select('*')
+ .eq('merchant_id', merchantId)
+ .single()
+ .executeAs()
+
+ if (response.error) {
+ console.error('获取店铺信息失败:', response.error)
+ return null
+ }
+
+ const data = response.data
+ if (Array.isArray(data)) {
+ if (data.length > 0) return data[0] as Shop
+ return null
+ }
+ return data as Shop
+ } catch (error) {
+ console.error('获取店铺信息异常:', error)
+ return null
+ }
+ }
+
+ // 根据商户ID获取商品列表
+ async getProductsByMerchantId(merchantId: string, page: number = 1, limit: number = 20): Promise> {
+ try {
+ const response = await supa
+ .from('ml_products')
+ .select('*', { count: 'exact' })
+ .eq('merchant_id', merchantId)
+ .order('created_at', { ascending: false })
+ .page(page)
+ .limit(limit)
+ .execute()
+
+ if (response.error) {
+ console.error('获取商户商品失败:', response.error)
+ return {
+ data: [],
+ total: 0,
+ page,
+ limit,
+ hasmore: false
+ }
+ }
+
+ return {
+ data: response.data as Product[],
+ total: response.total || 0,
+ page,
+ limit,
+ hasmore: response.hasmore || false
+ }
+ } catch (error) {
+ console.error('获取商户商品异常:', error)
+ return {
+ data: [],
+ total: 0,
+ page,
+ limit,
+ hasmore: false
+ }
+ }
+ }
+
// 获取热销商品(按销量排序)
async getHotProducts(limit: number = 10): Promise {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.order('sales', { ascending: false })
.limit(limit)
@@ -259,7 +400,7 @@ class SupabaseService {
async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.order('price', { ascending })
.limit(limit)
@@ -281,7 +422,7 @@ class SupabaseService {
async getProductsByNewest(limit: number = 10): Promise {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.order('created_at', { ascending: false })
.limit(limit)
@@ -304,7 +445,7 @@ class SupabaseService {
try {
// 直接使用 neq 空字符串查询,忽略 null 值(null 表示没有 badge,不应被推荐)
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.neq('badge', '')
.order('sales', { ascending: false })
@@ -328,7 +469,7 @@ class SupabaseService {
async getDiscountProducts(limit: number = 10): Promise {
try {
const response = await supa
- .from('products')
+ .from('ml_products')
.select('*')
.eq('badge', '特价')
.order('sales', { ascending: false })
@@ -369,14 +510,13 @@ class SupabaseService {
selected,
created_at,
updated_at,
- products!inner (
+ ml_products!inner (
id,
name,
image,
price,
specification,
- shop_id,
- shop_name
+ merchant_id
)
`)
.eq('user_id', userId)
@@ -387,12 +527,59 @@ class SupabaseService {
console.error('获取购物车失败:', response.error)
return []
}
+
+ const cartData = response.data as any[]
+
+ // 调试日子:打印购物车数据第一条结构,确认产品字段名
+ if (cartData && Array.isArray(cartData) && cartData.length > 0) {
+ console.log('Cart Item Structure:', JSON.stringify(cartData[0]))
+ }
+
+ const merchantIds: string[] = []
+
+ if (cartData && Array.isArray(cartData)) {
+ for (const item of cartData) {
+ // PostgREST 返回的关联字段通常与表名一致
+ // 尝试获取ml_products,如果为空则尝试products
+ let product = item['ml_products'] as any
+ if (!product) {
+ product = item['products'] as any
+ }
+
+ if (product && product.merchant_id && !merchantIds.includes(product.merchant_id)) {
+ merchantIds.push(product.merchant_id as string)
+ }
+ }
+ }
+
+ // 查询店铺信息
+ const shopMap = new Map()
+ if (merchantIds.length > 0) {
+ const shopRes = await supa
+ .from('ml_shops')
+ .select('id, merchant_id, shop_name')
+ .in('merchant_id', merchantIds)
+ .execute()
+
+ if (!shopRes.error && shopRes.data != null) {
+ const shops = shopRes.data as any[]
+ for (const shop of shops) {
+ shopMap.set(shop.merchant_id as string, shop)
+ }
+ }
+ }
// 处理返回数据,构建CartItem数组
const cartItems: CartItem[] = []
- if (response.data && Array.isArray(response.data)) {
- for (const item of response.data) {
- const product = item.products as any
+ if (cartData && Array.isArray(cartData)) {
+ for (const item of cartData) {
+ let product = item['ml_products'] as any
+ if (!product) {
+ product = item['products'] as any
+ }
+
+ const merchantId = product?.merchant_id as string
+ const shopInfo = shopMap.get(merchantId)
cartItems.push({
id: item.id as string,
@@ -405,8 +592,8 @@ class SupabaseService {
product_image: product?.image as string,
product_price: product?.price as number,
product_specification: product?.specification as string,
- shop_id: product?.shop_id as string,
- shop_name: product?.shop_name as string,
+ shop_id: shopInfo ? (shopInfo['id'] as string) : (merchantId || 'unknown_shop'),
+ shop_name: shopInfo ? (shopInfo['shop_name'] as string) : '未知店铺',
created_at: item.created_at as string,
updated_at: item.updated_at as string
})
@@ -430,27 +617,59 @@ class SupabaseService {
}
// 检查商品是否已在购物车中
- const existingResponse = await supa
+ // 注意:必须处理 sku_id 为空的情况,使用 is.null 过滤器
+ let query = supa
.from('ml_shopping_cart')
.select('*')
.eq('user_id', userId)
.eq('product_id', productId)
- .eq('sku_id', skuId || '')
- .single()
- .execute()
+
+ if (skuId && skuId.length > 0) {
+ query = query.eq('sku_id', skuId)
+ } else {
+ query = query.is('sku_id', null)
+ }
+
+ const existingResponse = await query.single().execute()
+
+ let existingItem: any | null = null
+
+ if (existingResponse.data != null) {
+ const rawData = existingResponse.data as any
+ if (Array.isArray(rawData)) {
+ if (rawData.length > 0) {
+ existingItem = rawData[0]
+ }
+ } else {
+ existingItem = rawData
+ }
+ }
let response
- if (existingResponse.data) {
+ if (existingItem != null) {
// 商品已存在,更新数量
- const existingItem = existingResponse.data as any
- response = await supa
- .from('ml_shopping_cart')
- .update({
- quantity: (existingItem.quantity || 0) + quantity,
- updated_at: new Date().toISOString()
- })
- .eq('id', existingItem.id)
- .execute()
+ console.log('Found existing cart item:', JSON.stringify(existingItem))
+
+ // 确保 existingItem.id 存在
+ const itemId = existingItem['id']
+ const itemQty = existingItem['quantity']
+
+ if (itemId != null) {
+ const currentQty = typeof itemQty === 'number' ? itemQty : parseInt(String(itemQty || 0))
+ const newQty = currentQty + quantity
+
+ response = await supa
+ .from('ml_shopping_cart')
+ .update({
+ quantity: newQty,
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', itemId)
+ .execute()
+ } else {
+ console.error('购物车已有商品但缺少ID,无法更新. Data:', JSON.stringify(existingItem))
+ return false
+ }
} else {
// 商品不存在,添加新记录
response = await supa
@@ -671,7 +890,7 @@ class SupabaseService {
const response = await supa
.from('ml_user_addresses')
- .select('*')
+ .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
.eq('user_id', userId)
.order('is_default', { ascending: false })
.order('created_at', { ascending: false })
@@ -700,7 +919,7 @@ class SupabaseService {
const response = await supa
.from('ml_user_addresses')
- .select('*')
+ .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
.eq('id', addressId)
.eq('user_id', userId)
.single()
@@ -745,12 +964,12 @@ class SupabaseService {
.from('ml_user_addresses')
.insert({
user_id: userId,
- recipient_name: address.recipient_name,
- phone: address.phone,
+ receiver_name: address.recipient_name,
+ receiver_phone: address.phone,
province: address.province,
city: address.city,
district: address.district,
- detail_address: address.detail_address,
+ address_detail: address.detail_address,
postal_code: address.postal_code || null,
is_default: address.is_default || false,
created_at: new Date().toISOString(),
@@ -792,13 +1011,22 @@ class SupabaseService {
if (address.is_default) {
await this.clearDefaultAddress(userId)
}
+
+ // 构造更新数据,映射字段名到数据库列名
+ const updateData = {}
+ if (address.recipient_name != null) updateData['receiver_name'] = address.recipient_name
+ if (address.phone != null) updateData['receiver_phone'] = address.phone
+ if (address.province != null) updateData['province'] = address.province
+ if (address.city != null) updateData['city'] = address.city
+ if (address.district != null) updateData['district'] = address.district
+ if (address.detail_address != null) updateData['address_detail'] = address.detail_address
+ if (address.postal_code != null) updateData['postal_code'] = address.postal_code
+ if (address.is_default != null) updateData['is_default'] = address.is_default
+ updateData['updated_at'] = new Date().toISOString()
const response = await supa
.from('ml_user_addresses')
- .update({
- ...address,
- updated_at: new Date().toISOString()
- })
+ .update(updateData)
.eq('id', addressId)
.eq('user_id', userId)
.execute()
@@ -843,6 +1071,298 @@ class SupabaseService {
}
}
+ // 清除默认地址(内部使用)
+ private async clearDefaultAddress(userId: string): Promise {
+ try {
+ await supa
+ .from('ml_user_addresses')
+ .update({
+ is_default: false,
+ updated_at: new Date().toISOString()
+ })
+ .eq('user_id', userId)
+ .eq('is_default', true)
+ .execute()
+ } catch (error) {
+ console.error('清除默认地址异常:', error)
+ }
+ }
+
+ // 获取用户资料
+ async getUserProfile(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return null
+
+ // 联合查询 auth user 和 profile
+ // 由于 Supabase auth table 不可直接访问,这里查询 ml_user_profiles
+ const response = await supa
+ .from('ml_user_profiles')
+ .select('*')
+ .eq('user_id', userId)
+ .single()
+ .execute()
+
+ if (response.error) {
+ // 如果不存在 profile,可能只有 auth user,这里暂时返回空或创建默认
+ return null
+ }
+ return response.data
+ } catch (e) {
+ return null
+ }
+ }
+
+ // 创建订单
+ async createOrder(orderData: {
+ merchant_id: string,
+ product_amount: number,
+ shipping_fee: number,
+ total_amount: number,
+ shipping_address: any,
+ items: any[]
+ }): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return null
+
+ // 生成订单号
+ const orderNo = 'ORD' + Date.now() + Math.floor(Math.random() * 1000)
+
+ // 1. 创建主订单
+ const orderResponse = await supa
+ .from('ml_orders')
+ .insert({
+ user_id: userId,
+ merchant_id: orderData.merchant_id,
+ order_no: orderNo,
+ product_amount: orderData.product_amount,
+ shipping_fee: orderData.shipping_fee,
+ total_amount: orderData.total_amount,
+ paid_amount: 0,
+ shipping_address: JSON.stringify(orderData.shipping_address),
+ order_status: 1, // 待付款
+ payment_status: 1, // 未支付
+ shipping_status: 1, // 未发货
+ created_at: new Date().toISOString()
+ })
+ .select()
+ .single()
+ .execute()
+
+ if (orderResponse.error) {
+ console.error('创建订单失败:', orderResponse.error)
+ return null
+ }
+
+ const orderId = orderResponse.data['id'] as string
+
+ // 2. 创建订单项
+ const orderItems = orderData.items.map((item: any) => ({
+ order_id: orderId,
+ product_id: item.product_id,
+ sku_id: item.sku_id || null,
+ product_name: item.product_name,
+ sku_name: item.sku_name || '',
+ specifications: item.specifications ? JSON.stringify(item.specifications) : '{}',
+ image_url: item.image_url,
+ price: item.price,
+ quantity: item.quantity,
+ total_amount: item.price * item.quantity,
+ created_at: new Date().toISOString()
+ }))
+
+ const itemsResponse = await supa
+ .from('ml_order_items')
+ .insert(orderItems)
+ .execute()
+
+ if (itemsResponse.error) {
+ console.error('创建订单项失败:', itemsResponse.error)
+ // 此时应该回滚订单,但这里简化处理
+ return null
+ }
+
+ // 3. 清除购物车中已购买的商品(如果是从购物车购买)
+ // 这一步通常在前端调用 removeCartItem 或在此处根据参数处理
+
+ return orderId
+ } catch (error) {
+ console.error('创建订单异常:', error)
+ return null
+ }
+ }
+
+ // 获取订单列表
+ async getOrders(status: number = 0): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return []
+
+ let query = supa
+ .from('ml_orders')
+ .select(`
+ *,
+ ml_order_items (*)
+ `)
+ .eq('user_id', userId)
+ .order('created_at', { ascending: false })
+
+ if (status > 0) {
+ query = query.eq('order_status', status)
+ }
+
+ const response = await query.execute()
+
+ if (response.error) {
+ console.error('获取订单列表失败:', response.error)
+ return []
+ }
+
+ return response.data || []
+ } catch (error) {
+ console.error('获取订单列表异常:', error)
+ return []
+ }
+ }
+
+ // 获取订单详情
+ async getOrderDetail(orderId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return null
+
+ const response = await supa
+ .from('ml_orders')
+ .select(`
+ *,
+ ml_order_items (*),
+ ml_shops (shop_name, id)
+ `)
+ .eq('id', orderId)
+ .eq('user_id', userId)
+ .single()
+ .execute()
+
+ if (response.error) {
+ return null
+ }
+ return response.data
+ } catch (e) {
+ return null
+ }
+ }
+
+ // 收藏相关
+ async checkFavorite(productId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return false
+
+ const response = await supa
+ .from('ml_user_favorites')
+ .select('id')
+ .eq('user_id', userId)
+ .eq('target_id', productId)
+ .eq('target_type', 1) // 1 for product
+ .single()
+ .execute()
+
+ return !!response.data
+ } catch(e) {
+ return false
+ }
+ }
+
+ async toggleFavorite(productId: string): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return false
+
+ // Check if exists
+ const exists = await this.checkFavorite(productId)
+
+ if (exists) {
+ // Delete
+ await supa
+ .from('ml_user_favorites')
+ .delete()
+ .eq('user_id', userId)
+ .eq('target_id', productId)
+ .eq('target_type', 1)
+ .execute()
+ return false // Now not favorite
+ } else {
+ // Add
+ await supa
+ .from('ml_user_favorites')
+ .insert({
+ user_id: userId,
+ target_id: productId,
+ target_type: 1,
+ created_at: new Date().toISOString()
+ })
+ .execute()
+ return true // Now favorite
+ }
+ } catch (e) {
+ return false
+ }
+ }
+
+ async getFavorites(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return []
+
+ // 需要关联查询商品信息
+ const response = await supa
+ .from('ml_user_favorites')
+ .select(`
+ id,
+ target_id,
+ created_at,
+ ml_products!target_id (
+ id, name, image_urls, main_image_url, price, sales
+ )
+ `)
+ .eq('user_id', userId)
+ .eq('target_type', 1)
+ .order('created_at', { ascending: false })
+ .execute()
+
+ if (response.error) return []
+ return response.data || []
+ } catch (e) {
+ return []
+ }
+ }
+
+
+ async getAddressList(): Promise {
+ try {
+ const userId = this.getCurrentUserId()
+ if (!userId) return []
+
+ const response = await supa
+ .from('ml_user_addresses')
+ .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail')
+ .eq('user_id', userId)
+ .order('is_default', { ascending: false })
+ .order('created_at', { ascending: false })
+ .execute()
+
+ if (response.error) {
+ console.error('获取地址列表失败:', response.error)
+ return []
+ }
+ return response.data as UserAddress[]
+ } catch (e) {
+ console.error('获取地址列表异常:', e)
+ return []
+ }
+ }
+
// 设置默认地址
async setDefaultAddress(addressId: string): Promise {
try {
@@ -877,31 +1397,6 @@ class SupabaseService {
return false
}
}
-
- // 清除用户的默认地址(内部方法)
- private async clearDefaultAddress(userId: string): Promise {
- try {
- const response = await supa
- .from('ml_user_addresses')
- .update({
- is_default: false,
- updated_at: new Date().toISOString()
- })
- .eq('user_id', userId)
- .eq('is_default', true)
- .execute()
-
- if (response.error) {
- console.error('清除默认地址失败:', response.error)
- return false
- }
-
- return true
- } catch (error) {
- console.error('清除默认地址异常:', error)
- return false
- }
- }
}
// 导出单例实例
From 21149dd3febdead4c37d285504ad9f2ddacad123 Mon Sep 17 00:00:00 2001
From: comlibmb <1844410276@qq.com>
Date: Mon, 2 Feb 2026 18:09:30 +0800
Subject: [PATCH 15/18] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=E7=BC=96=E5=86=99=EF=BC=8C=E5=BC=80=E5=8F=91=E8=A7=84?=
=?UTF-8?q?=E8=8C=83=E6=96=87=E6=A1=A3=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=BA=93?=
=?UTF-8?q?=E6=8E=A5=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components/analytics/AnalyticsComboChart.uvue | 5 +-
.../analytics/AnalyticsSidebarMenu.uvue | 188 +++++++++-
docs/AGENT_PROJECT_SPEC.md | 325 +++++++++++++++++
docs/sql/00_overview.md | 32 ++
docs/sql/01_tables_catalog.md | 236 +++++++++++++
docs/sql/02_relationships_er.md | 147 ++++++++
docs/sql/03_enums_status_dict.md | 213 +++++++++++
docs/sql/04_triggers_and_functions.md | 240 +++++++++++++
docs/sql/05_rls_permissions_matrix.md | 159 +++++++++
docs/sql/06_indexes_and_query_patterns.md | 179 ++++++++++
docs/sql/07_business_workflows.md | 333 ++++++++++++++++++
docs/sql/08_data_consistency_boundaries.md | 116 ++++++
docs/sql/09_migrations_and_versions.md | 126 +++++++
docs/sql/10_quality_checks.md | 91 +++++
docs/sql/11_roles_and_permissions_strategy.md | 184 ++++++++++
docs/sql/README.md | 17 +
docs/sql_summary.md | 90 +++++
mall_sql/migrations/ml_analytics_rpcs.sql | 46 ++-
pages/mall/analytics/coupon-analysis.uvue | 54 ++-
pages/mall/analytics/custom-report.uvue | 30 +-
pages/mall/analytics/delivery-analysis.uvue | 52 ++-
pages/mall/analytics/index.uvue | 13 +-
pages/mall/analytics/market-trends.uvue | 54 ++-
pages/mall/analytics/product-insights.uvue | 66 +++-
pages/mall/analytics/sales-report.uvue | 5 +-
.../test/01_ml_analytics_rpcs_user.sql | 46 ++-
pages/mall/analytics/user-analysis.uvue | 50 ++-
pages/user/login.uvue | 15 +-
pages/user/register.uvue | 17 +-
services/analytics/authGuard.uts | 20 ++
services/analytics/couponAnalysisService.uts | 15 +-
services/analytics/customReportService.uts | 4 +-
.../analytics/deliveryAnalysisService.uts | 14 +-
services/analytics/marketTrendsService.uts | 15 +-
services/analytics/productInsightsService.uts | 75 +++-
utils/authRedirect.uts | 62 ++++
36 files changed, 3245 insertions(+), 89 deletions(-)
create mode 100644 docs/AGENT_PROJECT_SPEC.md
create mode 100644 docs/sql/00_overview.md
create mode 100644 docs/sql/01_tables_catalog.md
create mode 100644 docs/sql/02_relationships_er.md
create mode 100644 docs/sql/03_enums_status_dict.md
create mode 100644 docs/sql/04_triggers_and_functions.md
create mode 100644 docs/sql/05_rls_permissions_matrix.md
create mode 100644 docs/sql/06_indexes_and_query_patterns.md
create mode 100644 docs/sql/07_business_workflows.md
create mode 100644 docs/sql/08_data_consistency_boundaries.md
create mode 100644 docs/sql/09_migrations_and_versions.md
create mode 100644 docs/sql/10_quality_checks.md
create mode 100644 docs/sql/11_roles_and_permissions_strategy.md
create mode 100644 docs/sql/README.md
create mode 100644 docs/sql_summary.md
create mode 100644 services/analytics/authGuard.uts
create mode 100644 utils/authRedirect.uts
diff --git a/components/analytics/AnalyticsComboChart.uvue b/components/analytics/AnalyticsComboChart.uvue
index fd3ef7b2..a328103d 100644
--- a/components/analytics/AnalyticsComboChart.uvue
+++ b/components/analytics/AnalyticsComboChart.uvue
@@ -154,10 +154,11 @@ export default {
data: x,
axisTick: { alignWithLabel: true },
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.12)' } },
- axisLabel: {
+ axisLabel: {
color: 'rgba(0,0,0,0.55)',
rotate: x.length > 12 ? 45 : 0,
- interval: 0
+ // 数据量大时不要强制全部展示,否则会全部重叠
+ interval: x.length > 60 ? 'auto' : 0
}
},
yAxis: [
diff --git a/components/analytics/AnalyticsSidebarMenu.uvue b/components/analytics/AnalyticsSidebarMenu.uvue
index a79dab5d..a0d6c18b 100644
--- a/components/analytics/AnalyticsSidebarMenu.uvue
+++ b/components/analytics/AnalyticsSidebarMenu.uvue
@@ -4,15 +4,32 @@
@@ -22,11 +39,15 @@