feat(M08-D): 补后台订单运营

This commit is contained in:
Codex
2026-08-10 11:52:25 +08:00
parent 3502f5b5f7
commit 20a292e4ce
10 changed files with 619 additions and 37 deletions
+24 -22
View File
@@ -32,29 +32,31 @@ export async function registerOrderQueryRoutes(
app: FastifyInstance,
options: OrderQueryRouteOptions
) {
app.get('/app-api/orders', async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const query = listSchema.safeParse(request.query);
if (!auth) return unauthorized(reply, request.traceId);
if (!query.success) return invalid(reply, request.traceId);
return {
code: 0,
data: await options.repository.listMine({ ...auth, ...query.data }),
traceId: request.traceId
};
});
for (const prefix of ['/app-api/orders', '/admin-api/orders', '/app-api/management/orders']) {
app.get(prefix, async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const query = listSchema.safeParse(request.query);
if (!auth) return unauthorized(reply, request.traceId);
if (!query.success) return invalid(reply, request.traceId);
return {
code: 0,
data: await options.repository.listMine({ ...auth, ...query.data }),
traceId: request.traceId
};
});
app.get('/app-api/orders/:orderId', async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const params = paramsSchema.safeParse(request.params);
if (!auth) return unauthorized(reply, request.traceId);
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.getMine({ ...auth, orderId: params.data.orderId }),
traceId: request.traceId
}));
});
app.get(`${prefix}/:orderId`, async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const params = paramsSchema.safeParse(request.params);
if (!auth) return unauthorized(reply, request.traceId);
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.getMine({ ...auth, orderId: params.data.orderId }),
traceId: request.traceId
}));
});
}
}
async function authenticate(
+19 -13
View File
@@ -27,19 +27,25 @@ export interface OrderStateRouteOptions {
export async function registerOrderStateRoutes(
app: FastifyInstance, options: OrderStateRouteOptions
) {
app.get('/app-api/orders/:orderId/history', async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const params = paramsSchema.safeParse(request.params);
if (!auth) return unauthorized(reply, request.traceId);
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.history(
auth.tenantId, auth.userId, params.data.orderId, auth.access
),
traceId: request.traceId
}));
});
for (const path of [
'/app-api/orders/:orderId/history',
'/admin-api/orders/:orderId/history',
'/app-api/management/orders/:orderId/history'
]) {
app.get(path, async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
const params = paramsSchema.safeParse(request.params);
if (!auth) return unauthorized(reply, request.traceId);
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.history(
auth.tenantId, auth.userId, params.data.orderId, auth.access
),
traceId: request.traceId
}));
});
}
app.post('/app-api/orders/:orderId/cancel', async (request, reply) => {
const auth = await authenticate(request.headers.authorization, options);
+15
View File
@@ -89,6 +89,14 @@ assert.equal(listInput.pageSize, 5);
assert.equal(listInput.status, 'PENDING_PAYMENT');
assert.equal(listInput.storeId, '18');
const adminListed = await app.inject({
method: 'GET',
url: '/admin-api/orders?page=1&pageSize=20&storeId=18',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(adminListed.statusCode, 200);
assert.equal(listInput.storeId, '18');
const detail = await app.inject({
method: 'GET',
url: '/app-api/orders/31',
@@ -97,6 +105,13 @@ const detail = await app.inject({
assert.equal(detail.statusCode, 200);
assert.equal(detailInput.orderId, '31');
const adminDetail = await app.inject({
method: 'GET', url: '/admin-api/orders/31',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(adminDetail.statusCode, 200);
assert.equal(detailInput.orderId, '31');
const unauthenticated = await app.inject({
method: 'GET',
url: '/app-api/orders'
+14 -1
View File
@@ -8,6 +8,7 @@ const token = signAccessToken({
tid: '7', aid: '9', rv: 1
}, secret, 900);
let transitionInput;
let historyInput;
const options = {
jwtSecret: secret,
authRepository: {
@@ -33,7 +34,10 @@ const options = {
transitionInput = { actor, orderId, action, reason };
return { orderId, status: action === 'CANCEL' ? 'CANCELLED' : 'PAID' };
},
async history() { return []; }
async history(tenantId, userId, orderId, access) {
historyInput = { tenantId, userId, orderId, access };
return [{ id: '1', fromStatus: null, toStatus: 'DRAFT', action: 'CREATED' }];
}
}
};
const app = await buildApp({ orderState: options });
@@ -76,6 +80,15 @@ assert.equal(transitionInput.action, 'START');
assert.equal(transitionInput.actor.source, 'ADMIN');
assert.equal(transitionInput.actor.traceId, 'm08c-route-transition');
const adminHistory = await app.inject({
method: 'GET', url: '/admin-api/orders/31/history',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(adminHistory.statusCode, 200);
assert.equal(adminHistory.json().data[0].action, 'CREATED');
assert.equal(historyInput.orderId, '31');
assert.equal(historyInput.access.roles[0], 'TENANT_ADMIN');
const cancelled = await app.inject({
method: 'POST',
url: '/app-api/orders/31/cancel',