feat(M08-C): 补会员检索与员工概况
This commit is contained in:
@@ -72,6 +72,7 @@ export class UserManagementRepository {
|
||||
status?: 'ACTIVE' | 'DISABLED';
|
||||
role?: AssignableRole;
|
||||
search?: string;
|
||||
userType?: 'STAFF';
|
||||
}): Promise<{ items: ManagedUser[]; total: number }> {
|
||||
const filters = ['u.tenant_id = ?', 'u.deleted_at IS NULL'];
|
||||
const params: Array<string | number> = [input.actor.tenantId];
|
||||
@@ -79,6 +80,10 @@ export class UserManagementRepository {
|
||||
filters.push('u.status = ?');
|
||||
params.push(input.status);
|
||||
}
|
||||
if (input.userType) {
|
||||
filters.push('u.user_type = ?');
|
||||
params.push(input.userType);
|
||||
}
|
||||
if (input.role) {
|
||||
filters.push(`EXISTS (
|
||||
SELECT 1 FROM qipai_user_roles fur
|
||||
@@ -102,6 +107,8 @@ export class UserManagementRepository {
|
||||
WHERE ${filters.join(' AND ')}`,
|
||||
params
|
||||
);
|
||||
const pageSize = Math.max(1, Math.min(100, Math.trunc(input.pageSize)));
|
||||
const offset = Math.max(0, (Math.trunc(input.page) - 1) * pageSize);
|
||||
const [rows] = await this.pool.execute<UserRow[]>(
|
||||
`SELECT u.id, u.user_type AS userType, u.status, u.nickname,
|
||||
u.avatar_url AS avatarUrl, u.phone,
|
||||
@@ -119,8 +126,8 @@ export class UserManagementRepository {
|
||||
LEFT JOIN qipai_user_admin_profiles p
|
||||
ON p.tenant_id = u.tenant_id AND p.user_id = u.id
|
||||
WHERE ${filters.join(' AND ')}
|
||||
ORDER BY u.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, input.pageSize, (input.page - 1) * input.pageSize]
|
||||
ORDER BY u.id DESC LIMIT ${pageSize} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
const items = await Promise.all(rows.map(async (row) => ({
|
||||
id: String(row.id),
|
||||
|
||||
@@ -517,7 +517,8 @@ export class CleaningTaskRepository {
|
||||
params.push(input.cleanerUserId);
|
||||
}
|
||||
const whereSql = where.join(' AND ');
|
||||
const offset = (input.page - 1) * input.pageSize;
|
||||
const pageSize = Math.max(1, Math.min(100, Math.trunc(input.pageSize)));
|
||||
const offset = Math.max(0, (Math.trunc(input.page) - 1) * pageSize);
|
||||
const [counts] = await this.pool.execute<CountRow[]>(
|
||||
`SELECT COUNT(*) AS total FROM qipai_cleaning_settlements s WHERE ${whereSql}`,
|
||||
params
|
||||
@@ -538,14 +539,14 @@ export class CleaningTaskRepository {
|
||||
LEFT JOIN qipai_stores st ON st.tenant_id = s.tenant_id AND st.id = s.store_id
|
||||
WHERE ${whereSql}
|
||||
ORDER BY s.created_at DESC, s.id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, input.pageSize, offset]
|
||||
LIMIT ${pageSize} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
return {
|
||||
items: rows.map(publicSettlement),
|
||||
total: Number(counts[0]?.total ?? 0),
|
||||
page: input.page,
|
||||
pageSize: input.pageSize
|
||||
pageSize
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1046,7 +1047,8 @@ export class CleaningTaskRepository {
|
||||
orderBy: string
|
||||
) {
|
||||
const whereSql = where.join(' AND ');
|
||||
const offset = (page - 1) * pageSize;
|
||||
const safePageSize = Math.max(1, Math.min(100, Math.trunc(pageSize)));
|
||||
const offset = Math.max(0, (Math.trunc(page) - 1) * safePageSize);
|
||||
const [counts] = await this.pool.execute<CountRow[]>(
|
||||
`SELECT COUNT(*) AS total FROM qipai_cleaning_tasks t WHERE ${whereSql}`,
|
||||
params
|
||||
@@ -1069,10 +1071,13 @@ export class CleaningTaskRepository {
|
||||
LEFT JOIN qipai_orders o ON o.tenant_id = t.tenant_id AND o.id = t.order_id
|
||||
WHERE ${whereSql}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, pageSize, offset]
|
||||
LIMIT ${safePageSize} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
return { items: rows.map(publicTask), total: Number(counts[0]?.total ?? 0), page, pageSize };
|
||||
return {
|
||||
items: rows.map(publicTask), total: Number(counts[0]?.total ?? 0),
|
||||
page, pageSize: safePageSize
|
||||
};
|
||||
}
|
||||
|
||||
private async getMineTask(input: CleaningActor, taskId: string) {
|
||||
|
||||
@@ -58,7 +58,8 @@ export class OrderQueryRepository {
|
||||
params.push(input.storeId);
|
||||
}
|
||||
const whereSql = where.join(' AND ');
|
||||
const offset = (input.page - 1) * input.pageSize;
|
||||
const pageSize = Math.max(1, Math.min(50, Math.trunc(input.pageSize)));
|
||||
const offset = Math.max(0, (Math.trunc(input.page) - 1) * pageSize);
|
||||
const [counts] = await this.pool.execute<CountRow[]>(
|
||||
`SELECT COUNT(DISTINCT o.id) AS total
|
||||
FROM qipai_orders o
|
||||
@@ -89,14 +90,14 @@ export class OrderQueryRepository {
|
||||
)
|
||||
WHERE ${whereSql}
|
||||
ORDER BY o.created_at DESC, o.id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, input.pageSize, offset]
|
||||
LIMIT ${pageSize} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
return {
|
||||
items: rows.map(publicOrder),
|
||||
total: Number(counts[0]?.total ?? 0),
|
||||
page: input.page,
|
||||
pageSize: input.pageSize
|
||||
pageSize
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,29 +54,33 @@ export async function registerMemberRoutes(
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/admin-api/members', async (request, reply) => {
|
||||
const actor = await requireReader(request, reply, options);
|
||||
if (!actor) return;
|
||||
const query = listSchema.safeParse(request.query);
|
||||
if (!query.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.listMembers({ actor, ...query.data }),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
for (const path of ['/admin-api/members', '/app-api/management/members']) {
|
||||
app.get(path, async (request, reply) => {
|
||||
const actor = await requireReader(request, reply, options);
|
||||
if (!actor) return;
|
||||
const query = listSchema.safeParse(request.query);
|
||||
if (!query.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.listMembers({ actor, ...query.data }),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/admin-api/members/:id', async (request, reply) => {
|
||||
const actor = await requireReader(request, reply, options);
|
||||
if (!actor) return;
|
||||
const params = idSchema.safeParse(request.params);
|
||||
if (!params.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.getMember({ actor, memberId: params.data.id }),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
for (const path of ['/admin-api/members/:id', '/app-api/management/members/:id']) {
|
||||
app.get(path, async (request, reply) => {
|
||||
const actor = await requireReader(request, reply, options);
|
||||
if (!actor) return;
|
||||
const params = idSchema.safeParse(request.params);
|
||||
if (!params.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.getMember({ actor, memberId: params.data.id }),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function requireProfileReader(
|
||||
|
||||
@@ -47,14 +47,23 @@ export async function registerUserManagementRoutes(
|
||||
app: FastifyInstance,
|
||||
options: UserManagementRouteOptions
|
||||
): Promise<void> {
|
||||
app.get('/admin-api/users', async (request, reply) => {
|
||||
const actor = await requireManager(request, reply, options);
|
||||
if (!actor) return;
|
||||
const query = listSchema.safeParse(request.query);
|
||||
if (!query.success) return invalid(reply, request.traceId);
|
||||
const data = await options.repository.listUsers({ actor, ...query.data });
|
||||
return { code: 0, data, traceId: request.traceId };
|
||||
});
|
||||
for (const [path, staffOnly] of [
|
||||
['/admin-api/users', false],
|
||||
['/app-api/management/staff', true]
|
||||
] as const) {
|
||||
app.get(path, async (request, reply) => {
|
||||
const actor = await requireManager(request, reply, options);
|
||||
if (!actor) return;
|
||||
const query = listSchema.safeParse(request.query);
|
||||
if (!query.success) return invalid(reply, request.traceId);
|
||||
const data = await options.repository.listUsers({
|
||||
actor,
|
||||
...query.data,
|
||||
...(staffOnly ? { userType: 'STAFF' as const } : {})
|
||||
});
|
||||
return { code: 0, data, traceId: request.traceId };
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/admin-api/staff', async (request, reply) => {
|
||||
const actor = await requireManager(request, reply, options);
|
||||
|
||||
@@ -138,13 +138,15 @@ export class MemberProfileService {
|
||||
WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
const pageSize = Math.max(1, Math.min(100, Math.trunc(input.pageSize)));
|
||||
const offset = Math.max(0, (Math.trunc(input.page) - 1) * pageSize);
|
||||
const [rows] = await this.pool.execute<MemberRow[]>(
|
||||
`SELECT u.id, u.status, u.nickname, u.phone,
|
||||
u.created_at AS createdAt, u.last_login_at AS lastLoginAt
|
||||
FROM qipai_users u
|
||||
WHERE ${where}
|
||||
ORDER BY u.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, input.pageSize, (input.page - 1) * input.pageSize]
|
||||
ORDER BY u.id DESC LIMIT ${pageSize} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
const items = await Promise.all(rows.map((row) => this.memberCard(input.actor, row)));
|
||||
return { items, total: Number(counts[0]?.total ?? 0) };
|
||||
@@ -341,9 +343,9 @@ export class MemberProfileService {
|
||||
);
|
||||
const [orderRows] = await this.pool.execute<OrderSummaryRow[]>(
|
||||
`SELECT COUNT(*) AS orderCount,
|
||||
COALESCE(SUM(CASE WHEN o.status IN ('PAID', 'IN_USE', 'COMPLETED')
|
||||
COALESCE(SUM(CASE WHEN o.status IN ('PAID', 'RESERVED', 'IN_PROGRESS', 'FINISHED')
|
||||
THEN 1 ELSE 0 END), 0) AS paidOrderCount,
|
||||
COALESCE(SUM(CASE WHEN o.status IN ('PAID', 'IN_USE', 'COMPLETED')
|
||||
COALESCE(SUM(CASE WHEN o.status IN ('PAID', 'RESERVED', 'IN_PROGRESS', 'FINISHED')
|
||||
THEN o.paid_amount_cents ELSE 0 END), 0) AS paidAmountCents,
|
||||
MAX(o.created_at) AS lastOrderAt
|
||||
FROM qipai_orders o
|
||||
@@ -433,12 +435,19 @@ function memberScopeClause(actor: MemberActor, userExpression: string) {
|
||||
}
|
||||
const placeholders = actor.access.storeIds.map(() => '?').join(',');
|
||||
return {
|
||||
sql: `EXISTS (
|
||||
sql: `(EXISTS (
|
||||
SELECT 1 FROM qipai_wallet_accounts wa
|
||||
WHERE wa.tenant_id = u.tenant_id AND wa.user_id = ${userExpression}
|
||||
AND wa.store_id IN (${placeholders})
|
||||
)`,
|
||||
params: actor.access.storeIds
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM qipai_order_user_access oa
|
||||
INNER JOIN qipai_orders mo
|
||||
ON mo.tenant_id = oa.tenant_id AND mo.id = oa.order_id
|
||||
AND mo.deleted_at IS NULL
|
||||
WHERE oa.tenant_id = u.tenant_id AND oa.user_id = ${userExpression}
|
||||
AND oa.access_type = 'OWNER' AND mo.store_id IN (${placeholders})
|
||||
))`,
|
||||
params: [...actor.access.storeIds, ...actor.access.storeIds]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user