create-fullstack-scaffold 0.4.25 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +432 -15
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -13
- package/template/drizzle.config.ts +10 -4
- package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
- package/template/eslint-rules/no-cross-module-service-import.js +4 -0
- package/template/eslint-rules/no-merged-api-type-export.js +190 -0
- package/template/eslint.config.js +3 -0
- package/template/package.json +9 -6
- package/template/scripts/sync-agent-hooks.mjs +189 -0
- package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
- package/template/src/admin/pages/ContentPage.tsx +1 -1
- package/template/src/admin/pages/DisputesPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +1 -1
- package/template/src/admin/pages/TicketsPage.tsx +1 -1
- package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
- package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
- package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
- package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
- package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
- package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
- package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
- package/template/src/admin/services/apiClient.ts +2 -3
- package/template/src/cli/modules/content/index.ts +3 -3
- package/template/src/cli/modules/dispute/index.ts +3 -3
- package/template/src/cli/modules/ticket/index.ts +3 -3
- package/template/src/cli/modules/todo/index.ts +1 -1
- package/template/src/cli/rpc/client.ts +3 -4
- package/template/src/cli/rpc/index.ts +1 -1
- package/template/src/client/App.tsx +3 -39
- package/template/src/client/AppRoutes.tsx +53 -0
- package/template/src/client/entry-server.tsx +75 -0
- package/template/src/client/main.tsx +3 -1
- package/template/src/client/pages/SearchPage.tsx +1 -1
- package/template/src/client/services/apiClient.ts +12 -4
- package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
- package/template/src/client/stores/entry-stores.ts +36 -0
- package/template/src/client/stores/notificationStore.ts +4 -4
- package/template/src/client/stores/todoStore.ts +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +3 -3
- package/template/src/merchant/pages/OrdersPage.tsx +1 -1
- package/template/src/merchant/pages/ProductsPage.tsx +1 -1
- package/template/src/merchant/pages/SettingsPage.tsx +1 -1
- package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
- package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
- package/template/src/server/app.ts +6 -4
- package/template/src/server/core/__tests__/isr-cache-cf.test.ts +96 -0
- package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
- package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
- package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
- package/template/src/server/core/isr-cache.ts +61 -18
- package/template/src/server/core/isr-invalidation.ts +6 -12
- package/template/src/server/core/isr-registry.ts +104 -0
- package/template/src/server/core/isr-renderer.ts +75 -0
- package/template/src/server/db/schema/contents.ts +28 -20
- package/template/src/server/db/schema/disputes.ts +30 -22
- package/template/src/server/db/schema/notifications.ts +20 -14
- package/template/src/server/db/schema/orders.ts +23 -16
- package/template/src/server/db/schema/plugins.ts +56 -38
- package/template/src/server/db/schema/products.ts +24 -17
- package/template/src/server/db/schema/tickets.ts +44 -31
- package/template/src/server/db/schema/todos.ts +26 -18
- package/template/src/server/entries/cloudflare.ts +97 -21
- package/template/src/server/entries/node.ts +0 -2
- package/template/src/server/index.ts +0 -1
- package/template/src/server/isr-modules.ts +10 -0
- package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
- package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
- package/template/src/server/module-admin/services/admin-service.ts +57 -13
- package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
- package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
- package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
- package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
- package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
- package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
- package/template/src/server/module-content/isr.ts +63 -0
- package/template/src/server/module-content/routes/content-routes.ts +10 -10
- package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
- package/template/src/server/module-content/routes/topics-routes.ts +3 -0
- package/template/src/server/module-content/services/content-service.ts +23 -10
- package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
- package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
- package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
- package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
- package/template/src/server/module-file/routes/file-routes.ts +3 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
- package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
- package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
- package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
- package/template/src/server/module-order/routes/cart-routes.ts +3 -0
- package/template/src/server/module-order/routes/order-routes.ts +9 -4
- package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
- package/template/src/server/module-order/services/order-service.ts +25 -13
- package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
- package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
- package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
- package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
- package/template/src/server/module-permission/routes/role-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
- package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
- package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
- package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
- package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
- package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
- package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
- package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
- package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
- package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
- package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
- package/template/src/server/module-todos/isr.ts +41 -0
- package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
- package/template/src/server/module-todos/services/todo-service.ts +31 -10
- package/template/src/server/route-registry.ts +0 -4
- package/template/src/server/rpc-merge.ts +27 -0
- package/template/src/server/rpc-surface.ts +117 -0
- package/template/src/server/rpc-type-canary.ts +80 -0
- package/template/src/server/test-utils/test-client.ts +7 -9
- package/template/src/server/test-utils/test-isr-helper.ts +140 -0
- package/template/src/shared/modules/content/schemas.ts +14 -0
- package/template/src/shared/modules/dispute/schemas.ts +14 -0
- package/template/src/shared/modules/order/schemas.ts +9 -1
- package/template/src/shared/modules/ticket/schemas.ts +14 -0
- package/template/src/shared/modules/todos/index.ts +4 -0
- package/template/src/shared/modules/todos/schemas.ts +14 -0
- package/template/src/shared/schemas/index.ts +18 -0
- package/template/tsup.config.ts +48 -1
- package/template/vitest.config.ts +19 -3
- package/template/vitest.setup.ts +68 -5
- package/template/patches/typescript+5.9.3.patch +0 -24
- /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
|
@@ -88,8 +88,13 @@ export async function getContents(filters?: {
|
|
|
88
88
|
category?: ContentCategory
|
|
89
89
|
status?: ContentStatus
|
|
90
90
|
search?: string
|
|
91
|
-
|
|
91
|
+
page?: number
|
|
92
|
+
limit?: number
|
|
93
|
+
}): Promise<{ contents: Content[]; total: number; page: number; limit: number }> {
|
|
92
94
|
const db = await getDb()
|
|
95
|
+
const page = filters?.page ?? 1
|
|
96
|
+
const limit = filters?.limit ?? 20
|
|
97
|
+
const offset = (page - 1) * limit
|
|
93
98
|
const conditions = []
|
|
94
99
|
|
|
95
100
|
if (filters?.category) {
|
|
@@ -105,16 +110,24 @@ export async function getContents(filters?: {
|
|
|
105
110
|
)
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
const
|
|
109
|
-
conditions.length > 0
|
|
110
|
-
? await db
|
|
111
|
-
.select()
|
|
112
|
-
.from(contents)
|
|
113
|
-
.where(and(...conditions))
|
|
114
|
-
.orderBy(desc(contents.createdAt))
|
|
115
|
-
: await db.select().from(contents).orderBy(desc(contents.createdAt))
|
|
113
|
+
const baseQuery = conditions.length > 0 ? and(...conditions) : undefined
|
|
116
114
|
|
|
117
|
-
|
|
115
|
+
// SQL 分页(避免全表扫 + 内存 slice)
|
|
116
|
+
const rows = await db
|
|
117
|
+
.select()
|
|
118
|
+
.from(contents)
|
|
119
|
+
.where(baseQuery)
|
|
120
|
+
.orderBy(desc(contents.createdAt))
|
|
121
|
+
.limit(limit)
|
|
122
|
+
.offset(offset)
|
|
123
|
+
const total = await db.$count(contents, baseQuery)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
contents: rows.map(mapContentRow),
|
|
127
|
+
total,
|
|
128
|
+
page,
|
|
129
|
+
limit,
|
|
130
|
+
}
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
export async function getContentById(id: string): Promise<Content | null> {
|
|
@@ -22,7 +22,8 @@ describe('Dispute Routes', () => {
|
|
|
22
22
|
const data = await res.json()
|
|
23
23
|
expect(data.success).toBe(true)
|
|
24
24
|
if (data.success) {
|
|
25
|
-
expect(
|
|
25
|
+
expect(data.data.disputes).toBeDefined()
|
|
26
|
+
expect(typeof data.data.total).toBe('number')
|
|
26
27
|
}
|
|
27
28
|
})
|
|
28
29
|
})
|
|
@@ -17,7 +17,8 @@ describe('Dispute Service', () => {
|
|
|
17
17
|
describe('getDisputes', () => {
|
|
18
18
|
it('should return all disputes', async () => {
|
|
19
19
|
const result = await service.getDisputes()
|
|
20
|
-
expect(
|
|
20
|
+
expect(result.disputes).toBeDefined()
|
|
21
|
+
expect(typeof result.total).toBe('number')
|
|
21
22
|
})
|
|
22
23
|
})
|
|
23
24
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRoute
|
|
1
|
+
import { createRoute } from '@hono/zod-openapi'
|
|
2
2
|
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
3
3
|
import * as disputeService from '../services/dispute-service'
|
|
4
4
|
import {
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
DisputeSchema,
|
|
16
16
|
CreateDisputeSchema,
|
|
17
17
|
UpdateDisputeSchema,
|
|
18
|
-
|
|
18
|
+
DisputeListResponseSchema,
|
|
19
|
+
DisputeListQuerySchema,
|
|
19
20
|
ResolveDisputeSchema,
|
|
20
21
|
DisputeDeleteResultSchema,
|
|
21
22
|
} from '@shared/modules/dispute'
|
|
@@ -28,13 +29,10 @@ const listRoute = createRoute({
|
|
|
28
29
|
security: [{ Bearer: [] }],
|
|
29
30
|
middleware: [authMiddleware({ requiredPermissions: [Permission.DISPUTE_VIEW] })],
|
|
30
31
|
request: {
|
|
31
|
-
query:
|
|
32
|
-
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
33
|
-
offset: z.coerce.number().int().min(0).default(0),
|
|
34
|
-
}),
|
|
32
|
+
query: DisputeListQuerySchema,
|
|
35
33
|
},
|
|
36
34
|
responses: {
|
|
37
|
-
200: successResponse(
|
|
35
|
+
200: successResponse(DisputeListResponseSchema, 'List all disputes'),
|
|
38
36
|
401: errorResponse('Unauthorized'),
|
|
39
37
|
403: errorResponse('Forbidden'),
|
|
40
38
|
},
|
|
@@ -117,9 +115,9 @@ const resolveRoute = createRoute({
|
|
|
117
115
|
|
|
118
116
|
export const disputeRoutes = new OpenAPIHono()
|
|
119
117
|
.openapi(listRoute, async c => {
|
|
120
|
-
const {
|
|
121
|
-
const result = await disputeService.getDisputes()
|
|
122
|
-
return c.json(success(result
|
|
118
|
+
const { page, limit } = c.req.valid('query')
|
|
119
|
+
const result = await disputeService.getDisputes({ page, limit })
|
|
120
|
+
return c.json(success(result), 200)
|
|
123
121
|
})
|
|
124
122
|
.openapi(getRoute, async c => {
|
|
125
123
|
const { id } = c.req.valid('param')
|
|
@@ -152,3 +150,6 @@ export const disputeRoutes = new OpenAPIHono()
|
|
|
152
150
|
if (!result) throw new BusinessError('Cannot resolve dispute in current state')
|
|
153
151
|
return c.json(success(result), 200)
|
|
154
152
|
})
|
|
153
|
+
|
|
154
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
155
|
+
export type DisputesApiType = typeof disputeRoutes
|
|
@@ -91,8 +91,13 @@ function mapDisputeRow(row: DisputeTable): Dispute {
|
|
|
91
91
|
export async function getDisputes(filters?: {
|
|
92
92
|
status?: DisputeStatus
|
|
93
93
|
type?: DisputeType
|
|
94
|
-
|
|
94
|
+
page?: number
|
|
95
|
+
limit?: number
|
|
96
|
+
}): Promise<{ disputes: Dispute[]; total: number; page: number; limit: number }> {
|
|
95
97
|
const db = await getDb()
|
|
98
|
+
const page = filters?.page ?? 1
|
|
99
|
+
const limit = filters?.limit ?? 20
|
|
100
|
+
const offset = (page - 1) * limit
|
|
96
101
|
const conditions = []
|
|
97
102
|
|
|
98
103
|
if (filters?.status) {
|
|
@@ -102,7 +107,7 @@ export async function getDisputes(filters?: {
|
|
|
102
107
|
conditions.push(eq(disputes.type, filters.type))
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
const
|
|
110
|
+
const allRows =
|
|
106
111
|
conditions.length > 0
|
|
107
112
|
? await db
|
|
108
113
|
.select()
|
|
@@ -111,7 +116,14 @@ export async function getDisputes(filters?: {
|
|
|
111
116
|
.orderBy(desc(disputes.createdAt))
|
|
112
117
|
: await db.select().from(disputes).orderBy(desc(disputes.createdAt))
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
const rows = allRows.slice(offset, offset + limit)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
disputes: rows.map(mapDisputeRow),
|
|
123
|
+
total: allRows.length,
|
|
124
|
+
page,
|
|
125
|
+
limit,
|
|
126
|
+
}
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
export async function getDisputeById(id: string): Promise<Dispute | null> {
|
|
@@ -136,3 +136,6 @@ export const apiRoutes = new OpenAPIHono()
|
|
|
136
136
|
const product = await merchantService.createProduct(merchant.id, input)
|
|
137
137
|
return c.json(success(product), 201)
|
|
138
138
|
})
|
|
139
|
+
|
|
140
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
141
|
+
export type MerchantApiType = typeof apiRoutes
|
|
@@ -92,7 +92,7 @@ export async function merchantLogin(
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Generate token (simplified for demo - use JWT in production)
|
|
95
|
-
const token = `
|
|
95
|
+
const token = `mt_${crypto.randomUUID().replace(/-/g, '')}`
|
|
96
96
|
|
|
97
97
|
log.info({ merchantId: merchant.id }, 'Merchant logged in')
|
|
98
98
|
|
|
@@ -121,11 +121,11 @@ export async function getMerchantStats(merchantId: number): Promise<MerchantStat
|
|
|
121
121
|
const allProducts = await db.select().from(products).where(eq(products.merchantId, merchantId))
|
|
122
122
|
const activeProducts = allProducts.filter(p => p.status === 'active').length
|
|
123
123
|
|
|
124
|
-
// Get order stats (
|
|
125
|
-
const totalOrders =
|
|
126
|
-
const totalRevenue =
|
|
127
|
-
const pendingOrders =
|
|
128
|
-
const thisMonthRevenue =
|
|
124
|
+
// Get order stats (mock data with realistic values)
|
|
125
|
+
const totalOrders = 156
|
|
126
|
+
const totalRevenue = 45890.5
|
|
127
|
+
const pendingOrders = 12
|
|
128
|
+
const thisMonthRevenue = 8750.25
|
|
129
129
|
|
|
130
130
|
return {
|
|
131
131
|
totalOrders,
|
|
@@ -254,7 +254,7 @@ const seedMerchants = [
|
|
|
254
254
|
phone: '13800138000',
|
|
255
255
|
email: 'merchant@example.com',
|
|
256
256
|
address: '123 Main St',
|
|
257
|
-
password: '
|
|
257
|
+
password: 'Demo@2024!',
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
260
|
userId: 'merchant-2',
|
|
@@ -266,7 +266,7 @@ const seedMerchants = [
|
|
|
266
266
|
phone: null,
|
|
267
267
|
email: null,
|
|
268
268
|
address: null,
|
|
269
|
-
password: '
|
|
269
|
+
password: 'Demo@2024!',
|
|
270
270
|
},
|
|
271
271
|
]
|
|
272
272
|
|
|
@@ -167,7 +167,8 @@ export const notificationRoutes = new OpenAPIHono()
|
|
|
167
167
|
const response = await adapter.handleSSERequest()
|
|
168
168
|
return response
|
|
169
169
|
}
|
|
170
|
-
} catch {
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[NotificationRoutes] handleSSERequest failed:', error)
|
|
171
172
|
return createFallbackSSEResponse()
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -214,3 +215,6 @@ export const notificationRoutes = new OpenAPIHono()
|
|
|
214
215
|
if (!deleted) throw new NotFoundError('Notification', id)
|
|
215
216
|
return c.json(success({ id }), 200)
|
|
216
217
|
})
|
|
218
|
+
|
|
219
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
220
|
+
export type NotificationsApiType = typeof notificationRoutes
|
|
@@ -22,7 +22,7 @@ describe('Order Routes', () => {
|
|
|
22
22
|
const data = await res.json()
|
|
23
23
|
expect(data.success).toBe(true)
|
|
24
24
|
if (data.success) {
|
|
25
|
-
expect(Array.isArray(data.data)).toBe(true)
|
|
25
|
+
expect(Array.isArray(data.data.orders)).toBe(true)
|
|
26
26
|
}
|
|
27
27
|
})
|
|
28
28
|
|
|
@@ -37,8 +37,8 @@ describe('Order Routes', () => {
|
|
|
37
37
|
const data = await res.json()
|
|
38
38
|
expect(data.success).toBe(true)
|
|
39
39
|
if (data.success) {
|
|
40
|
-
expect(Array.isArray(data.data)).toBe(true)
|
|
41
|
-
data.data.forEach((order: { status: string }) => {
|
|
40
|
+
expect(Array.isArray(data.data.orders)).toBe(true)
|
|
41
|
+
data.data.orders.forEach((order: { status: string }) => {
|
|
42
42
|
expect(order.status).toBe('completed')
|
|
43
43
|
})
|
|
44
44
|
}
|
|
@@ -55,8 +55,8 @@ describe('Order Routes', () => {
|
|
|
55
55
|
const data = await res.json()
|
|
56
56
|
expect(data.success).toBe(true)
|
|
57
57
|
if (data.success) {
|
|
58
|
-
expect(Array.isArray(data.data)).toBe(true)
|
|
59
|
-
data.data.forEach((order: { customerName: string }) => {
|
|
58
|
+
expect(Array.isArray(data.data.orders)).toBe(true)
|
|
59
|
+
data.data.orders.forEach((order: { customerName: string }) => {
|
|
60
60
|
expect(order.customerName).toContain('张三')
|
|
61
61
|
})
|
|
62
62
|
}
|
|
@@ -83,8 +83,8 @@ describe('Order Routes', () => {
|
|
|
83
83
|
const data = await res.json()
|
|
84
84
|
expect(data.success).toBe(true)
|
|
85
85
|
if (data.success) {
|
|
86
|
-
expect(Array.isArray(data.data)).toBe(true)
|
|
87
|
-
data.data.forEach((order: { status: string; customerName: string }) => {
|
|
86
|
+
expect(Array.isArray(data.data.orders)).toBe(true)
|
|
87
|
+
data.data.orders.forEach((order: { status: string; customerName: string }) => {
|
|
88
88
|
expect(order.status).toBe('pending')
|
|
89
89
|
expect(order.customerName).toContain('Filter Test')
|
|
90
90
|
})
|
|
@@ -112,7 +112,7 @@ describe('Order Routes', () => {
|
|
|
112
112
|
const data = await res.json()
|
|
113
113
|
expect(data.success).toBe(true)
|
|
114
114
|
if (data.success) {
|
|
115
|
-
expect(data.data).toHaveLength(0)
|
|
115
|
+
expect(data.data.orders).toHaveLength(0)
|
|
116
116
|
}
|
|
117
117
|
})
|
|
118
118
|
})
|
|
@@ -21,7 +21,8 @@ describe('Order Service', () => {
|
|
|
21
21
|
describe('getOrders', () => {
|
|
22
22
|
it('should return all orders when no filters provided', async () => {
|
|
23
23
|
const result = await service.getOrders()
|
|
24
|
-
expect(
|
|
24
|
+
expect(result.orders).toBeDefined()
|
|
25
|
+
expect(typeof result.total).toBe('number')
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
it('should filter orders by status', async () => {
|
|
@@ -37,8 +38,8 @@ describe('Order Service', () => {
|
|
|
37
38
|
await service.updateOrder(created.id, { status: 'processing' })
|
|
38
39
|
|
|
39
40
|
const result = await service.getOrders({ status: 'processing' })
|
|
40
|
-
expect(Array.isArray(result)).toBe(true)
|
|
41
|
-
result.forEach(order => {
|
|
41
|
+
expect(Array.isArray(result.orders)).toBe(true)
|
|
42
|
+
result.orders.forEach(order => {
|
|
42
43
|
expect(order.status).toBe('processing')
|
|
43
44
|
})
|
|
44
45
|
})
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
OrderSchema,
|
|
10
10
|
CreateOrderSchema,
|
|
11
11
|
UpdateOrderSchema,
|
|
12
|
-
|
|
12
|
+
OrderListResponseSchema,
|
|
13
13
|
OrderDeleteResultSchema,
|
|
14
14
|
OrderQuerySchema,
|
|
15
15
|
} from '@shared/modules/order'
|
|
@@ -24,7 +24,7 @@ const listRoute = createRoute({
|
|
|
24
24
|
query: OrderQuerySchema,
|
|
25
25
|
},
|
|
26
26
|
responses: {
|
|
27
|
-
200: successResponse(
|
|
27
|
+
200: successResponse(OrderListResponseSchema, 'List all orders'),
|
|
28
28
|
401: errorResponse('Unauthorized'),
|
|
29
29
|
403: errorResponse('Forbidden'),
|
|
30
30
|
500: errorResponse('Internal server error'),
|
|
@@ -157,12 +157,14 @@ const cancelRoute = createRoute({
|
|
|
157
157
|
|
|
158
158
|
export const orderRoutes = new OpenAPIHono()
|
|
159
159
|
.openapi(listRoute, async c => {
|
|
160
|
-
const { status, customerName,
|
|
160
|
+
const { status, customerName, page, limit } = c.req.valid('query')
|
|
161
161
|
const result = await orderService.getOrders({
|
|
162
162
|
status: status ?? undefined,
|
|
163
163
|
customerName: customerName ?? undefined,
|
|
164
|
+
page,
|
|
165
|
+
limit,
|
|
164
166
|
})
|
|
165
|
-
return c.json(success(result
|
|
167
|
+
return c.json(success(result), 200)
|
|
166
168
|
})
|
|
167
169
|
.openapi(getRoute, async c => {
|
|
168
170
|
const { id } = c.req.valid('param')
|
|
@@ -200,3 +202,6 @@ export const orderRoutes = new OpenAPIHono()
|
|
|
200
202
|
if (!result) throw new NotFoundError('Order', id)
|
|
201
203
|
return c.json(success(result), 200)
|
|
202
204
|
})
|
|
205
|
+
|
|
206
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
207
|
+
export type OrdersApiType = typeof orderRoutes
|
|
@@ -65,3 +65,6 @@ const getOrdersRoute = createRoute({
|
|
|
65
65
|
export const ordersMockRoutes = new OpenAPIHono().openapi(getOrdersRoute, async c => {
|
|
66
66
|
return c.json({ success: true as const, data: MOCK_ORDERS })
|
|
67
67
|
})
|
|
68
|
+
|
|
69
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
70
|
+
export type OrdersMockApiType = typeof ordersMockRoutes
|
|
@@ -50,8 +50,13 @@ export async function seedOrdersIfEmpty(): Promise<void> {
|
|
|
50
50
|
export async function getOrders(filters?: {
|
|
51
51
|
status?: OrderStatus
|
|
52
52
|
customerName?: string
|
|
53
|
-
|
|
53
|
+
page?: number
|
|
54
|
+
limit?: number
|
|
55
|
+
}): Promise<{ orders: Order[]; total: number; page: number; limit: number }> {
|
|
54
56
|
const db = await getDb()
|
|
57
|
+
const page = filters?.page ?? 1
|
|
58
|
+
const limit = filters?.limit ?? 20
|
|
59
|
+
const offset = (page - 1) * limit
|
|
55
60
|
const conditions = []
|
|
56
61
|
|
|
57
62
|
if (filters?.status) {
|
|
@@ -66,7 +71,7 @@ export async function getOrders(filters?: {
|
|
|
66
71
|
)
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
const
|
|
74
|
+
const allRows =
|
|
70
75
|
conditions.length > 0
|
|
71
76
|
? await db
|
|
72
77
|
.select()
|
|
@@ -75,17 +80,24 @@ export async function getOrders(filters?: {
|
|
|
75
80
|
.orderBy(desc(orders.createdAt))
|
|
76
81
|
: await db.select().from(orders).orderBy(desc(orders.createdAt))
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const rows = allRows.slice(offset, offset + limit)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
orders: rows.map((row: OrderTable) => ({
|
|
87
|
+
id: `order-${row.id}`,
|
|
88
|
+
orderNo: row.orderNo,
|
|
89
|
+
customerName: row.customerName,
|
|
90
|
+
customerEmail: row.customerEmail,
|
|
91
|
+
productName: row.productName,
|
|
92
|
+
amount: row.amount,
|
|
93
|
+
status: row.status,
|
|
94
|
+
createdAt: toISOString(row.createdAt),
|
|
95
|
+
updatedAt: toISOString(row.updatedAt),
|
|
96
|
+
})),
|
|
97
|
+
total: allRows.length,
|
|
98
|
+
page,
|
|
99
|
+
limit,
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
export async function getOrderById(id: string): Promise<Order | null> {
|