feat(M08-C): 补会员检索与员工概况
This commit is contained in:
@@ -11,9 +11,15 @@ const forbiddenToken = signAccessToken({
|
||||
sub: '22', sid: '6c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
||||
tid: '7', aid: '9', rv: 1
|
||||
}, secret, 900);
|
||||
const managerToken = signAccessToken({
|
||||
sub: '23', sid: '7c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
||||
tid: '7', aid: '9', rv: 1
|
||||
}, secret, 900);
|
||||
|
||||
let profileInput;
|
||||
let benefitsInput;
|
||||
let memberListInput;
|
||||
let memberDetailInput;
|
||||
const app = await buildApp({
|
||||
members: {
|
||||
jwtSecret: secret,
|
||||
@@ -25,7 +31,8 @@ const app = await buildApp({
|
||||
platformAppId: '9',
|
||||
expiresAt: new Date(Date.now() + 60000),
|
||||
user: {
|
||||
id: sessionId === '6c4d3af8-c63c-4edb-bf95-b84127bb3f6e' ? '22' : '21',
|
||||
id: sessionId === '6c4d3af8-c63c-4edb-bf95-b84127bb3f6e' ? '22'
|
||||
: sessionId === '7c4d3af8-c63c-4edb-bf95-b84127bb3f6e' ? '23' : '21',
|
||||
tenantId: '7',
|
||||
userType: 'CUSTOMER',
|
||||
status: 'ACTIVE',
|
||||
@@ -39,14 +46,28 @@ const app = await buildApp({
|
||||
},
|
||||
accessControl: {
|
||||
async getAccessProfile(tenantId, userId) {
|
||||
return userId === '21'
|
||||
return userId === '23'
|
||||
? { roles: ['STORE_ADMIN'], capabilities: ['user.read'], storeIds: ['11'] }
|
||||
: userId === '21'
|
||||
? { roles: ['CUSTOMER'], capabilities: ['profile.read', 'order.self.read'], storeIds: [] }
|
||||
: { roles: ['CUSTOMER'], capabilities: ['order.self.read'], storeIds: [] };
|
||||
}
|
||||
},
|
||||
service: {
|
||||
async listMembers() { throw new Error('not called'); },
|
||||
async getMember() { throw new Error('not called'); },
|
||||
async listMembers(input) {
|
||||
memberListInput = input;
|
||||
return {
|
||||
items: [{
|
||||
memberId: '31', nickname: '门店会员', maskedPhone: '138****8000',
|
||||
wallet: { totalBalanceCents: 1200 }, orders: { orderCount: 2 }
|
||||
}],
|
||||
total: 1
|
||||
};
|
||||
},
|
||||
async getMember(input) {
|
||||
memberDetailInput = input;
|
||||
return { memberId: input.memberId, nickname: '门店会员', recentLedger: [] };
|
||||
},
|
||||
async getMyBenefits(input) {
|
||||
benefitsInput = input;
|
||||
return {
|
||||
@@ -100,6 +121,33 @@ assert.equal(benefitsInput.userId, '21');
|
||||
assert.equal(benefits.json().data.coupons[0].couponGrantId, '401');
|
||||
assert.equal(benefits.json().data.packages[0].packageHoldingId, '501');
|
||||
|
||||
const members = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/management/members?page=1&pageSize=20&status=ACTIVE&search=138',
|
||||
headers: { authorization: `Bearer ${managerToken}` }
|
||||
});
|
||||
assert.equal(members.statusCode, 200);
|
||||
assert.equal(memberListInput.actor.userId, '23');
|
||||
assert.deepEqual(memberListInput.actor.access.storeIds, ['11']);
|
||||
assert.equal(memberListInput.status, 'ACTIVE');
|
||||
assert.equal(memberListInput.search, '138');
|
||||
assert.equal(members.json().data.items[0].memberId, '31');
|
||||
|
||||
const memberDetail = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/management/members/31',
|
||||
headers: { authorization: `Bearer ${managerToken}` }
|
||||
});
|
||||
assert.equal(memberDetail.statusCode, 200);
|
||||
assert.equal(memberDetailInput.memberId, '31');
|
||||
|
||||
const memberForbidden = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/management/members',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(memberForbidden.statusCode, 403);
|
||||
|
||||
const unauthorized = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/profile'
|
||||
|
||||
@@ -210,7 +210,8 @@ const orders = [
|
||||
id: '301',
|
||||
tenantId: '7',
|
||||
userId: '21',
|
||||
status: 'COMPLETED',
|
||||
storeId: '11',
|
||||
status: 'FINISHED',
|
||||
paidAmountCents: 12_000,
|
||||
createdAt: new Date('2026-06-23T00:00:00.000Z')
|
||||
},
|
||||
@@ -218,9 +219,19 @@ const orders = [
|
||||
id: '302',
|
||||
tenantId: '7',
|
||||
userId: '21',
|
||||
storeId: '11',
|
||||
status: 'CANCELLED',
|
||||
paidAmountCents: 0,
|
||||
createdAt: new Date('2026-06-24T00:00:00.000Z')
|
||||
},
|
||||
{
|
||||
id: '303',
|
||||
tenantId: '7',
|
||||
userId: '22',
|
||||
storeId: '11',
|
||||
status: 'CANCELLED',
|
||||
paidAmountCents: 0,
|
||||
createdAt: new Date('2026-06-25T00:00:00.000Z')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -335,7 +346,7 @@ function createService() {
|
||||
const userId = String(params[0]);
|
||||
const tenantId = String(params[1]);
|
||||
const rows = orders.filter((item) => item.tenantId === tenantId && item.userId === userId);
|
||||
const paid = rows.filter((item) => ['PAID', 'IN_USE', 'COMPLETED'].includes(item.status));
|
||||
const paid = rows.filter((item) => ['PAID', 'RESERVED', 'IN_PROGRESS', 'FINISHED'].includes(item.status));
|
||||
return [[{
|
||||
orderCount: rows.length,
|
||||
paidOrderCount: paid.length,
|
||||
@@ -379,11 +390,17 @@ function queryMembers(sql, params) {
|
||||
if (user.tenantId !== tenantId || user.userType !== 'CUSTOMER' || user.deleted) return false;
|
||||
if (statusFilter && user.status !== statusFilter) return false;
|
||||
if (hasStoreScope) {
|
||||
return wallets.some((wallet) =>
|
||||
const walletMatch = wallets.some((wallet) =>
|
||||
wallet.tenantId === tenantId
|
||||
&& wallet.userId === user.id
|
||||
&& storeParams.includes(wallet.storeId)
|
||||
);
|
||||
const orderMatch = orders.some((order) =>
|
||||
order.tenantId === tenantId
|
||||
&& order.userId === user.id
|
||||
&& storeParams.includes(order.storeId)
|
||||
);
|
||||
return walletMatch || orderMatch;
|
||||
}
|
||||
return true;
|
||||
}).map((user) => ({
|
||||
@@ -458,12 +475,9 @@ const forbiddenReader = {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
assert.equal(scoped.total, 1);
|
||||
assert.equal(scoped.items[0].memberId, '21');
|
||||
await assert.rejects(
|
||||
() => service.getMember({ actor: storeReader, memberId: '22' }),
|
||||
(error) => error instanceof MemberProfileError && error.code === 'MEMBER_NOT_FOUND'
|
||||
);
|
||||
assert.equal(scoped.total, 2);
|
||||
assert.equal(scoped.items[0].memberId, '22');
|
||||
assert.equal((await service.getMember({ actor: storeReader, memberId: '22' })).memberId, '22');
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ThirdPartyClient } from '../dist/third-party/third-party-client.js';
|
||||
import { ThirdPartyService } from '../dist/third-party/third-party-service.js';
|
||||
import { DeviceRepository } from '../dist/devices/device-repository.js';
|
||||
import { IotMessageService } from '../dist/devices/iot-message-service.js';
|
||||
import { MemberProfileService } from '../dist/wallets/member-profile-service.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -708,6 +709,25 @@ async function assertPricingAndReservations(pool, context) {
|
||||
assert.equal(managedAudit[0].action, 'ORDER_CREATED_ON_BEHALF');
|
||||
assert.equal(String(JSON.parse(managedAudit[0].metadata).targetUserId), customerId);
|
||||
|
||||
const scopedMembers = await new MemberProfileService(pool).listMembers({
|
||||
actor: {
|
||||
tenantId: context.tenantId,
|
||||
userId: adminId,
|
||||
access: {
|
||||
roles: ['STORE_ADMIN'],
|
||||
capabilities: ['user.read'],
|
||||
storeIds: [String(targetRows[0].storeId)]
|
||||
}
|
||||
},
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
status: 'ACTIVE',
|
||||
search: customerId
|
||||
});
|
||||
assert.equal(scopedMembers.total, 1);
|
||||
assert.equal(scopedMembers.items[0].memberId, customerId);
|
||||
assert.ok(scopedMembers.items[0].orders.orderCount >= 1);
|
||||
|
||||
const [foreignUser] = await pool.query(
|
||||
`INSERT INTO qipai_users (tenant_id, user_type, status)
|
||||
VALUES (?, 'CUSTOMER', 'ACTIVE')`,
|
||||
@@ -2000,6 +2020,7 @@ try {
|
||||
'store-scoped on-behalf booking rejection',
|
||||
'manager on-behalf booking history and audit attribution',
|
||||
'cross-tenant on-behalf target user rejection',
|
||||
'store-scoped member discovery through historical owner orders',
|
||||
'share token stored as SHA-256 only',
|
||||
'default view and door permissions',
|
||||
'renew permission denied by default',
|
||||
|
||||
@@ -115,6 +115,18 @@ assert.equal(listedInput.role, 'CLEANER');
|
||||
assert.equal(listedInput.status, 'ACTIVE');
|
||||
assert.equal(listedInput.search, '保洁');
|
||||
|
||||
const miniappStaff = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/app-api/management/staff?page=1&pageSize=20&status=ACTIVE&search=%E5%A4%9C%E7%8F%AD',
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
});
|
||||
assert.equal(miniappStaff.statusCode, 200);
|
||||
assert.equal(listedInput.page, 1);
|
||||
assert.equal(listedInput.pageSize, 20);
|
||||
assert.equal(listedInput.status, 'ACTIVE');
|
||||
assert.equal(listedInput.search, '夜班');
|
||||
assert.equal(listedInput.userType, 'STAFF');
|
||||
|
||||
const updated = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/admin-api/users/22',
|
||||
|
||||
Reference in New Issue
Block a user