feat(M09-D4): 完成商品选购与运营工作台

This commit is contained in:
Codex
2026-08-11 08:06:13 +08:00
parent 60264cbd11
commit e91b979128
32 changed files with 1065 additions and 60 deletions
+4 -2
View File
@@ -113,8 +113,10 @@ function adminAccess(access: AccessProfile) {
const storeRead = tenant || access.capabilities.includes('store.operation.read');
const menus = [
...(storeRead ? ['overview', 'stores', 'orders', 'thirdParty'] : []),
...(tenant || access.capabilities.includes('product.catalog.read')
|| access.capabilities.includes('inventory.read') ? ['products'] : []),
...(tenant || access.capabilities.some((capability) => [
'product.catalog.read', 'product.catalog.write', 'inventory.read', 'inventory.adjust',
'goods.order.read', 'goods.order.manage', 'goods.storage.read', 'goods.storage.manage'
].includes(capability)) ? ['products'] : []),
...(tenant ? ['platformApps', 'content', 'franchise', 'system', 'payments', 'people'] : []),
...(tenant || access.capabilities.includes('device.read') ? ['devices'] : []),
...(tenant || access.capabilities.includes('cleaning.task.read') ? ['cleaning'] : [])
+22 -1
View File
@@ -119,13 +119,34 @@ export interface ProductRouteOptions {
| 'listProducts' | 'createProduct' | 'updateProduct' | 'archiveProduct'
| 'listSkus' | 'createSku' | 'updateSku' | 'archiveSku'
| 'listListings' | 'putListing' | 'archiveListing'
| 'getStoreSettings' | 'putStoreSettings'>;
| 'getStoreSettings' | 'putStoreSettings' | 'listStoreCatalog'>;
authRepository: Pick<AuthRepository, 'validateSession'>;
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
jwtSecret: string;
}
export async function registerProductRoutes(app: FastifyInstance, options: ProductRouteOptions) {
app.get('/app-api/stores/:storeId/product-catalog', async (request, reply) => {
const auth = await authenticateAccessToken(
request.headers.authorization, options.authRepository, options.jwtSecret
);
const params = storeParamsSchema.safeParse(request.params);
if (!auth) {
reply.status(401).send({
code: 'AUTH_SESSION_INVALID', message: 'Authentication required.', traceId: request.traceId
});
return;
}
if (!params.success) return invalid(reply, request.traceId);
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.repository.listStoreCatalog({
tenantId: auth.session.tenantId, storeId: params.data.storeId
}),
traceId: request.traceId
}));
});
for (const path of [
'/admin-api/stores/:storeId/product-categories',
'/app-api/management/stores/:storeId/product-categories'
+15
View File
@@ -24,6 +24,9 @@ const record = (method, result) => async (...args) => {
return typeof result === 'function' ? result(...args) : result;
};
const repository = {
listStoreCatalog: record('listStoreCatalog', {
storeId: '11', salesOpen: true, categories: []
}),
listCategories: record('listCategories', []),
createCategory: record('createCategory', { categoryId: '31', version: 1 }),
updateCategory: record('updateCategory', { categoryId: '31', version: 2 }),
@@ -79,6 +82,18 @@ const unauthorized = await app.inject({
assert.equal(unauthorized.statusCode, 401);
assert.equal(unauthorized.json().code, 'AUTH_SESSION_INVALID');
assert.equal((await app.inject({
method: 'GET', url: '/app-api/stores/11/product-catalog'
})).statusCode, 401);
const customerCatalog = await app.inject({
method: 'GET', url: '/app-api/stores/11/product-catalog',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(customerCatalog.statusCode, 200);
assert.deepEqual(calls.find((call) => call.method === 'listStoreCatalog').args[0], {
tenantId: '7', storeId: '11'
});
currentAccess = { roles: ['STAFF'], capabilities: [], storeIds: ['11'] };
const forbidden = await app.inject({
method: 'GET', url: '/admin-api/products', headers: { authorization: `Bearer ${token}` }