consumer模块完成度95%,实现数据库多端注册登录,优化安卓端小程序bug
This commit is contained in:
@@ -703,14 +703,17 @@ export class AkSupa {
|
||||
if (this.apikey == null || this.apikey.trim() === '' || this.apikey === 'your-anon-key') {
|
||||
throw new Error('Supabase 配置错误:请在 ak/config.uts 中设置 SUPA_KEY(当前为占位符)');
|
||||
}
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
const reqData = new UTSJSONObject()
|
||||
reqData.set('email', email)
|
||||
reqData.set('password', password)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/token?grant_type=password',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json'
|
||||
} as UTSJSONObject,
|
||||
data: { email, password } as UTSJSONObject,
|
||||
headers: headers,
|
||||
data: reqData,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
// 如果响应不是 2xx(例如 401),提取后端错误信息并抛出,便于上层显示具体原因
|
||||
@@ -720,7 +723,14 @@ export class AkSupa {
|
||||
try {
|
||||
if (res.data != null) {
|
||||
const obj = new UTSJSONObject(res.data);
|
||||
msg = obj.getString('message') ?? obj.getString('error') ?? obj.getString('msg') ?? obj.getString('description') ?? obj.getString('error_description') ?? msg;
|
||||
const rawMsg = obj.getString('message') ?? obj.getString('error') ?? obj.getString('msg') ?? obj.getString('description') ?? obj.getString('error_description') ?? '';
|
||||
|
||||
// 核心修复:在这里拦截英文错误并转换为中文
|
||||
if (rawMsg.includes('Invalid login credentials')) {
|
||||
msg = '用户名或密码错误';
|
||||
} else if (rawMsg != '') {
|
||||
msg = rawMsg;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@@ -763,15 +773,26 @@ export class AkSupa {
|
||||
};
|
||||
}
|
||||
|
||||
async signUp(email : string, password : string) : Promise<UTSJSONObject> {
|
||||
async signUp(email : string, password : string, options : UTSJSONObject | null = null) : Promise<UTSJSONObject> {
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
const data = new UTSJSONObject()
|
||||
data.set('email', email)
|
||||
data.set('password', password)
|
||||
|
||||
if (options != null) {
|
||||
const dataField = options.getJSON('data')
|
||||
if (dataField != null) {
|
||||
data.set('data', dataField)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/signup',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json'
|
||||
} as UTSJSONObject,
|
||||
data: { email, password } as UTSJSONObject,
|
||||
headers: headers,
|
||||
data: data,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
return res.data as UTSJSONObject;
|
||||
@@ -786,11 +807,10 @@ export class AkSupa {
|
||||
*/
|
||||
async select(table : string, filter ?: string | null, options ?: AkSupaSelectOptions) : Promise<AkReqResponse<any>> {
|
||||
let url = this.baseUrl + '/rest/v1/' + table;
|
||||
let headers = {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AkReq.getToken() ?? ''}`
|
||||
} as UTSJSONObject;
|
||||
let headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
let params : string[] = [];
|
||||
if (options != null) {
|
||||
if (options.columns != null && !(options.columns == "")) params.push('select=' + encodeURIComponent(options.columns ?? ""));
|
||||
@@ -876,20 +896,17 @@ async select_uts(table : string, filter ?: UTSJSONObject | null, options ?: AkSu
|
||||
*/
|
||||
async insert(table : string, row : UTSJSONObject | Array<UTSJSONObject>) : Promise<AkReqResponse<any>> {
|
||||
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;
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
headers.set('Prefer', 'return=representation')
|
||||
|
||||
// 如果是数组,直接传递;如果是单个对象,也直接传递
|
||||
// Supabase REST API 原生支持两种格式
|
||||
let reqOptions : AkReqOptions = {
|
||||
url,
|
||||
method: 'POST',
|
||||
headers,
|
||||
data: row, // 可以是单个对象或数组
|
||||
data: row,
|
||||
contentType: 'application/json'
|
||||
};
|
||||
return await this.requestWithAutoRefresh(reqOptions);
|
||||
@@ -907,12 +924,11 @@ async update(table : string, filter : string | null, values : UTSJSONObject) : P
|
||||
if (filter!=null && filter !== "") {
|
||||
url += '?' + filter;
|
||||
}
|
||||
const headers = {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AkReq.getToken() ?? ''}`,
|
||||
Prefer: 'return=representation'
|
||||
} as UTSJSONObject;
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
headers.set('Prefer', 'return=representation')
|
||||
let reqOptions : AkReqOptions = {
|
||||
url,
|
||||
method: 'PATCH',
|
||||
@@ -934,12 +950,11 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
if (filter!=null && filter !== "") {
|
||||
url += '?' + filter;
|
||||
}
|
||||
const headers = {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AkReq.getToken() ?? ''}`,
|
||||
Prefer: 'return=representation'
|
||||
} as UTSJSONObject;
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
headers.set('Prefer', 'return=representation')
|
||||
let reqOptions : AkReqOptions = {
|
||||
url,
|
||||
method: 'DELETE',
|
||||
@@ -956,17 +971,16 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
* @returns AkReqResponse<any>
|
||||
*/
|
||||
async rpc(functionName : string, params ?: UTSJSONObject) : Promise<AkReqResponse<any>> {
|
||||
const url = `${this.baseUrl}/rest/v1/rpc/${functionName}`;
|
||||
const headers = {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${AkReq.getToken() ?? ''}`
|
||||
} as UTSJSONObject;
|
||||
const url = this.baseUrl + '/rest/v1/rpc/' + functionName;
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
let reqOptions : AkReqOptions = {
|
||||
url,
|
||||
method: 'POST',
|
||||
headers,
|
||||
data: params ?? {},
|
||||
data: params ?? new UTSJSONObject(),
|
||||
contentType: 'application/json'
|
||||
};
|
||||
return await this.requestWithAutoRefresh(reqOptions);
|
||||
@@ -998,14 +1012,16 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
async refreshSession() : Promise<boolean> {
|
||||
if (this.session == null || this.session?.refresh_token == null) return false;
|
||||
try {
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
const data = new UTSJSONObject()
|
||||
data.set('refresh_token', this.session?.refresh_token)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/token?grant_type=refresh_token',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: this.apikey,
|
||||
'Content-Type': 'application/json'
|
||||
} as UTSJSONObject,
|
||||
data: { refresh_token: this.session?.refresh_token } as UTSJSONObject,
|
||||
headers: headers,
|
||||
data: data,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
if (res.status == 200 && (res.data != null)) {
|
||||
@@ -1034,6 +1050,23 @@ async delete(table : string, filter : string | null) : Promise<AkReqResponse<any
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserMetadata(metadata: UTSJSONObject): Promise<UTSJSONObject> {
|
||||
const headers = new UTSJSONObject()
|
||||
headers.set('apikey', this.apikey)
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${AkReq.getToken() ?? ''}`)
|
||||
const data = new UTSJSONObject()
|
||||
data.set('data', metadata)
|
||||
const res = await AkReq.request({
|
||||
url: this.baseUrl + '/auth/v1/user',
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
data: data,
|
||||
contentType: 'application/json'
|
||||
}, false);
|
||||
return res.data as UTSJSONObject;
|
||||
}
|
||||
|
||||
// AkSupa类内新增:自动刷新封装
|
||||
async requestWithAutoRefresh(reqOptions : AkReqOptions, isRetry = false) : Promise<AkReqResponse<any>> {
|
||||
let res = await AkReq.request(reqOptions, false);
|
||||
|
||||
100
doc_mall/consumer/MULTI_ROLE_LOGIN_ARCHITECTURE.md
Normal file
100
doc_mall/consumer/MULTI_ROLE_LOGIN_ARCHITECTURE.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 多端、多角色登录架构设计与权限控制方案 (RFC)
|
||||
|
||||
## 1. 业务背景
|
||||
本项目包含多个业务线(如:商城、学校管理、配送中心等)及多种用户角色(如:消费者、学生、教师、配送员等)。为减少重复开发并保持用户体验一致,各端统一共用一套登录模板。
|
||||
|
||||
当前面临挑战:
|
||||
1. **角色误判**:新用户注册后被分配了错误的角色(如:注册商城用户却分配了学生角色)。
|
||||
2. **越权访问**:RLS 策略未对角色进行细分,导致特定操作在特定角色下报 403 权限错误。
|
||||
3. **分流逻辑耦合**:登录成功后的跳转逻辑分散在各页面,难以维护。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计核心:解耦登录与业务角色
|
||||
|
||||
### 2.1 统一身份中心 (Auth)
|
||||
- 依然使用 Supabase Auth 作为认证源。
|
||||
- 登录仅负责:验证凭证 -> 获取 JWT -> 建立 Session。
|
||||
|
||||
### 2.2 业务配置化分流
|
||||
在 `utils/store.uts` 或专门的导航服务中维护角色首页映射表。
|
||||
|
||||
```typescript
|
||||
// 路由分流映射配置
|
||||
export const ROLE_HOME_PAGES = {
|
||||
'admin': '/pages/mall/admin/index',
|
||||
'consumer': '/pages/mall/consumer/index',
|
||||
'teacher': '/pages/school/teacher/index',
|
||||
'student': '/pages/school/student/index',
|
||||
'delivery': '/pages/mall/delivery/index'
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色执行自动导航
|
||||
*/
|
||||
export function navigateToRoleHome(role: string) {
|
||||
const target = ROLE_HOME_PAGES[role] || '/pages/mall/consumer/index';
|
||||
uni.reLaunch({ url: target });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 注册阶段的角色控制
|
||||
|
||||
### 3.1 显式角色声明
|
||||
注册时,根据当前所属的应用端,向 `ensureUserProfile` 传递预期的 `defaultRole`。
|
||||
|
||||
- **商城端注册**:默认传 `consumer`
|
||||
- **学校端注册**:默认传 `student`
|
||||
|
||||
### 3.2 修补逻辑建议 (utils/sapi.uts)
|
||||
在 `insert` 进 `ak_users` 表时,必须显式包含 `role` 字段。
|
||||
|
||||
```typescript
|
||||
const newUserData = new UTSJSONObject()
|
||||
newUserData.set('id', userId)
|
||||
newUserData.set('email', email)
|
||||
newUserData.set('role', finalRole) // 显式写入业务角色
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库 RLS 策略完善 (SQL 示例)
|
||||
|
||||
### 4.1 用户资料表 (ak_users)
|
||||
确保用户只能更新自己的资料,但不能绕过逻辑修改自己的 `role`。
|
||||
|
||||
```sql
|
||||
-- 开启 RLS
|
||||
ALTER TABLE ak_users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 允许用户查看自己的资料
|
||||
CREATE POLICY "Users can view own data" ON ak_users
|
||||
FOR SELECT TO authenticated
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
### 4.2 业务表角色锁 (以 ml_shopping_cart 为例)
|
||||
只有角色为 `consumer` 的用户才能执行购物车操作。
|
||||
|
||||
```sql
|
||||
-- 只有消费者可以插入购物车
|
||||
CREATE POLICY "Only consumers can use cart"
|
||||
ON ml_shopping_cart
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
auth.uid() = user_id AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM ak_users
|
||||
WHERE id = auth.uid() AND role = 'consumer'
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 迁移与修复建议
|
||||
1. **现状修正**:检查 Supabase 后台 `ak_users` 表,手动或批量修改 `student` 角色的消费者用户为 `consumer`。
|
||||
2. **清理缓存**:小程序环境下需清理本地 `user_id` 缓存后重新登录,以刷新受限角色。
|
||||
3. **URL 规范**:建议将各端的页面放入对应的分层目录中(如 `/pages/mall/...`, `/pages/school/...`),便于权限隔离。
|
||||
@@ -334,35 +334,55 @@ const updateRecommendList = (recommends: Product[]) => {
|
||||
|
||||
const refreshRecommend = async () => {
|
||||
try {
|
||||
// 鷻加随机偏移量, const randomOffset = Math.floor(Math.random() * 1000)
|
||||
const hotResp = await supabaseService.searchProducts('', recommendPage.value, 6, 'sales')
|
||||
const recommends = hotResp.data
|
||||
// 1. 模拟市面加载感,锁定按钮防止连续快速点击
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
// 如果新页码没有数据,重置为第一页再试一次
|
||||
if (recommends.length === 0 && recommendPage.value > 1) {
|
||||
recommendPage.value = 1
|
||||
const firstPageResp = await supabaseService.searchProducts('', 1, 6, 'sales')
|
||||
const firstRecommends = firstPageResp.data
|
||||
|
||||
if (firstRecommends.length > 0) {
|
||||
updateRecommendList(firstRecommends)
|
||||
uni.showToast({
|
||||
title: '已重置推荐',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return
|
||||
uni.showLoading({
|
||||
title: '正在挑选...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 2. 模拟市面“随机性”逻辑:
|
||||
// 淘宝京东不会按顺序翻页,而是跳跃选取页码,并打乱排序规则
|
||||
const maxOffsetPages = 20 // 假设数据库中至少有 20 页热推商品
|
||||
const sorts = ['sales', 'price_asc', 'rating']
|
||||
|
||||
// 随机页码 + 随机排序 = 每次点击都有新发现
|
||||
const nextRandomPage = Math.floor(Math.random() * maxOffsetPages) + 1
|
||||
const randomSort = sorts[Math.floor(Math.random() * sorts.length)]
|
||||
|
||||
console.log(`[refreshRecommend] 换一批: 随机页=${nextRandomPage}, 随机排=${randomSort}`)
|
||||
|
||||
const hotResp = await supabaseService.searchProducts('', nextRandomPage, 6, randomSort)
|
||||
let recommends = hotResp.data
|
||||
|
||||
// 3. 兜底逻辑:如果随机到的页码没数据,回退到第 1 页
|
||||
if (recommends.length === 0) {
|
||||
const fallbackResp = await supabaseService.searchProducts('', 1, 6, 'sales')
|
||||
recommends = fallbackResp.data
|
||||
}
|
||||
|
||||
// 4. 前端打乱 (Shuffle):即使是同一页数据,乱序排布也会增加“新鲜感”
|
||||
if (recommends.length > 0) {
|
||||
recommends.sort(() => Math.random() - 0.5)
|
||||
updateRecommendList(recommends)
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '已更新推荐',
|
||||
icon: 'none'
|
||||
title: '已为你换一批好物',
|
||||
icon: 'none',
|
||||
duration: 1000
|
||||
})
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('刷新推荐失败:', error)
|
||||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
<text class="service-icon">👑</text>
|
||||
<text class="service-text">会员中心</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToSettings">
|
||||
<text class="service-icon">⚙️</text>
|
||||
<text class="service-text">设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -102,7 +106,7 @@
|
||||
<view class="order-shortcuts">
|
||||
<view class="section-header-row">
|
||||
<text class="section-title">我的订单</text>
|
||||
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
|
||||
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ❯</text>
|
||||
</view>
|
||||
<view class="order-tabs">
|
||||
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
|
||||
@@ -144,7 +148,7 @@
|
||||
<view class="order-shop">
|
||||
<text class="shop-icon">🏪</text>
|
||||
<text class="shop-name">{{ getOrderShopName(order) }}</text>
|
||||
<text class="shop-arrow">›</text>
|
||||
<text class="shop-arrow"> › </text>
|
||||
</view>
|
||||
<view class="status-row">
|
||||
<text class="order-status-text" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- 聊天头部 -->
|
||||
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="header-back" @click="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
<text class="back-icon">❮</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<view class="header-info-text-wrapper">
|
||||
@@ -566,7 +566,7 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
85
pages/mall/consumer/doc/MULTI_TERMINAL_REGISTRATION_GUIDE.md
Normal file
85
pages/mall/consumer/doc/MULTI_TERMINAL_REGISTRATION_GUIDE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 多端用户注册与身份识别实现指南 (Multi-Terminal Registration Guide)
|
||||
|
||||
本档说明了如何在当前的 Supabase 架构下实现“消费者端”、“商家端”及“管理端”的统一注册逻辑,并确保用户身份(Role)在入库时能够自动、准确地被识别。
|
||||
|
||||
## 1. 核心架构原理
|
||||
|
||||
系统采用 **“前端声明意图 + 后端自动触发”** 的模式:
|
||||
1. **前端 App**:在调用接口注册时,通过 `raw_user_meta_data` 声明用户的目标角色(如 `consumer` 或 `merchant`)。
|
||||
2. **Supabase Auth**:接收并存储这些元数据。
|
||||
3. **数据库触发器 (Trigger)**:在 `auth.users` 产生新记录的一瞬间,由数据库自动读取元数据,并将用户信息连带其正确的角色属性同步到业务表 `ak_users` 中。
|
||||
|
||||
---
|
||||
|
||||
## 2. 前端实现步骤 (代码参考)
|
||||
|
||||
在各端 App 的注册逻辑中,需在调用 `signUp` 接口时传递 `options.data`。
|
||||
|
||||
### 消费者端 (Consumer App)
|
||||
在 `pages/user/register.uvue` 中:
|
||||
```typescript
|
||||
const options = new UTSJSONObject()
|
||||
const metaData = new UTSJSONObject()
|
||||
metaData.set('user_role', 'consumer') // 核心:声明为消费者
|
||||
options.set('data', metaData)
|
||||
|
||||
const result = await supa.signUp(email, password, options)
|
||||
```
|
||||
|
||||
### 商家端 (Merchant App)
|
||||
在商家端的注册页面中,只需修改 `user_role` 的值:
|
||||
```typescript
|
||||
metaData.set('user_role', 'merchant') // 核心:声明为商家
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据库实现步骤 (SQL 设置)
|
||||
|
||||
为了让数据库能够“看碟下菜”,必须在 Supabase SQL Editor 中运行以下脚本,安装/更新智能触发器:
|
||||
|
||||
```sql
|
||||
-- 1. 创建或更新处理函数
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
-- 向业务表插入数据,并智能识别角色
|
||||
INSERT INTO public.ak_users (id, auth_id, email, role, nickname, status)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.id, -- 统一使用 Auth ID
|
||||
NEW.email,
|
||||
-- 核心逻辑:读取 metadata 中的 user_role,如果没有传则默认为 'consumer'
|
||||
COALESCE(NEW.raw_user_meta_data->>'user_role', 'consumer'),
|
||||
-- 默认昵称取邮箱前缀
|
||||
split_part(NEW.email, '@', 1),
|
||||
1 -- 默认激活状态
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- 2. 绑定触发器
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 三端互不干扰的优势
|
||||
|
||||
* **全自动入库**:一旦 SQL 触发器设置完成,前端不再需要手动调用 `ensureUserProfile` 或 `insert` 接口,减少了网络请求和前端报错几率。
|
||||
* **物理隔离与 RLS 安全**:通过 `ak_users` 表的 RLS 策略(`auth.uid() = id`),确保即使用户通过 API 尝试修改他人数据,也会被数据库直接拦截。
|
||||
* **统一维护**:所有端的注册逻辑在数据库层面是统一的,未来若需增加新角色(如 `admin_manager`),只需修改触发器逻辑即可,无需大规模重构代码。
|
||||
|
||||
---
|
||||
|
||||
## 5. 开发建议
|
||||
|
||||
* **强制校验**:在生产环境下,可以在触发器内增加校验逻辑,防止普通用户通过伪造元数据获得 `admin` 角色。
|
||||
* **日志排查**:如果新用户注册后 `ak_users` 表没有数据,请检查 Supabase 控制台的 `Database -> Logs`,查看触发器执行是否有报错(通常是唯一索引冲突导致)。
|
||||
|
||||
---
|
||||
*最后更新时间:2026-03-10*
|
||||
@@ -0,0 +1,76 @@
|
||||
# 电商平台生产环境上线准备清单 (Production Readiness Checklist)
|
||||
|
||||
本指南旨在指导开发者在将“消费者端”和“商家端”推向实际生产环境时,所需准备的各项核心服务申请、资质认定及技术对接点。
|
||||
|
||||
---
|
||||
|
||||
## 1. 身份认证与登录系统 (Identity & Login)
|
||||
|
||||
为了提供丝滑的登录体验,需申请以下第三方服务:
|
||||
|
||||
### A. 微信登录 (WeChat Login)
|
||||
* **主体要求**:必须是企业或个体工商户(个人主体无法开通部分权限)。
|
||||
* **准备工作**:
|
||||
* 在 [微信开放平台](https://open.weixin.qq.com/) 注册账号并完成 **开发者资质认证** (300元/年)。
|
||||
* 创建一个“移动应用”获取 `AppID` 和 `AppSecret`。
|
||||
* 在 [Supabase 控制台](https://supabase.com/dashboard/project/_/auth/providers) 启用 **WeChat Provider** 并填入上述秘钥。
|
||||
* **注意**:如果是微信小程序环境,需在 [微信公众平台](https://mp.weixin.qq.com/) 另外申请小程序账号。
|
||||
|
||||
### B. 手机短信验证码 (SMS Authentication)
|
||||
* **推荐方案**:阿里云短信、腾讯云短信 或 Twilio。
|
||||
* **准备工作**:
|
||||
* **签名申请**:如“【XX商城】”,需提交营业执照审核。
|
||||
* **模板申请**:如“验证码${code},您正在进行登录操作,5分钟内有效。”
|
||||
* **Supabase 对接**:在 Auth -> Providers -> Phone 开启,并配置短信服务商提供的 API 秘钥。
|
||||
|
||||
---
|
||||
|
||||
## 2. 支付与结算系统 (Payments & Settlement)
|
||||
|
||||
支付是电商的命脉,涉及非常严格的合规性审核。
|
||||
|
||||
### A. 消费者端:支付方式与钱包提醒
|
||||
* **资质准备**:
|
||||
* **微信支付**:在 [微信支付商户平台](https://pay.weixin.qq.com/) 申请“商户号”,需关联上述微信 AppID。
|
||||
* **支付宝**:在 [支付宝开放平台](https://open.alipay.com/) 申请“APP支付”或“小程序支付”能力。
|
||||
* **钱包逻辑实现**:
|
||||
* **余额系统**:在数据库中建立 `user_wallets` 表,记录 `balance`(余额)和 `points`(积分)。
|
||||
* **支付拦截器**:用户下单时,需通过后端逻辑(Edge Functions)校验余额是否足够,不足时弹出“余额不足,请充值”或“引导去微信支付”。
|
||||
* **变动提醒**:每当发生消费或退款,需通过 **微信模板消息** 或 **App 推送** 发送钱包变动实时通知。
|
||||
|
||||
### B. 商家端:收款账号与提现
|
||||
* **资金流控制**:生产环境下,资金通常先进入平台大账户(二清合规性建议)。
|
||||
* **提现准备**:
|
||||
* **实名认证**:商家入驻时必须上传 **身份证**、**营业执照**、**开户许可证**。
|
||||
* **打款接口**:需要申请“微信支付-企业付款到零钱”或“支付宝-单笔转账到账户”能力。
|
||||
* **手续费逻辑**:需确定平台抽成比例(佣金),在商家点击提现时自动计算并扣除。
|
||||
|
||||
---
|
||||
|
||||
## 3. 服务器与合规性 (Infrastructure & Compliance)
|
||||
|
||||
* **域名备案**:在中国境内运营,必须完成 **ICP 备案**。
|
||||
* **HTTPS 证书**:所有生产接口必须使用 SSL 证书(不能使用 HTTP)。
|
||||
* **隐私政策与用户协议**:必须在 App 登录页面显著位置提供,且符合 GDPR 或国内网络安全法规定。
|
||||
* **CDN 存储**:商品图片和视频应存放在对象存储(如 Supabase Storage + 自定义 CDN 域名),以保证加载速度。
|
||||
|
||||
---
|
||||
|
||||
## 4. 商家入驻所需材料模板
|
||||
|
||||
建议在商家端后台提供以下材料的上传入口:
|
||||
1. **负责人身份证正反面**。
|
||||
2. **统一社会信用代码证**。
|
||||
3. **银行结算账户信息(开户行、支行名称、账号、持卡人姓名)**。
|
||||
4. **行业特许经营许可**(如卖食品需要《食品经营许可证》)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术架构建议 (Production Stack)
|
||||
|
||||
* **数据库保护**:开启 Supabase 的 **RLS (行级安全策略)**,防止数据越权读取。
|
||||
* **日志记录**:接入 **Sentry** 或 **阿里云日志服务**,记录生产环境中的所有前端报错(尤其是支付环节)。
|
||||
* **压力测试**:上线前需进行接口压力测试,确保在促销活动期间数据库连接数不会爆满。
|
||||
|
||||
---
|
||||
*生成日期:2026-03-10*
|
||||
@@ -97,7 +97,7 @@
|
||||
<view class="log-item" v-for="log in logs" :key="log.id">
|
||||
<view class="log-left">
|
||||
<text class="log-change">{{ getLevelName(log.old_level) }} → {{ getLevelName(log.new_level) }}</text>
|
||||
<text class="log-reason">{{ log.reason || '系统升级' }}</text>
|
||||
<text class="log-reason">{{ log.reason != null && log.reason != '' ? log.reason : '系统升级' }}</text>
|
||||
</view>
|
||||
<text class="log-time">{{ formatDate(log.created_at) }}</text>
|
||||
</view>
|
||||
@@ -154,7 +154,7 @@ const loadMemberInfo = async (): Promise<void> => {
|
||||
try {
|
||||
const result = await supabaseService.getUserMemberInfo()
|
||||
|
||||
memberInfo.value = {
|
||||
const info: MemberInfo = {
|
||||
member_level: result.getNumber('member_level') ?? 0,
|
||||
level_name: result.getString('level_name') ?? '普通会员',
|
||||
discount: result.getNumber('discount') ?? 1.0,
|
||||
@@ -172,14 +172,16 @@ const loadMemberInfo = async (): Promise<void> => {
|
||||
} else {
|
||||
nextLevelObj = JSON.parse(JSON.stringify(nextLevelRaw)) as UTSJSONObject
|
||||
}
|
||||
memberInfo.value.next_level = {
|
||||
const nextLevel: MemberLevel = {
|
||||
id: nextLevelObj.getNumber('id') ?? 0,
|
||||
name: nextLevelObj.getString('name') ?? '',
|
||||
min_amount: nextLevelObj.getNumber('min_amount') ?? 0,
|
||||
discount: 1.0,
|
||||
description: null
|
||||
}
|
||||
info.next_level = nextLevel
|
||||
}
|
||||
memberInfo.value = info
|
||||
} catch (e) {
|
||||
console.error('加载会员信息失败:', e)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
type MessageType = {
|
||||
@@ -60,31 +61,14 @@ const message = ref<MessageType>({
|
||||
|
||||
const extraInfo = ref<ExtraInfoItem[]>([])
|
||||
|
||||
const loadMessage = async (id: string) => {
|
||||
try {
|
||||
const notifications = await supabaseService.getUserNotifications(null)
|
||||
const found = notifications.find(n => n.id === id)
|
||||
|
||||
if (found != null) {
|
||||
message.value = {
|
||||
id: found.id,
|
||||
type: found.type,
|
||||
title: found.title,
|
||||
content: found.content,
|
||||
icon_url: found.icon_url,
|
||||
link_url: found.link_url,
|
||||
extra_data: found.extra_data,
|
||||
created_at: found.created_at
|
||||
}
|
||||
|
||||
// 解析extra_data
|
||||
if (found.extra_data != null) {
|
||||
parseExtraData(found.extra_data)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载消息失败:', e)
|
||||
}
|
||||
const formatLabel = (key: string): string => {
|
||||
if (key === 'share_code') return '分享码'
|
||||
if (key === 'product_name') return '商品名称'
|
||||
if (key === 'reward_amount') return '奖励金额'
|
||||
if (key === 'order_no') return '订单号'
|
||||
if (key === 'buyer_name') return '购买者'
|
||||
if (key === 'quantity') return '数量'
|
||||
return key
|
||||
}
|
||||
|
||||
const parseExtraData = (data: any) => {
|
||||
@@ -93,21 +77,29 @@ const parseExtraData = (data: any) => {
|
||||
if (data == null) return
|
||||
|
||||
try {
|
||||
let dataObj: any = data
|
||||
let dataObj: UTSJSONObject | null = null
|
||||
if (typeof data === 'string') {
|
||||
dataObj = JSON.parse(data)
|
||||
const parsed = JSON.parse(data as string)
|
||||
if (parsed != null) {
|
||||
dataObj = parsed as UTSJSONObject
|
||||
}
|
||||
} else if (data instanceof UTSJSONObject) {
|
||||
dataObj = data
|
||||
} else {
|
||||
dataObj = JSON.parse(JSON.stringify(data)) as UTSJSONObject
|
||||
}
|
||||
|
||||
if (typeof dataObj === 'object') {
|
||||
const keys = Object.keys(dataObj)
|
||||
if (dataObj != null) {
|
||||
const keys = UTSJSONObject.keys(dataObj)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
const value = dataObj[key]
|
||||
const key = keys[i] as string
|
||||
const value = dataObj.get(key)
|
||||
if (value != null) {
|
||||
extraInfo.value.push({
|
||||
const item: ExtraInfoItem = {
|
||||
label: formatLabel(key),
|
||||
value: String(value)
|
||||
})
|
||||
value: `${value}`
|
||||
}
|
||||
extraInfo.value.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,16 +108,32 @@ const parseExtraData = (data: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatLabel = (key: string): string => {
|
||||
const labelMap: Record<string, string> = {
|
||||
'share_code': '分享码',
|
||||
'product_name': '商品名称',
|
||||
'reward_amount': '奖励金额',
|
||||
'order_no': '订单号',
|
||||
'buyer_name': '购买者',
|
||||
'quantity': '数量'
|
||||
const loadMessage = async (id: string) => {
|
||||
try {
|
||||
const notifications = await supabaseService.getUserNotifications(null)
|
||||
const found = notifications.find(n => n.id === id)
|
||||
|
||||
if (found != null) {
|
||||
const extraData = found.extra_data
|
||||
const msg: MessageType = {
|
||||
id: found.id,
|
||||
type: found.type,
|
||||
title: found.title,
|
||||
content: found.content,
|
||||
icon_url: found.icon_url,
|
||||
link_url: found.link_url,
|
||||
extra_data: extraData,
|
||||
created_at: found.created_at ?? ''
|
||||
}
|
||||
message.value = msg
|
||||
|
||||
if (extraData != null) {
|
||||
parseExtraData(extraData)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载消息失败:', e)
|
||||
}
|
||||
return labelMap[key] ?? key
|
||||
}
|
||||
|
||||
const formatTime = (timeStr: string): string => {
|
||||
@@ -155,13 +163,11 @@ const goToLink = () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = (currentPage as any).options
|
||||
if (options != null && options.id != null) {
|
||||
loadMessage(options.id as string)
|
||||
onLoad((options) => {
|
||||
if (options != null) {
|
||||
const idVal = options['id']
|
||||
if (idVal != null) {
|
||||
loadMessage(idVal as string)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -220,7 +220,7 @@ const loadReviews = async (): Promise<void> => {
|
||||
const loadPendingItems = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const orders = await supabaseService.getOrders('completed')
|
||||
const orders = await supabaseService.getOrders(4)
|
||||
const pending: PendingItem[] = []
|
||||
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
@@ -329,18 +329,6 @@ const submitAppend = async (): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (review: MyReviewItem): void => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除这条评价吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
doDelete(review)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const doDelete = async (review: MyReviewItem): Promise<void> => {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
|
||||
@@ -363,6 +351,18 @@ const doDelete = async (review: MyReviewItem): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = (review: MyReviewItem): void => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除这条评价吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
doDelete(review)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const previewImage = (images: string[], index: number): void => {
|
||||
uni.previewImage({
|
||||
urls: images,
|
||||
|
||||
@@ -118,10 +118,10 @@
|
||||
</view>
|
||||
|
||||
<!-- 分享免单入口 (已付款订单显示: 待发货、待收货、已完成,且商家开启了分享免单) -->
|
||||
<view v-if="order.status >= 2 && order.status <= 4 && merchantShareFreeEnabled[order.merchant_id]" class="share-free-row" @click.stop="shareForFree(order)">
|
||||
<view v-if="order.status >= 2 && order.status <= 4 && isShareFreeEnabled(order.merchant_id)" class="share-free-row" @click.stop="shareForFree(order)">
|
||||
<text class="share-free-icon">🎁</text>
|
||||
<text class="share-free-text">分享免单</text>
|
||||
<text class="share-free-tip">分享给好友,{{ merchantRequiredCount[order.merchant_id] != null ? merchantRequiredCount[order.merchant_id] : 4 }}人购买即可免单</text>
|
||||
<text class="share-free-tip">分享给好友,{{ getRequiredCount(order.merchant_id) }}人购买即可免单</text>
|
||||
<text class="share-free-arrow">›</text>
|
||||
</view>
|
||||
|
||||
@@ -235,8 +235,8 @@ const statusBarHeight = ref<number>(0)
|
||||
const searchKeyword = ref<string>('')
|
||||
|
||||
// 商家推销配置缓存
|
||||
const merchantShareFreeEnabled = ref<Record<string, boolean>>({})
|
||||
const merchantRequiredCount = ref<Record<string, number>>({})
|
||||
const merchantShareFreeEnabled = ref<UTSJSONObject>(new UTSJSONObject())
|
||||
const merchantRequiredCount = ref<UTSJSONObject>(new UTSJSONObject())
|
||||
|
||||
// 订单标签页 - 使用 ref 以便整体替换
|
||||
const orderTabs = ref<OrderTabItem[]>([
|
||||
@@ -376,6 +376,62 @@ const filterOrdersByTab = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商家是否开启分享免单
|
||||
const isShareFreeEnabled = (merchantId: string): boolean => {
|
||||
const val = merchantShareFreeEnabled.value.get(merchantId)
|
||||
return val === true
|
||||
}
|
||||
|
||||
// 获取商家要求的购买人数
|
||||
const getRequiredCount = (merchantId: string): number => {
|
||||
const val = merchantRequiredCount.value.get(merchantId)
|
||||
if (val != null && typeof val === 'number') {
|
||||
return val as number
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
// 加载商家推销配置
|
||||
const loadMerchantPromotionConfigs = async (orderList: OrderItem[]) => {
|
||||
// 收集所有唯一的商家ID
|
||||
const merchantIds = new Set<string>()
|
||||
for (let i = 0; i < orderList.length; i++) {
|
||||
const merchantId = orderList[i].merchant_id
|
||||
const existingVal = merchantShareFreeEnabled.value.get(merchantId)
|
||||
if (merchantId != null && merchantId !== '' && existingVal == null) {
|
||||
merchantIds.add(merchantId)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量加载商家配置
|
||||
const merchantIdArray = Array.from(merchantIds)
|
||||
for (let i = 0; i < merchantIdArray.length; i++) {
|
||||
const merchantIdRaw = merchantIdArray[i]
|
||||
const merchantId = merchantIdRaw as string
|
||||
try {
|
||||
const config = await supabaseService.getMerchantPromotionConfig(merchantId)
|
||||
const promotionEnabled = config.get('promotion_enabled')
|
||||
const shareFreeEnabled = config.get('share_free_enabled')
|
||||
const requiredCount = config.get('required_count')
|
||||
|
||||
const isEnabled: any =
|
||||
(promotionEnabled === true || promotionEnabled === 'true') &&
|
||||
(shareFreeEnabled === true || shareFreeEnabled === 'true')
|
||||
merchantShareFreeEnabled.value.set(merchantId, isEnabled)
|
||||
|
||||
if (requiredCount != null) {
|
||||
merchantRequiredCount.value.set(merchantId, requiredCount)
|
||||
} else {
|
||||
merchantRequiredCount.value.set(merchantId, 4 as any)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载商家推销配置失败:', merchantId, e)
|
||||
merchantShareFreeEnabled.value.set(merchantId, false as any)
|
||||
merchantRequiredCount.value.set(merchantId, 4 as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载订单数据
|
||||
const loadOrders = async () => {
|
||||
loading.value = true
|
||||
@@ -547,39 +603,6 @@ onShow(() => {
|
||||
loadOrders()
|
||||
})
|
||||
|
||||
// 加载商家推销配置
|
||||
const loadMerchantPromotionConfigs = async (orderList: OrderItem[]) => {
|
||||
// 收集所有唯一的商家ID
|
||||
const merchantIds = new Set<string>()
|
||||
for (let i = 0; i < orderList.length; i++) {
|
||||
const merchantId = orderList[i].merchant_id
|
||||
if (merchantId != null && merchantId !== '' && !merchantShareFreeEnabled.value.hasOwnProperty(merchantId)) {
|
||||
merchantIds.add(merchantId)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量加载商家配置
|
||||
const merchantIdArray = Array.from(merchantIds)
|
||||
for (let i = 0; i < merchantIdArray.length; i++) {
|
||||
const merchantId = merchantIdArray[i]
|
||||
try {
|
||||
const config = await supabaseService.getMerchantPromotionConfig(merchantId)
|
||||
const promotionEnabled = config.get('promotion_enabled')
|
||||
const shareFreeEnabled = config.get('share_free_enabled')
|
||||
const requiredCount = config.get('required_count')
|
||||
|
||||
merchantShareFreeEnabled.value[merchantId] =
|
||||
(promotionEnabled === true || promotionEnabled === 'true') &&
|
||||
(shareFreeEnabled === true || shareFreeEnabled === 'true')
|
||||
merchantRequiredCount.value[merchantId] = (requiredCount as number) ?? 4
|
||||
} catch (e) {
|
||||
console.error('加载商家推销配置失败:', merchantId, e)
|
||||
merchantShareFreeEnabled.value[merchantId] = false
|
||||
merchantRequiredCount.value[merchantId] = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (isoString: string): string => {
|
||||
if (isoString == '') return ''
|
||||
const date = new Date(isoString)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
|
||||
<text class="enter-shop" @click.stop="goToShop">进店 ❯</text>
|
||||
</view>
|
||||
|
||||
<!-- 功能主治(药品功能) -->
|
||||
@@ -53,7 +53,7 @@
|
||||
{{ coupon.name }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="cell-arrow">领券 ></text>
|
||||
<text class="cell-arrow">领券 ❯</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品参数 -->
|
||||
@@ -62,7 +62,7 @@
|
||||
<view class="cell-content">
|
||||
<text class="params-summary-text">{{ getParamsSummary() }}</text>
|
||||
</view>
|
||||
<text class="cell-arrow">></text>
|
||||
<text class="cell-arrow">❯</text>
|
||||
</view>
|
||||
|
||||
<!-- 规格选择 -->
|
||||
@@ -71,7 +71,7 @@
|
||||
<view class="cell-content">
|
||||
<text class="spec-selected">{{ selectedSpec ?? '请选择规格' }}</text>
|
||||
</view>
|
||||
<text class="cell-arrow">></text>
|
||||
<text class="cell-arrow">❯</text>
|
||||
</view>
|
||||
|
||||
<!-- 数量选择 -->
|
||||
@@ -283,13 +283,13 @@ export default {
|
||||
},
|
||||
onLoad(options: any) {
|
||||
const opts = options as UTSJSONObject
|
||||
const productId = (opts.getString('productId') ?? opts.getString('id')) as string
|
||||
const priceStr = opts.getString('price')
|
||||
const productId = (opts['productId'] ?? opts['id']) as string | null
|
||||
const priceStr = opts['price'] as string | null
|
||||
const productPrice = priceStr != null ? parseFloat(priceStr) : null
|
||||
const originalPriceStr = opts.getString('originalPrice')
|
||||
const originalPriceStr = opts['originalPrice'] as string | null
|
||||
const productOriginalPrice = originalPriceStr != null ? parseFloat(originalPriceStr) : null
|
||||
|
||||
let productName = opts.getString('name') as string | null
|
||||
let productName = opts['name'] as string | null
|
||||
if (productName != null) {
|
||||
try {
|
||||
const decodedName = decodeURIComponent(productName)
|
||||
@@ -299,7 +299,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
let productImage = opts.getString('image') as string | null
|
||||
let productImage = opts['image'] as string | null
|
||||
if (productImage != null) {
|
||||
try {
|
||||
const decodedImage = decodeURIComponent(productImage)
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
type ReviewItem = {
|
||||
@@ -222,18 +223,19 @@ const loadStats = async (): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
stats.value = {
|
||||
const statsData: StatsType = {
|
||||
total_count: result.getNumber('total_count') ?? 0,
|
||||
avg_rating: result.getNumber('avg_rating') ?? 0,
|
||||
good_rate: result.getNumber('good_rate') ?? 0,
|
||||
rating_distribution: distMap
|
||||
}
|
||||
stats.value = statsData
|
||||
} catch (e) {
|
||||
console.error('加载统计失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadReviews = async (pageNum: number = 1): Promise<void> => {
|
||||
const loadReviews = async (pageNum: number): Promise<void> => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
@@ -371,13 +373,11 @@ const formatTime = (timeStr: string | null): string => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = (currentPage as any).options
|
||||
if (options != null && options.product_id != null) {
|
||||
productId.value = options.product_id as string
|
||||
onLoad((options) => {
|
||||
if (options != null) {
|
||||
const idVal = options['product_id']
|
||||
if (idVal != null) {
|
||||
productId.value = idVal as string
|
||||
loadStats()
|
||||
loadReviews(1)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="refund-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="refund-header">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="back-btn" @click="goBack">❮</text>
|
||||
<text class="header-title">退款/售后</text>
|
||||
</view>
|
||||
|
||||
@@ -543,7 +543,7 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
padding: 5px;
|
||||
margin-right: 15px;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<!-- 搜索头部 -->
|
||||
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="search-bar-container">
|
||||
<!-- 返回按钮:小于号加粗 -->
|
||||
<!-- 返回按钮:使用转义字符的直接形式 -->
|
||||
<view class="back-btn" @click="goBack">
|
||||
<text class="back-icon"><</text>
|
||||
<text class="back-icon">❮</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
@@ -935,10 +935,8 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<view class="settings-page">
|
||||
<!-- 顶部栏 -->
|
||||
<!--<view class="settings-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="back-btn" @click="goBack">❮</text>
|
||||
<text class="header-title">设置</text>
|
||||
</view>-->
|
||||
|
||||
<scroll-view class="settings-content" scroll-y>
|
||||
<scroll-view class="settings-content" direction="vertical">
|
||||
<!-- 账户设置 -->
|
||||
<view class="settings-section">
|
||||
<text class="section-title">账户设置</text>
|
||||
@@ -714,6 +714,7 @@ const deleteAccount = () => {
|
||||
.settings-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -722,6 +723,7 @@ const deleteAccount = () => {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
@@ -741,6 +743,8 @@ const deleteAccount = () => {
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
|
||||
type ShareRecordType = {
|
||||
@@ -151,7 +152,7 @@ const loadShareDetail = async (): Promise<void> => {
|
||||
recordObj = JSON.parse(JSON.stringify(recordRaw)) as UTSJSONObject
|
||||
}
|
||||
|
||||
shareRecord.value = {
|
||||
const record: ShareRecordType = {
|
||||
id: recordObj.getString('id') ?? '',
|
||||
product_name: recordObj.getString('product_name') ?? '',
|
||||
product_image: recordObj.getString('product_image'),
|
||||
@@ -164,6 +165,7 @@ const loadShareDetail = async (): Promise<void> => {
|
||||
created_at: recordObj.getString('created_at') ?? '',
|
||||
completed_at: recordObj.getString('completed_at')
|
||||
}
|
||||
shareRecord.value = record
|
||||
}
|
||||
|
||||
const purchasesRaw = result.get('secondary_purchases')
|
||||
@@ -251,13 +253,11 @@ const formatTime = (timeStr: string | null): string => {
|
||||
return `${y}-${m}-${d} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = (currentPage as any).options
|
||||
if (options != null && options.id != null) {
|
||||
shareId.value = options.id as string
|
||||
onLoad((options) => {
|
||||
if (options != null) {
|
||||
const idVal = options['id']
|
||||
if (idVal != null) {
|
||||
shareId.value = idVal as string
|
||||
loadShareDetail()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,10 @@ const loadShopProducts = async (id: string) => {
|
||||
|
||||
const rawList = res.data
|
||||
if (rawList != null && Array.isArray(rawList) && rawList.length > 0) {
|
||||
// 过滤掉已经在列表中的重复商品 (防止分页计算错误导致的重复)
|
||||
const newItems: ProductType[] = []
|
||||
const existingIds = products.value.map(p => p.id)
|
||||
|
||||
const list = rawList.map((item: any): ProductType => {
|
||||
// 解析图片数组
|
||||
let images: string[] = []
|
||||
@@ -267,11 +271,20 @@ const loadShopProducts = async (id: string) => {
|
||||
} as ProductType
|
||||
})
|
||||
|
||||
// 只有在 currentPage > 1 时才需要过滤,currentPage = 1 时直接替换
|
||||
if (currentPage.value === 1) {
|
||||
products.value = list
|
||||
} else {
|
||||
products.value.push(...list)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (existingIds.indexOf(list[i].id) === -1) {
|
||||
newItems.push(list[i])
|
||||
}
|
||||
}
|
||||
if (newItems.length > 0) {
|
||||
products.value.push(...newItems)
|
||||
}
|
||||
}
|
||||
|
||||
currentPage.value++
|
||||
hasMore.value = list.length >= pageSize.value
|
||||
} else {
|
||||
@@ -514,11 +527,15 @@ const goToProduct = (id: string) => {
|
||||
background-color: #fff;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center; /* 使 PC 端内容居中 */
|
||||
}
|
||||
|
||||
.shop-banner {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
max-width: 1200px;
|
||||
height: 200px; /* PC 端稍微加高一点 */
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
@@ -527,32 +544,35 @@ const goToProduct = (id: string) => {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
margin-top: -30px; /* Logo 向上重叠 banner */
|
||||
margin-top: -30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.shop-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 80px; /* PC 端稍微加大 */
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #fff;
|
||||
background-color: #fff;
|
||||
margin-right: 12px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.shop-basic-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 30px; /* 给 logo 上浮留空间 */
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
font-size: 18px;
|
||||
font-size: 22px; /* PC 端字体加大 */
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.shop-stats {
|
||||
@@ -561,11 +581,11 @@ const goToProduct = (id: string) => {
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 12px;
|
||||
margin-right: 15px;
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -577,14 +597,14 @@ const goToProduct = (id: string) => {
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
/* Common Button Styles */
|
||||
border-radius: 20px;
|
||||
margin-left: 10px;
|
||||
margin-left: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 16px;
|
||||
padding: 8px 24px; /* PC 端按钮加大 */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
@@ -614,39 +634,45 @@ const goToProduct = (id: string) => {
|
||||
}
|
||||
|
||||
.shop-desc {
|
||||
|
||||
color: #666;
|
||||
padding: 10px 15px 0;
|
||||
line-height: 1.4;
|
||||
padding: 15px 15px 0;
|
||||
line-height: 1.6;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Coupon Styles */
|
||||
.shop-coupons {
|
||||
margin-top: 15px;
|
||||
margin-top: 20px;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.coupon-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
flex-direction: row; /* Ensure flex direction for scroll view */
|
||||
flex-direction: row;
|
||||
}
|
||||
.coupon-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap; /* Prevent wrapping */
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
.coupon-card {
|
||||
display: flex; /* Changed from inline-flex to flex */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff5f5;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
width: 150px; /* Slight increase */
|
||||
height: 64px;
|
||||
margin-right: 15px;
|
||||
width: 180px; /* PC 端优惠券加宽 */
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0; /* Critical for horizontal scroll */
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.coupon-left {
|
||||
flex: 1;
|
||||
@@ -655,44 +681,47 @@ const goToProduct = (id: string) => {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-right: 1px dashed #ffccc7;
|
||||
padding: 0 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.coupon-amount {
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.coupon-cond {
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.coupon-right {
|
||||
width: 40px;
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #ff5000;
|
||||
flex-direction: column;
|
||||
}
|
||||
.coupon-btn-label {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 14px; /* Force vertical flow by width constraint if needed, or just let it stack naturally if char by char */
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
.coupon-btn-label {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.product-section {
|
||||
padding: 15px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #ff5000;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 10px;
|
||||
border-left: 5px solid #ff5000;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
@@ -700,7 +729,6 @@ const goToProduct = (id: string) => {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
@@ -709,27 +737,38 @@ const goToProduct = (id: string) => {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
width: calc(50% - 10px); /* 默认两列 */
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.product-item:nth-child(2n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.product-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
height: 200px;
|
||||
background: #f5f5f5;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
margin: 10px 0;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
@@ -737,20 +776,20 @@ const goToProduct = (id: string) => {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px 8px;
|
||||
padding: 0 10px 12px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 15px;
|
||||
font-size: 18px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: #ff5000;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -758,27 +797,49 @@ const goToProduct = (id: string) => {
|
||||
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* PC/Tablet Responsive */
|
||||
@media (min-width: 768px) {
|
||||
.product-item {
|
||||
width: 32% !important;
|
||||
width: calc(33.33% - 14px) !important;
|
||||
}
|
||||
.product-item:nth-child(2n) {
|
||||
margin-right: 20px !important;
|
||||
}
|
||||
.product-item:nth-child(3n) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.product-item {
|
||||
width: 16% !important; /* PC: 6 items */
|
||||
width: calc(20% - 16px) !important; /* 五列 */
|
||||
}
|
||||
.product-item:nth-child(3n) {
|
||||
margin-right: 20px !important;
|
||||
}
|
||||
.product-item:nth-child(5n) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.shop-info-card, .shop-header, .product-section {
|
||||
/* Limit max width on PC to avoid overly stretched content */
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
.shop-banner {
|
||||
height: 300px; /* 大屏加宽 Banner */
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.product-item {
|
||||
width: calc(16.66% - 17px) !important; /* 六列 */
|
||||
}
|
||||
.product-item:nth-child(5n) {
|
||||
margin-right: 20px !important;
|
||||
}
|
||||
.product-item:nth-child(6n) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="wallet-page">
|
||||
<!-- 顶部栏 -->
|
||||
<!--<view class="wallet-header">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="back-btn" @click="goBack">❮</text>
|
||||
</view>-->
|
||||
|
||||
<scroll-view class="wallet-content" scroll-y>
|
||||
|
||||
@@ -325,8 +325,10 @@ const handleLogin = async () => {
|
||||
errorMsg.includes('邮箱') && errorMsg.includes('确认')) {
|
||||
throw new Error('邮箱未确认,请先检查邮箱并点击确认链接')
|
||||
} else if (errorMsg.includes('Invalid login credentials') ||
|
||||
errorCode === 'invalid_credentials') {
|
||||
throw new Error('邮箱或密码错误')
|
||||
errorCode === 'invalid_credentials' ||
|
||||
errorMsg.includes('credentials') ||
|
||||
errorMsg.includes('invalid')) {
|
||||
throw new Error('用户名或密码错误')
|
||||
} else {
|
||||
throw new Error(errorMsg != '' ? errorMsg : '登录失败,请重试')
|
||||
}
|
||||
|
||||
@@ -183,7 +183,13 @@
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await supa.signUp(email.value.trim(), password.value)
|
||||
// 在注册时传递 user_role 元数据,以便数据库触发器识别
|
||||
const options = new UTSJSONObject()
|
||||
const metaData = new UTSJSONObject()
|
||||
metaData.set('user_role', 'consumer')
|
||||
options.set('data', metaData)
|
||||
|
||||
const result = await supa.signUp(email.value.trim(), password.value, options)
|
||||
|
||||
console.log('注册返回结果:', result)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts';
|
||||
import { SUPA_URL, IS_TEST_MODE } from '@/ak/config.uts';
|
||||
import { SUPA_URL, SUPA_KEY, IS_TEST_MODE } from '@/ak/config.uts';
|
||||
|
||||
// token 持久化 key
|
||||
const ACCESS_TOKEN_KEY = 'akreq_access_token';
|
||||
@@ -71,14 +71,17 @@ export class AkReq {
|
||||
return false;
|
||||
}
|
||||
// 构造 header,必须带 apikey
|
||||
let headers = {} as UTSJSONObject;
|
||||
let headers = new UTSJSONObject();
|
||||
if (apikey !== null && apikey !== "") {
|
||||
headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject;
|
||||
} try {
|
||||
headers.set('apikey', apikey)
|
||||
}
|
||||
const reqData = new UTSJSONObject()
|
||||
reqData.set('refresh_token', refreshToken)
|
||||
try {
|
||||
const res = await this.request({
|
||||
url: SUPA_URL + '/auth/v1/token?grant_type=refresh_token',
|
||||
method: 'POST',
|
||||
data: ({ refresh_token: refreshToken } as UTSJSONObject),
|
||||
data: reqData,
|
||||
headers: headers,
|
||||
contentType: 'application/json'
|
||||
}, true); // skipRefresh=true,避免递归
|
||||
@@ -123,9 +126,9 @@ export class AkReq {
|
||||
const originalHeaders = options.headers
|
||||
if (typeof originalHeaders.getString === 'function') {
|
||||
// 复制 apikey
|
||||
const apikey = originalHeaders.getString('apikey')
|
||||
if (apikey != null) {
|
||||
newHeaders.set('apikey', apikey)
|
||||
const apikeyStr = originalHeaders.getString('apikey')
|
||||
if (apikeyStr != null) {
|
||||
newHeaders.set('apikey', apikeyStr)
|
||||
}
|
||||
// 复制 Content-Type
|
||||
const contentType = originalHeaders.getString('Content-Type')
|
||||
@@ -144,6 +147,13 @@ export class AkReq {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 补齐 apikey (如果 headers 中没有,则直接使用 SUPA_KEY 补全)
|
||||
if (newHeaders.getString('apikey') == null) {
|
||||
if (SUPA_KEY != null && SUPA_KEY != "") {
|
||||
newHeaders.set('apikey', SUPA_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/更新 Authorization
|
||||
const token = this.getToken();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773115566370.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773115566370.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116010340.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116010340.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116224367.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116224367.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116675132.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773116675132.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773125237819.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773125237819.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773129490890.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773129490890.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773129910152.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773129910152.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user