feat(list): 订单/通知/组局列表分页,生产 NODE_ENV 切换 production
- 仓储层 getOrders/getOrdersForUser/getActivities/getUserNotifications 支持 LIMIT/OFFSET,新增 countOrders/countOrdersForUser/countActivities/getUserNotificationTotal - 接口支持 page/pageSize/status/keyword/payType/creatorId/category,返回 list/total/hasMore;未传 page 保持旧数组格式,兼容后台与工作台 - 小程序订单/通知/组局三页触底加载,筛选与搜索下推服务端,避免只过滤当前页 - 新增 116 套件/758 用例,全绿;线上实测分页与筛选接口正常
这个提交包含在:
@@ -51,16 +51,36 @@ export function formatActivityData(raw) {
|
||||
/**
|
||||
* 获取组局活动列表 (真实请求 MySQL,异常时返回空列表)
|
||||
*/
|
||||
export async function getActivities() {
|
||||
export async function getActivities(options = {}) {
|
||||
const paged = Number(options.page) > 0;
|
||||
try {
|
||||
const list = await http.get('/activities');
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return list.map(formatActivityData);
|
||||
const qs = [];
|
||||
if (paged) {
|
||||
qs.push(`page=${Math.floor(Number(options.page))}`);
|
||||
qs.push(`pageSize=${Math.floor(Number(options.pageSize) || 20)}`);
|
||||
if (options.keyword) qs.push(`keyword=${encodeURIComponent(String(options.keyword).trim())}`);
|
||||
if (options.payType) qs.push(`payType=${encodeURIComponent(options.payType)}`);
|
||||
if (Number(options.creatorId) > 0) qs.push(`creatorId=${Math.floor(Number(options.creatorId))}`);
|
||||
if (options.category) qs.push(`category=${encodeURIComponent(options.category)}`);
|
||||
if (options.categoryName) qs.push(`categoryName=${encodeURIComponent(options.categoryName)}`);
|
||||
}
|
||||
const res = await http.get(`/activities${qs.length ? `?${qs.join('&')}` : ''}`);
|
||||
if (Array.isArray(res)) {
|
||||
return res.map(formatActivityData);
|
||||
}
|
||||
if (res && Array.isArray(res.list)) {
|
||||
return {
|
||||
list: res.list.map(formatActivityData),
|
||||
total: Number(res.total || 0),
|
||||
page: Number(res.page || 1),
|
||||
pageSize: Number(res.pageSize || 20),
|
||||
hasMore: Boolean(res.hasMore)
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取后端活动列表异常:', err.message);
|
||||
}
|
||||
return [];
|
||||
return paged ? { list: [], total: 0, page: 1, pageSize: 20, hasMore: false } : [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,16 +322,33 @@ export function formatOrderData(raw) {
|
||||
/**
|
||||
* 获取订单列表 (真实读取 MySQL)
|
||||
*/
|
||||
export async function getOrders() {
|
||||
export async function getOrders(options = {}) {
|
||||
const paged = Number(options.page) > 0;
|
||||
try {
|
||||
const list = await http.get(`/orders?userId=${normalizeUserId()}`);
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return list.map(formatOrderData);
|
||||
const qs = [];
|
||||
if (options.status) qs.push(`status=${encodeURIComponent(options.status)}`);
|
||||
if (paged) {
|
||||
qs.push(`page=${Math.floor(Number(options.page))}`);
|
||||
qs.push(`pageSize=${Math.floor(Number(options.pageSize) || 20)}`);
|
||||
}
|
||||
const suffix = qs.length ? `&${qs.join('&')}` : '';
|
||||
const res = await http.get(`/orders?userId=${normalizeUserId()}${suffix}`);
|
||||
if (Array.isArray(res)) {
|
||||
return res.map(formatOrderData);
|
||||
}
|
||||
if (res && Array.isArray(res.list)) {
|
||||
return {
|
||||
list: res.list.map(formatOrderData),
|
||||
total: Number(res.total || 0),
|
||||
page: Number(res.page || 1),
|
||||
pageSize: Number(res.pageSize || 20),
|
||||
hasMore: Boolean(res.hasMore)
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取订单列表异常:', err.message);
|
||||
}
|
||||
return [];
|
||||
return paged ? { list: [], total: 0, page: 1, pageSize: 20, hasMore: false } : [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1368,10 +1405,17 @@ export async function getSubscribeTemplates() {
|
||||
/**
|
||||
* 获取站内消息通知列表与未读数
|
||||
*/
|
||||
export async function getNotifications(userId, limit = 50) {
|
||||
export async function getNotifications(userId, limit = 50, extra = {}) {
|
||||
const uid = normalizeUserId(userId);
|
||||
try {
|
||||
const res = await http.get(`/notifications?userId=${uid}&limit=${Number(limit) || 50}`);
|
||||
const qs = [`userId=${uid}`];
|
||||
if (Number(extra.page) > 0) {
|
||||
qs.push(`page=${Math.floor(Number(extra.page))}`);
|
||||
qs.push(`pageSize=${Math.floor(Number(extra.pageSize) || 20)}`);
|
||||
} else {
|
||||
qs.push(`limit=${Number(limit) || 50}`);
|
||||
}
|
||||
const res = await http.get(`/notifications?${qs.join('&')}`);
|
||||
return res || { list: [], unread: 0 };
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
@@ -163,6 +163,13 @@
|
||||
清空搜索条件
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoadingMore" class="load-more-tip">
|
||||
<text>正在加载更多…</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && filteredActivities.length > 0" class="load-more-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -172,8 +179,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app';
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow, onLoad, onReachBottom } from '@dcloudio/uni-app';
|
||||
import AppTabBar from '@/components/CustomTabBar.vue';
|
||||
import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import { getActivities, getNavCategories, normalizeUserId } from '@/common/api.js';
|
||||
@@ -184,6 +191,7 @@ import { getNavBarInfo } from '@/common/nav.js';
|
||||
const CURRENT_USER_ID = normalizeUserId();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const isLoadingMore = ref(false);
|
||||
const activities = ref([]);
|
||||
const currentTab = ref('ALL');
|
||||
const currentCategory = ref('ALL');
|
||||
@@ -191,6 +199,11 @@ const dynamicCategories = ref([]);
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const searchKeyword = ref('');
|
||||
const navInfo = ref(getNavBarInfo());
|
||||
const page = ref(1);
|
||||
const pageSize = 20;
|
||||
const hasMore = ref(false);
|
||||
let searchTimer = null;
|
||||
let requestSeq = 0;
|
||||
|
||||
const navStyle = computed(() => {
|
||||
return {
|
||||
@@ -206,7 +219,7 @@ const clearSearch = () => {
|
||||
|
||||
const loadInitialActivities = async () => {
|
||||
try {
|
||||
await fetchLiveActivities();
|
||||
await fetchLiveActivities(true);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
@@ -214,14 +227,43 @@ const loadInitialActivities = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLiveActivities = async () => {
|
||||
const fetchLiveActivities = async (reset = true) => {
|
||||
if (!reset && isLoadingMore.value) return;
|
||||
const seq = ++requestSeq;
|
||||
const targetPage = reset ? 1 : page.value + 1;
|
||||
isLoadingMore.value = true;
|
||||
try {
|
||||
const list = await getActivities();
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
const categoryMeta = navCapsules.value.find((c) => c.code === currentCategory.value);
|
||||
const res = await getActivities({
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
keyword: searchKeyword.value.trim(),
|
||||
payType: currentTab.value === 'AA' || currentTab.value === 'PROJECT' ? currentTab.value : '',
|
||||
creatorId: currentTab.value === 'MINE' ? CURRENT_USER_ID : 0,
|
||||
category: currentCategory.value === 'ALL' ? '' : currentCategory.value,
|
||||
categoryName: categoryMeta ? categoryMeta.name : ''
|
||||
});
|
||||
if (seq !== requestSeq) return;
|
||||
const list = Array.isArray(res) ? res : (res && Array.isArray(res.list) ? res.list : []);
|
||||
if (reset) {
|
||||
activities.value = list;
|
||||
hasMore.value = Array.isArray(res) ? false : Boolean(res && res.hasMore);
|
||||
} else {
|
||||
const seen = new Set(activities.value.map((a) => a.id));
|
||||
activities.value = activities.value.concat(list.filter((a) => !seen.has(a.id)));
|
||||
hasMore.value = Boolean(res && res.hasMore);
|
||||
}
|
||||
page.value = targetPage;
|
||||
} catch (err) {
|
||||
console.warn('[ActivityList] 获取线上数据异常,保持缓存:', err);
|
||||
if (seq === requestSeq && reset) {
|
||||
activities.value = [];
|
||||
hasMore.value = false;
|
||||
}
|
||||
} finally {
|
||||
if (seq === requestSeq) {
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -301,6 +343,10 @@ onShow(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('theme-change', onThemeChanged);
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
const getCategoryTagClass = (game) => {
|
||||
@@ -318,61 +364,32 @@ const getStatusText = (status) => {
|
||||
return '招募中';
|
||||
};
|
||||
|
||||
const filteredActivities = computed(() => {
|
||||
const kw = (searchKeyword.value || '').trim().toLowerCase();
|
||||
|
||||
return activities.value.filter(a => {
|
||||
// 1. 关键词搜索筛选 (标题、商圈地点、品类、局长昵称)
|
||||
let matchKeyword = true;
|
||||
if (kw) {
|
||||
const titleMatch = (a.title || '').toLowerCase().includes(kw);
|
||||
const locationMatch = (a.location || '').toLowerCase().includes(kw);
|
||||
const gameMatch = (a.game || '').toLowerCase().includes(kw);
|
||||
const creatorMatch = (a.creatorNickname || '').toLowerCase().includes(kw);
|
||||
matchKeyword = titleMatch || locationMatch || gameMatch || creatorMatch;
|
||||
}
|
||||
|
||||
// 2. 支付模式或发单人筛选
|
||||
let matchTab = true;
|
||||
if (currentTab.value === 'MINE') {
|
||||
matchTab = Number(a.creatorId) === Number(CURRENT_USER_ID);
|
||||
} else if (currentTab.value !== 'ALL') {
|
||||
matchTab = a.payType === currentTab.value;
|
||||
}
|
||||
|
||||
// 3. 品类筛选 (支持动态匹配后台下发品类)
|
||||
let matchCat = true;
|
||||
if (currentCategory.value && currentCategory.value !== 'ALL') {
|
||||
const targetCatCode = currentCategory.value.toUpperCase();
|
||||
if (targetCatCode === 'MEAL') {
|
||||
matchCat = a.game === '美食陪吃' || (a.gameCategory || '').includes('MEAL');
|
||||
} else if (targetCatCode === 'SING') {
|
||||
matchCat = a.game === '同城陪唱' || (a.gameCategory || '').includes('SING');
|
||||
} else if (targetCatCode === 'DRINK' || targetCatCode === 'BAR') {
|
||||
matchCat = a.game === '下午茶陪喝' || a.game === '同城微醺' || (a.gameCategory || '').includes('DRINK');
|
||||
} else if (targetCatCode === 'ESPORTS') {
|
||||
matchCat = a.game.includes('电竞') || a.game.includes('游戏') || (a.gameCategory || '').includes('ESPORTS');
|
||||
} else {
|
||||
const found = navCapsules.value.find((c) => c.code === targetCatCode);
|
||||
if (found) {
|
||||
matchCat = a.game.includes(found.name) || found.name.includes(a.game);
|
||||
} else {
|
||||
matchCat = a.game.includes(currentCategory.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchKeyword && matchTab && matchCat;
|
||||
});
|
||||
});
|
||||
// 筛选与搜索已交由服务端分页完成,这里直接展示当前页数据
|
||||
const filteredActivities = computed(() => activities.value);
|
||||
|
||||
const switchTab = (tab) => {
|
||||
if (currentTab.value === tab) return;
|
||||
currentTab.value = tab;
|
||||
fetchLiveActivities(true);
|
||||
};
|
||||
|
||||
const switchCategory = (cat) => {
|
||||
if (currentCategory.value === cat) return;
|
||||
currentCategory.value = cat;
|
||||
fetchLiveActivities(true);
|
||||
};
|
||||
|
||||
onReachBottom(() => {
|
||||
if (hasMore.value && !isLoadingMore.value) {
|
||||
fetchLiveActivities(false);
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchKeyword, () => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => fetchLiveActivities(true), 350);
|
||||
});
|
||||
|
||||
const goToDetail = (id) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/detail?id=${id}`
|
||||
@@ -592,6 +609,13 @@ const createActivity = () => {
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
|
||||
.load-more-tip {
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
@@ -106,11 +106,22 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="filteredOrders.length === 0" class="empty-state">
|
||||
<view v-if="isLoading" class="load-more-tip">
|
||||
<text>加载中…</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="filteredOrders.length === 0" class="empty-state">
|
||||
<image class="empty-svg" :src="ICONS.orderList" mode="aspectFit" />
|
||||
<text class="empty-text">暂无此类订单</text>
|
||||
<button class="empty-btn btn-active" @click="goToExplore">前往发现大厅寻找大神</button>
|
||||
</view>
|
||||
|
||||
<view v-else-if="isLoadingMore" class="load-more-tip">
|
||||
<text>正在加载更多…</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore" class="load-more-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -350,7 +361,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { onShow, onReachBottom } from '@dcloudio/uni-app';
|
||||
import {
|
||||
getOrders,
|
||||
cancelOrder as cancelOrderApi,
|
||||
@@ -411,18 +422,56 @@ const onThemeChanged = (key) => {
|
||||
};
|
||||
|
||||
const orders = ref([]);
|
||||
const page = ref(1);
|
||||
const pageSize = 20;
|
||||
const hasMore = ref(false);
|
||||
const isLoading = ref(true);
|
||||
const isLoadingMore = ref(false);
|
||||
let requestSeq = 0;
|
||||
|
||||
const loadLiveOrders = async () => {
|
||||
const loadLiveOrders = async (reset = true) => {
|
||||
if (!reset && isLoadingMore.value) return;
|
||||
const seq = ++requestSeq;
|
||||
const targetPage = reset ? 1 : page.value + 1;
|
||||
isLoadingMore.value = true;
|
||||
if (reset) isLoading.value = true;
|
||||
try {
|
||||
const list = await getOrders();
|
||||
if (Array.isArray(list)) {
|
||||
const res = await getOrders({
|
||||
page: targetPage,
|
||||
pageSize,
|
||||
status: currentTab.value === 'ALL' ? '' : currentTab.value
|
||||
});
|
||||
if (seq !== requestSeq) return;
|
||||
const list = Array.isArray(res) ? res : (res && Array.isArray(res.list) ? res.list : []);
|
||||
if (reset) {
|
||||
orders.value = list;
|
||||
hasMore.value = Array.isArray(res) ? false : Boolean(res && res.hasMore);
|
||||
} else {
|
||||
const seen = new Set(orders.value.map(o => o.id));
|
||||
orders.value = orders.value.concat(list.filter(o => !seen.has(o.id)));
|
||||
hasMore.value = Boolean(res && res.hasMore);
|
||||
}
|
||||
page.value = targetPage;
|
||||
} catch (err) {
|
||||
console.warn('[OrderList] 获取订单列表失败:', err);
|
||||
if (seq === requestSeq && reset) {
|
||||
orders.value = [];
|
||||
hasMore.value = false;
|
||||
}
|
||||
} finally {
|
||||
if (seq === requestSeq) {
|
||||
isLoading.value = false;
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onReachBottom(() => {
|
||||
if (hasMore.value && !isLoadingMore.value) {
|
||||
loadLiveOrders(false);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
navInfo.value = getNavBarInfo();
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
@@ -463,13 +512,14 @@ const tabs = [
|
||||
|
||||
const currentTab = ref('ALL');
|
||||
|
||||
const filteredOrders = computed(() => {
|
||||
if (currentTab.value === 'ALL') return orders.value;
|
||||
return orders.value.filter(o => o.status === currentTab.value);
|
||||
});
|
||||
// 状态筛选已由服务端分页完成,这里直接展示当前页数据
|
||||
const filteredOrders = computed(() => orders.value);
|
||||
|
||||
const switchTab = (tabKey) => {
|
||||
if (currentTab.value === tabKey) return;
|
||||
currentTab.value = tabKey;
|
||||
orders.value = [];
|
||||
loadLiveOrders(true);
|
||||
};
|
||||
|
||||
const goToExplore = () => {
|
||||
@@ -963,6 +1013,13 @@ const submitReview = async () => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.load-more-tip {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
background-color: #151D30;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view class="notif-body">
|
||||
<view class="notif-summary">
|
||||
@@ -56,6 +57,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoadingMore" class="load-more-tip">
|
||||
<text>正在加载更多…</text>
|
||||
</view>
|
||||
<view v-else-if="!hasMore && list.length > 0" class="load-more-tip">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -73,8 +81,13 @@ const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const navInfo = ref(getNavBarInfo());
|
||||
const isLoading = ref(true);
|
||||
const isRefreshing = ref(false);
|
||||
const isLoadingMore = ref(false);
|
||||
const list = ref([]);
|
||||
const unreadCount = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = 20;
|
||||
const hasMore = ref(false);
|
||||
let requestSeq = 0;
|
||||
|
||||
const navStyle = computed(() => ({
|
||||
'--safe-header-gap': `${navInfo.value.totalHeaderHeight}px`,
|
||||
@@ -90,28 +103,55 @@ const formatTime = (v) => {
|
||||
return s.length >= 16 ? s.slice(0, 16) : s;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
const loadData = async (reset = true) => {
|
||||
if (!reset && isLoadingMore.value) return;
|
||||
const uid = currentUserId();
|
||||
if (!uid) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
const seq = ++requestSeq;
|
||||
const targetPage = reset ? 1 : page.value + 1;
|
||||
isLoadingMore.value = true;
|
||||
if (reset) isLoading.value = true;
|
||||
try {
|
||||
const res = await getNotifications(uid, 50);
|
||||
list.value = Array.isArray(res?.list) ? res.list : [];
|
||||
const res = await getNotifications(uid, 50, { page: targetPage, pageSize });
|
||||
if (seq !== requestSeq) return;
|
||||
const incoming = Array.isArray(res?.list) ? res.list : [];
|
||||
if (reset) {
|
||||
list.value = incoming;
|
||||
hasMore.value = Boolean(res?.hasMore);
|
||||
} else {
|
||||
const seen = new Set(list.value.map((x) => x.id));
|
||||
list.value = list.value.concat(incoming.filter((x) => !seen.has(x.id)));
|
||||
hasMore.value = Boolean(res?.hasMore);
|
||||
}
|
||||
unreadCount.value = Number(res?.unread || 0);
|
||||
page.value = targetPage;
|
||||
} catch (_) {
|
||||
list.value = [];
|
||||
unreadCount.value = 0;
|
||||
if (seq === requestSeq && reset) {
|
||||
list.value = [];
|
||||
unreadCount.value = 0;
|
||||
hasMore.value = false;
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isRefreshing.value = false;
|
||||
if (seq === requestSeq) {
|
||||
isLoading.value = false;
|
||||
isLoadingMore.value = false;
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoadingMore.value) {
|
||||
loadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
isRefreshing.value = true;
|
||||
loadData();
|
||||
loadData(true);
|
||||
};
|
||||
|
||||
const handleReadAll = async () => {
|
||||
@@ -286,6 +326,12 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.load-more-tip {
|
||||
padding: 18px 0 8px;
|
||||
text-align: center;
|
||||
color: var(--text-muted, #6d7997);
|
||||
font-size: 12px;
|
||||
}
|
||||
.notif-card {
|
||||
position: relative;
|
||||
padding: 14px 14px 14px 18px;
|
||||
|
||||
@@ -55,8 +55,88 @@ export interface ActivityRow {
|
||||
}
|
||||
|
||||
export class MysqlRepository {
|
||||
/** 分页参数归一化:page 从 1 开始,pageSize 默认 20(最大 100) */
|
||||
private normalizePage(page: any, pageSize: any, defaultSize = 20): { page: number; pageSize: number; offset: number } {
|
||||
const p = Math.max(1, Math.floor(Number(page) || 1));
|
||||
const s = Math.min(100, Math.max(1, Math.floor(Number(pageSize) || defaultSize)));
|
||||
return { page: p, pageSize: s, offset: (p - 1) * s };
|
||||
}
|
||||
|
||||
/** 活动列表筛选条件(列表与计数共用,保证 total 与 list 口径一致) */
|
||||
private buildActivityFilters(opts: {
|
||||
keyword?: string;
|
||||
payType?: string;
|
||||
creatorId?: number;
|
||||
category?: string;
|
||||
categoryName?: string;
|
||||
} = {}): { where: string; params: any[] } {
|
||||
const conds: string[] = ["(u.status IS NULL OR u.status != '已封禁')"];
|
||||
const params: any[] = [];
|
||||
|
||||
const keyword = String(opts.keyword || '').trim();
|
||||
if (keyword) {
|
||||
const like = `%${keyword}%`;
|
||||
conds.push('(a.title LIKE ? OR a.location LIKE ? OR a.game_category LIKE ? OR u.nickname LIKE ?)');
|
||||
params.push(like, like, like, like);
|
||||
}
|
||||
|
||||
const payType = String(opts.payType || '').toUpperCase();
|
||||
if (payType === 'AA') {
|
||||
conds.push('a.pay_type = ?');
|
||||
params.push(1);
|
||||
} else if (payType === 'PROJECT') {
|
||||
conds.push('a.pay_type = ?');
|
||||
params.push(2);
|
||||
}
|
||||
|
||||
const creatorId = Number(opts.creatorId) || 0;
|
||||
if (creatorId > 0) {
|
||||
conds.push('a.creator_id = ?');
|
||||
params.push(creatorId);
|
||||
}
|
||||
|
||||
const category = String(opts.category || '').toUpperCase();
|
||||
if (category && category !== 'ALL') {
|
||||
if (category === 'MEAL') {
|
||||
conds.push('(a.game_category LIKE ? OR a.title LIKE ? OR a.game_category LIKE ?)');
|
||||
params.push('%美食%', '%美食%', '%MEAL%');
|
||||
} else if (category === 'SING') {
|
||||
conds.push('(a.game_category LIKE ? OR a.title LIKE ? OR a.game_category LIKE ?)');
|
||||
params.push('%陪唱%', '%陪唱%', '%SING%');
|
||||
} else if (category === 'DRINK' || category === 'BAR') {
|
||||
conds.push('(a.game_category LIKE ? OR a.game_category LIKE ? OR a.title LIKE ? OR a.game_category LIKE ? OR a.game_category LIKE ?)');
|
||||
params.push('%下午茶%', '%微醺%', '%下午茶%', '%DRINK%', '%BAR%');
|
||||
} else if (category === 'ESPORTS') {
|
||||
conds.push('(a.game_category LIKE ? OR a.game_category LIKE ? OR a.title LIKE ? OR a.game_category LIKE ?)');
|
||||
params.push('%电竞%', '%游戏%', '%电竞%', '%ESPORTS%');
|
||||
} else {
|
||||
const name = String(opts.categoryName || '').trim() || category;
|
||||
const like = `%${name}%`;
|
||||
conds.push('(a.game_category LIKE ? OR a.title LIKE ?)');
|
||||
params.push(like, like);
|
||||
}
|
||||
}
|
||||
|
||||
return { where: conds.join(' AND '), params };
|
||||
}
|
||||
|
||||
// 活动相关
|
||||
async getActivities(): Promise<ActivityRow[]> {
|
||||
async getActivities(opts: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
payType?: string;
|
||||
creatorId?: number;
|
||||
category?: string;
|
||||
categoryName?: string;
|
||||
} = {}): Promise<ActivityRow[]> {
|
||||
const { where, params } = this.buildActivityFilters(opts);
|
||||
const paged = Number(opts.page) > 0;
|
||||
let limitSql = '';
|
||||
if (paged) {
|
||||
const { pageSize, offset } = this.normalizePage(opts.page, opts.pageSize);
|
||||
limitSql = ` LIMIT ${pageSize} OFFSET ${offset}`;
|
||||
}
|
||||
const [actRows]: any = await pool.query(`
|
||||
SELECT
|
||||
a.id, a.creator_id as creatorId, a.title, a.game_category as gameCategory,
|
||||
@@ -69,9 +149,9 @@ export class MysqlRepository {
|
||||
u.avatar as creatorAvatar
|
||||
FROM activities a
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE (u.status IS NULL OR u.status != '已封禁')
|
||||
ORDER BY a.created_at DESC
|
||||
`);
|
||||
WHERE ${where}
|
||||
ORDER BY a.created_at DESC${limitSql}
|
||||
`, params);
|
||||
|
||||
const [ptRows]: any = await pool.query(`
|
||||
SELECT
|
||||
@@ -103,6 +183,23 @@ export class MysqlRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async countActivities(opts: {
|
||||
keyword?: string;
|
||||
payType?: string;
|
||||
creatorId?: number;
|
||||
category?: string;
|
||||
categoryName?: string;
|
||||
} = {}): Promise<number> {
|
||||
const { where, params } = this.buildActivityFilters(opts);
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT COUNT(*) as c
|
||||
FROM activities a
|
||||
LEFT JOIN users u ON a.creator_id = u.id
|
||||
WHERE ${where}
|
||||
`, params);
|
||||
return Number((rows && rows[0] && rows[0].c) || 0);
|
||||
}
|
||||
|
||||
async getActivityById(id: string): Promise<any | null> {
|
||||
const [actRows]: any = await pool.query(`
|
||||
SELECT
|
||||
@@ -1320,8 +1417,34 @@ export class MysqlRepository {
|
||||
}
|
||||
|
||||
// 订单相关
|
||||
async getOrders(): Promise<any[]> {
|
||||
/** 订单状态分组(列表 tab 与计数共用;状态值为库内中文枚举) */
|
||||
private orderStatusFilter(group?: string): string {
|
||||
switch (String(group || '').toUpperCase()) {
|
||||
case 'PENDING_PAY':
|
||||
return "status = '待付款'";
|
||||
case 'PENDING_ACCEPT':
|
||||
return "status = '待接单'";
|
||||
case 'IN_SERVICE':
|
||||
return "status IN ('已接单','履约中','服务中','已到场核销','待确认')";
|
||||
case 'COMPLETED':
|
||||
return "status = '已完成'";
|
||||
case 'REFUNDING':
|
||||
return "status IN ('争议仲裁中','退款中','退款审核中','已退款','已全额退款','已部分退款','售后处理中')";
|
||||
case 'CANCELLED':
|
||||
return "status = '已取消'";
|
||||
default:
|
||||
return '1=1';
|
||||
}
|
||||
}
|
||||
|
||||
async getOrders(opts: { page?: number; pageSize?: number; status?: string } = {}): Promise<any[]> {
|
||||
// 订单超时自动流转统一由后台定时任务处理(见 index.ts startOrderTimeoutScheduler)
|
||||
const statusWhere = this.orderStatusFilter(opts.status);
|
||||
let limitSql = 'LIMIT 500';
|
||||
if (Number(opts.page) > 0) {
|
||||
const { pageSize, offset } = this.normalizePage(opts.page, opts.pageSize);
|
||||
limitSql = `LIMIT ${pageSize} OFFSET ${offset}`;
|
||||
}
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
id,
|
||||
@@ -1348,18 +1471,34 @@ export class MysqlRepository {
|
||||
finish_applied_at as finishAppliedAt,
|
||||
created_at as createdAt
|
||||
FROM orders
|
||||
WHERE ${statusWhere}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 500
|
||||
${limitSql}
|
||||
`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async countOrders(opts: { status?: string } = {}): Promise<number> {
|
||||
const statusWhere = this.orderStatusFilter(opts.status);
|
||||
const [rows]: any = await pool.query(`SELECT COUNT(*) as c FROM orders WHERE ${statusWhere}`);
|
||||
return Number((rows && rows[0] && rows[0].c) || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回与指定用户相关的订单(买家 / 承接大神 / 已认领 / 其大神档案对应订单)。
|
||||
* 用于替代把全平台订单下发给任意客户端,避免订单隐私与核销码泄露。
|
||||
*/
|
||||
async getOrdersForUser(userId: number): Promise<any[]> {
|
||||
async getOrdersForUser(
|
||||
userId: number,
|
||||
opts: { page?: number; pageSize?: number; status?: string } = {}
|
||||
): Promise<any[]> {
|
||||
const uid = Number(userId) || 0;
|
||||
const statusWhere = this.orderStatusFilter(opts.status);
|
||||
let limitSql = 'LIMIT 500';
|
||||
if (Number(opts.page) > 0) {
|
||||
const { pageSize, offset } = this.normalizePage(opts.page, opts.pageSize);
|
||||
limitSql = `LIMIT ${pageSize} OFFSET ${offset}`;
|
||||
}
|
||||
// 订单超时自动流转统一由后台定时任务处理(见 index.ts startOrderTimeoutScheduler)
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT
|
||||
@@ -1387,16 +1526,37 @@ export class MysqlRepository {
|
||||
finish_applied_at as finishAppliedAt,
|
||||
created_at as createdAt
|
||||
FROM orders
|
||||
WHERE buyer_id = ?
|
||||
OR expert_id = ?
|
||||
OR claimed_by = ?
|
||||
OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)
|
||||
WHERE (
|
||||
buyer_id = ?
|
||||
OR expert_id = ?
|
||||
OR claimed_by = ?
|
||||
OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)
|
||||
)
|
||||
AND ${statusWhere}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 500
|
||||
${limitSql}
|
||||
`, [uid, uid, uid, uid, uid]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async countOrdersForUser(userId: number, opts: { status?: string } = {}): Promise<number> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return 0;
|
||||
const statusWhere = this.orderStatusFilter(opts.status);
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT COUNT(*) as c
|
||||
FROM orders
|
||||
WHERE (
|
||||
buyer_id = ?
|
||||
OR expert_id = ?
|
||||
OR claimed_by = ?
|
||||
OR companion_name IN (SELECT name FROM companions WHERE user_id = ? OR id = ?)
|
||||
)
|
||||
AND ${statusWhere}
|
||||
`, [uid, uid, uid, uid, uid]);
|
||||
return Number((rows && rows[0] && rows[0].c) || 0);
|
||||
}
|
||||
|
||||
async createOrder(data: {
|
||||
buyerId?: number;
|
||||
clientName?: string;
|
||||
@@ -3444,21 +3604,32 @@ export class MysqlRepository {
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getUserNotifications(userId: number, limit = 50): Promise<any[]> {
|
||||
async getUserNotifications(userId: number, limit = 50, offset = 0): Promise<any[]> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return [];
|
||||
const take = Math.min(200, Math.max(1, Number(limit) || 50));
|
||||
const skip = Math.max(0, Math.floor(Number(offset) || 0));
|
||||
const [rows]: any = await pool.query(
|
||||
`SELECT id, type, title, content, link, is_read as isRead, created_at as createdAt
|
||||
FROM user_notifications
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${take}`,
|
||||
LIMIT ${take} OFFSET ${skip}`,
|
||||
[uid]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async getUserNotificationTotal(userId: number): Promise<number> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return 0;
|
||||
const [rows]: any = await pool.query(
|
||||
'SELECT COUNT(*) as c FROM user_notifications WHERE user_id = ?',
|
||||
[uid]
|
||||
);
|
||||
return Number((rows && rows[0] && rows[0].c) || 0);
|
||||
}
|
||||
|
||||
async getUserUnreadNotificationCount(userId: number): Promise<number> {
|
||||
const uid = Number(userId) || 0;
|
||||
if (uid <= 0) return 0;
|
||||
|
||||
@@ -898,8 +898,25 @@ app.get('/api/notifications', async (req: Request, res: Response) => {
|
||||
Number(req.query.userId || req.headers['x-user-id'] || 0),
|
||||
'查看消息通知'
|
||||
);
|
||||
const list = await repo.getUserNotifications(uid, Number(req.query.limit) || 50);
|
||||
const unread = await repo.getUserUnreadNotificationCount(uid);
|
||||
if (Number(req.query.page) > 0) {
|
||||
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
||||
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(req.query.pageSize) || 20)));
|
||||
const total = await repo.getUserNotificationTotal(uid);
|
||||
const list = await repo.getUserNotifications(uid, pageSize, (page - 1) * pageSize);
|
||||
return res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
list,
|
||||
unread,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasMore: (page - 1) * pageSize + list.length < total,
|
||||
},
|
||||
});
|
||||
}
|
||||
const list = await repo.getUserNotifications(uid, Number(req.query.limit) || 50);
|
||||
res.json({ code: 20000, data: { list, unread } });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
@@ -952,12 +969,33 @@ app.get('/api/dashboard/todos', async (_req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// 3. 活动与4位核销码相关接口 (MySQL 持久化)
|
||||
app.get('/api/activities', async (_req: Request, res: Response) => {
|
||||
app.get('/api/activities', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const acts = await repo.getActivities();
|
||||
const filters = {
|
||||
keyword: String(req.query.keyword || '').trim(),
|
||||
payType: String(req.query.payType || '').trim(),
|
||||
creatorId: Number(req.query.creatorId) || 0,
|
||||
category: String(req.query.category || '').trim(),
|
||||
categoryName: String(req.query.categoryName || '').trim(),
|
||||
};
|
||||
const paged = Number(req.query.page) > 0;
|
||||
if (!paged) {
|
||||
const acts = await repo.getActivities(filters);
|
||||
return res.json({ code: 20000, data: acts });
|
||||
}
|
||||
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
||||
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(req.query.pageSize) || 20)));
|
||||
const acts = await repo.getActivities({ ...filters, page, pageSize });
|
||||
const total = await repo.countActivities(filters);
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: acts
|
||||
data: {
|
||||
list: acts,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasMore: (page - 1) * pageSize + acts.length < total,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
@@ -1458,12 +1496,31 @@ app.put('/api/companions/:id/profile', async (req: Request, res: Response) => {
|
||||
// 5. 订单与仲裁接口 (落盘 MySQL)
|
||||
app.get('/api/orders', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const status = String(req.query.status || '').trim();
|
||||
const paged = Number(req.query.page) > 0;
|
||||
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
||||
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(req.query.pageSize) || 20)));
|
||||
const queryOpts = paged ? { page, pageSize, status } : { status };
|
||||
|
||||
// 管理端(服务端签名 admin token)可取全量;普通用户仅能取与自己相关的订单
|
||||
if (isVerifiedAdminRequest(req)) {
|
||||
const orders = await repo.getOrders();
|
||||
const orders = await repo.getOrders(queryOpts);
|
||||
if (!paged) {
|
||||
return res.json({
|
||||
code: 20000,
|
||||
data: orders
|
||||
});
|
||||
}
|
||||
const total = await repo.countOrders({ status });
|
||||
return res.json({
|
||||
code: 20000,
|
||||
data: orders
|
||||
data: {
|
||||
list: orders,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasMore: (page - 1) * pageSize + orders.length < total,
|
||||
}
|
||||
});
|
||||
}
|
||||
const uid = resolveSecureUserId(
|
||||
@@ -1471,10 +1528,23 @@ app.get('/api/orders', async (req: Request, res: Response) => {
|
||||
Number(req.query.userId || req.headers['x-user-id'] || 0),
|
||||
'查看订单列表'
|
||||
);
|
||||
const orders = await repo.getOrdersForUser(uid);
|
||||
const orders = await repo.getOrdersForUser(uid, queryOpts);
|
||||
if (!paged) {
|
||||
return res.json({
|
||||
code: 20000,
|
||||
data: orders
|
||||
});
|
||||
}
|
||||
const total = await repo.countOrdersForUser(uid, { status });
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: orders
|
||||
data: {
|
||||
list: orders,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasMore: (page - 1) * pageSize + orders.length < total,
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 500;
|
||||
|
||||
@@ -515,7 +515,7 @@ describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
|
||||
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const repoContent = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(repoContent).toContain("WHERE (u.status IS NULL OR u.status != '已封禁')");
|
||||
expect(repoContent).toContain("(u.status IS NULL OR u.status != '已封禁')");
|
||||
expect(repoContent).toContain("UPDATE activities SET status = 'CANCELLED' WHERE creator_id = ? AND status = 'RECRUITING'");
|
||||
|
||||
const rolePath = path.join(rootDir, 'pwuniapp', 'common', 'role.js');
|
||||
|
||||
60
server/tests/pagination.spec.ts
普通文件
60
server/tests/pagination.spec.ts
普通文件
@@ -0,0 +1,60 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 列表分页闸门:订单 / 消息通知 / 组局
|
||||
* 要求:服务端支持 page/pageSize 并返回 total/hasMore;不传 page 时保持旧数组格式兼容;
|
||||
* 小程序三页必须支持触底加载更多。
|
||||
*/
|
||||
describe('列表分页(订单 / 通知 / 组局)', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const read = (rel: string) => fs.readFileSync(path.join(root, rel), 'utf-8');
|
||||
const repo = read('server/src/db/repository.ts');
|
||||
const indexCode = read('server/src/index.ts');
|
||||
const api = read('pwuniapp/common/api.js');
|
||||
const orderList = read('pwuniapp/pages/order/list.vue');
|
||||
const notifications = read('pwuniapp/pages/user/notifications.vue');
|
||||
const activityList = read('pwuniapp/pages/activity/list.vue');
|
||||
|
||||
it('仓储层必须使用 LIMIT/OFFSET 分页,而不是一次拉全量', () => {
|
||||
expect(repo).toContain('LIMIT ${pageSize} OFFSET ${offset}');
|
||||
expect(repo).toContain('LIMIT ${take} OFFSET ${skip}');
|
||||
expect(repo).toContain('async countOrders(');
|
||||
expect(repo).toContain('async countOrdersForUser(');
|
||||
expect(repo).toContain('async countActivities(');
|
||||
expect(repo).toContain('async getUserNotificationTotal(');
|
||||
});
|
||||
|
||||
it('接口必须返回 total/hasMore,未传 page 时保持旧数组格式', () => {
|
||||
expect(indexCode).toContain("if (Number(req.query.page) > 0)");
|
||||
expect(indexCode).toContain('hasMore: (page - 1) * pageSize');
|
||||
// 旧调用(管理后台 / 工作台)不传 page 仍拿数组
|
||||
expect(indexCode).toContain('const paged = Number(req.query.page) > 0;');
|
||||
expect(indexCode).toContain('data: orders');
|
||||
expect(indexCode).toContain('data: acts');
|
||||
});
|
||||
|
||||
it('小程序 API 层传递分页参数并保持旧调用兼容', () => {
|
||||
expect(api).toContain('page=${Math.floor(Number(options.page))}');
|
||||
expect(api).toContain('pageSize=${Math.floor(Number(options.pageSize) || 20)}');
|
||||
expect(api).toContain("return paged ? { list: [], total: 0, page: 1, pageSize: 20, hasMore: false } : [];");
|
||||
expect(api).toContain('if (Number(extra.page) > 0)');
|
||||
});
|
||||
|
||||
it('订单 / 通知 / 组局页面必须支持触底加载更多', () => {
|
||||
expect(orderList).toContain('onReachBottom');
|
||||
expect(orderList).toContain('loadLiveOrders(false)');
|
||||
expect(notifications).toContain('@scrolltolower="loadMore"');
|
||||
expect(notifications).toContain('loadData(false)');
|
||||
expect(activityList).toContain('onReachBottom');
|
||||
expect(activityList).toContain('fetchLiveActivities(false)');
|
||||
});
|
||||
|
||||
it('筛选与搜索必须下推到服务端,避免只过滤当前页造成漏数据', () => {
|
||||
expect(orderList).toContain('status: currentTab.value');
|
||||
expect(activityList).toContain('keyword: searchKeyword.value.trim()');
|
||||
expect(activityList).toContain('payType: currentTab.value');
|
||||
expect(activityList).toContain('creatorId: currentTab.value');
|
||||
});
|
||||
});
|
||||
@@ -21,16 +21,16 @@ describe('第十二批读接口数据隔离闸门 (订单 / 我的组局 / 分
|
||||
it('仓储层提供按用户隔离的 getOrdersForUser', () => {
|
||||
expect(repoCode).toContain('async getOrdersForUser(');
|
||||
const start = repoCode.indexOf('async getOrdersForUser(');
|
||||
const block = repoCode.slice(start, start + 1400);
|
||||
const block = repoCode.slice(start, start + 2200);
|
||||
expect(block).toContain('buyer_id = ?');
|
||||
expect(block).toContain('claimed_by = ?');
|
||||
expect(block).toContain('companion_name IN (SELECT name FROM companions');
|
||||
});
|
||||
|
||||
it('订单列表:管理端全量,普通用户仅本人相关', () => {
|
||||
const block = routeBlock("app.get('/api/orders',");
|
||||
const block = routeBlock("app.get('/api/orders',", 1800);
|
||||
expect(block).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(block).toContain('repo.getOrdersForUser(uid)');
|
||||
expect(block).toContain('repo.getOrdersForUser(uid,');
|
||||
expect(block).toContain("'查看订单列表'");
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe('第十二批读接口数据隔离闸门 (订单 / 我的组局 / 分
|
||||
describe('3. 小程序端订单列表携带当前用户身份', () => {
|
||||
it('getOrders 必须带上当前用户 id', () => {
|
||||
const start = apiCode.indexOf('export async function getOrders(');
|
||||
const block = apiCode.slice(start, start + 300);
|
||||
const block = apiCode.slice(start, start + 900);
|
||||
expect(block).toContain('/orders?userId=${normalizeUserId()}');
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户