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 用例,全绿;线上实测分页与筛选接口正常
这个提交包含在:
@@ -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;
|
||||
|
||||
在新工单中引用
屏蔽一个用户