feat(M08-A): 接入顾客端个人中心
This commit is contained in:
@@ -18,7 +18,7 @@ const listSchema = z.object({
|
||||
});
|
||||
|
||||
export interface MemberRouteOptions {
|
||||
service: Pick<MemberProfileService, 'listMembers' | 'getMember'>;
|
||||
service: Pick<MemberProfileService, 'listMembers' | 'getMember' | 'getMyProfile'>;
|
||||
authRepository: Pick<AuthRepository, 'validateSession'>;
|
||||
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
|
||||
jwtSecret: string;
|
||||
@@ -28,6 +28,37 @@ export async function registerMemberRoutes(
|
||||
app: FastifyInstance,
|
||||
options: MemberRouteOptions
|
||||
): Promise<void> {
|
||||
app.get('/app-api/profile', async (request, reply) => {
|
||||
const auth = await authenticateAccessToken(
|
||||
request.headers.authorization,
|
||||
options.authRepository,
|
||||
options.jwtSecret
|
||||
);
|
||||
if (!auth) {
|
||||
return reply.status(401).send({
|
||||
code: 'AUTH_SESSION_INVALID', message: 'Authentication required.', traceId: request.traceId
|
||||
});
|
||||
}
|
||||
const access = await options.accessControl.getAccessProfile(
|
||||
auth.session.tenantId,
|
||||
auth.session.user.id
|
||||
);
|
||||
if (!access.capabilities.includes('profile.read')) {
|
||||
return reply.status(403).send({
|
||||
code: 'PROFILE_READ_FORBIDDEN', message: 'Profile read permission is required.',
|
||||
traceId: request.traceId
|
||||
});
|
||||
}
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.getMyProfile({
|
||||
tenantId: auth.session.tenantId,
|
||||
userId: auth.session.user.id
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/admin-api/members', async (request, reply) => {
|
||||
const actor = await requireReader(request, reply, options);
|
||||
if (!actor) return;
|
||||
|
||||
@@ -33,6 +33,7 @@ import { CustomerDeviceAccessRepository } from './devices/customer-device-access
|
||||
import { IotMessageService } from './devices/iot-message-service.js';
|
||||
import { DeviceCommandService } from './devices/device-command-service.js';
|
||||
import { DeviceControlService } from './devices/device-control-service.js';
|
||||
import { MemberProfileService } from './wallets/member-profile-service.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const pool = createMySqlPool(config);
|
||||
@@ -162,6 +163,12 @@ const app = await buildApp({
|
||||
authRepository,
|
||||
accessControl,
|
||||
jwtSecret: config.auth.jwtSecret
|
||||
},
|
||||
members: {
|
||||
service: new MemberProfileService(pool),
|
||||
authRepository,
|
||||
accessControl,
|
||||
jwtSecret: config.auth.jwtSecret
|
||||
}
|
||||
});
|
||||
app.addHook('onClose', async () => {
|
||||
|
||||
@@ -137,6 +137,36 @@ export class MemberProfileService {
|
||||
};
|
||||
}
|
||||
|
||||
async getMyProfile(input: {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
ledgerLimit?: number;
|
||||
}) {
|
||||
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 u.tenant_id = ? AND u.id = ? AND u.user_type = 'CUSTOMER'
|
||||
AND u.deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[input.tenantId, input.userId]
|
||||
);
|
||||
if (!rows[0]) throw new MemberProfileError('MEMBER_NOT_FOUND');
|
||||
const actor = {
|
||||
tenantId: input.tenantId,
|
||||
userId: input.userId,
|
||||
access: { roles: ['CUSTOMER'], capabilities: ['profile.read'], storeIds: [] }
|
||||
};
|
||||
return {
|
||||
...await this.memberCard(actor, rows[0]),
|
||||
recentLedger: await this.recentLedger(
|
||||
input.tenantId,
|
||||
String(rows[0].id),
|
||||
input.ledgerLimit ?? 10
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
private async memberCard(actor: MemberActor, row: MemberRow) {
|
||||
const memberId = String(row.id);
|
||||
const [walletRows] = await this.pool.execute<WalletSummaryRow[]>(
|
||||
|
||||
Reference in New Issue
Block a user