feat(M08-D): 补团购与第三方平台运营
This commit is contained in:
@@ -39,6 +39,7 @@ const recordsQuery = z.object({
|
||||
status: z.string().min(1).max(32).optional(),
|
||||
storeId: z.string().regex(/^[1-9]\d{0,19}$/).optional()
|
||||
});
|
||||
const setupQuery = z.object({ provider: providerSchema.optional() });
|
||||
const configSchema = z.object({
|
||||
provider: providerSchema,
|
||||
storeId: z.string().regex(/^[1-9]\d{0,19}$/).nullable().default(null),
|
||||
@@ -83,56 +84,49 @@ export async function registerThirdPartyRoutes(
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/admin-api/group-vouchers/redeem-manual', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const body = manualSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.redeemVoucherManually({
|
||||
tenantId: auth.tenantId,
|
||||
actorId: auth.userId,
|
||||
access: auth.access!,
|
||||
...body.data
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
for (const path of [
|
||||
'/admin-api/group-vouchers/redeem',
|
||||
'/app-api/management/group-vouchers/redeem'
|
||||
]) {
|
||||
app.post(path, async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const body = redeemSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.redeemVoucherAsManager({
|
||||
tenantId: auth.tenantId,
|
||||
actorId: auth.userId,
|
||||
access: auth.access!,
|
||||
...body.data
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/app-api/management/group-vouchers/redeem', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const body = redeemSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.redeemVoucherAsManager({
|
||||
tenantId: auth.tenantId,
|
||||
actorId: auth.userId,
|
||||
access: auth.access!,
|
||||
...body.data
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/app-api/management/group-vouchers/redeem-manual', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const body = manualSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.redeemVoucherManually({
|
||||
tenantId: auth.tenantId,
|
||||
actorId: auth.userId,
|
||||
access: auth.access!,
|
||||
...body.data
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
for (const path of [
|
||||
'/admin-api/group-vouchers/redeem-manual',
|
||||
'/app-api/management/group-vouchers/redeem-manual'
|
||||
]) {
|
||||
app.post(path, async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const body = manualSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.redeemVoucherManually({
|
||||
tenantId: auth.tenantId,
|
||||
actorId: auth.userId,
|
||||
access: auth.access!,
|
||||
...body.data
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
app.post(
|
||||
'/app-api/third-party/:provider/tenants/:tenantId/bookings/notify',
|
||||
@@ -192,6 +186,22 @@ export async function registerThirdPartyRoutes(
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/admin-api/third-party/setup', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const query = setupQuery.safeParse(request.query);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!query.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.service.listSetup({
|
||||
tenantId: auth.tenantId,
|
||||
access: auth.access!,
|
||||
provider: query.data.provider as ThirdPartyProvider | undefined
|
||||
}),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/app-api/management/third-party/records', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options, true);
|
||||
const query = recordsQuery.safeParse(request.query);
|
||||
|
||||
+60
@@ -345,6 +345,55 @@ export class ThirdPartyService {
|
||||
return { bookings, redemptions };
|
||||
}
|
||||
|
||||
async listSetup(input: {
|
||||
tenantId: string;
|
||||
access: AccessProfile;
|
||||
provider?: ThirdPartyProvider;
|
||||
}) {
|
||||
if (!input.access.capabilities.includes('tenant.manage')
|
||||
&& !input.access.roles.includes('PLATFORM_ADMIN')) {
|
||||
throw new ThirdPartyError('THIRD_PARTY_CONFIG_FORBIDDEN');
|
||||
}
|
||||
const params = input.provider ? [input.tenantId, input.provider] : [input.tenantId];
|
||||
const providerFilter = input.provider ? 'AND provider = ?' : '';
|
||||
const [configs] = await this.pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, provider, store_id AS storeId, mode, enabled,
|
||||
credential_ref AS credentialRef, settings, updated_at AS updatedAt
|
||||
FROM qipai_third_party_configs
|
||||
WHERE tenant_id = ? ${providerFilter}
|
||||
ORDER BY provider, store_id IS NULL DESC, store_id, id`,
|
||||
params
|
||||
);
|
||||
const [mappings] = await this.pool.execute<RowDataPacket[]>(
|
||||
`SELECT id, provider, resource_type AS resourceType,
|
||||
external_ref AS externalRef, local_resource_id AS localResourceId
|
||||
FROM qipai_third_party_mappings
|
||||
WHERE tenant_id = ? ${providerFilter}
|
||||
ORDER BY provider, resource_type, id`,
|
||||
params
|
||||
);
|
||||
return {
|
||||
configs: configs.map((row) => ({
|
||||
id: String(row.id),
|
||||
provider: row.provider,
|
||||
storeId: row.storeId === null ? null : String(row.storeId),
|
||||
mode: row.mode,
|
||||
enabled: Boolean(row.enabled),
|
||||
credentialRef: String(row.credentialRef || ''),
|
||||
credentialConfigured: Boolean(row.credentialRef),
|
||||
settings: sanitizeSettings(row.settings),
|
||||
updatedAt: row.updatedAt
|
||||
})),
|
||||
mappings: mappings.map((row) => ({
|
||||
id: String(row.id),
|
||||
provider: row.provider,
|
||||
resourceType: row.resourceType,
|
||||
externalRef: row.externalRef,
|
||||
localResourceId: String(row.localResourceId)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async saveConfig(input: {
|
||||
tenantId: string;
|
||||
actorId: string;
|
||||
@@ -724,6 +773,17 @@ function sanitizePayload(value: Record<string, unknown>) {
|
||||
return copy;
|
||||
}
|
||||
|
||||
function sanitizeSettings(value: unknown): Record<string, unknown> {
|
||||
let source: unknown = value;
|
||||
if (typeof source === 'string') {
|
||||
try { source = JSON.parse(source); } catch { return {}; }
|
||||
}
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) return {};
|
||||
return Object.fromEntries(Object.entries(source as Record<string, unknown>)
|
||||
.filter(([key]) => !/(secret|token|password|private|api.?key|credential)/i.test(key))
|
||||
.map(([key, item]) => [key, item]));
|
||||
}
|
||||
|
||||
function normalizeBooking(row: BookingRow) {
|
||||
return {
|
||||
bookingId: String(row.id),
|
||||
|
||||
Reference in New Issue
Block a user