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.
Files changed (137) hide show
  1. package/dist/cli/index.js +432 -15
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +10 -13
  4. package/template/drizzle.config.ts +10 -4
  5. package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
  6. package/template/eslint-rules/no-cross-module-service-import.js +4 -0
  7. package/template/eslint-rules/no-merged-api-type-export.js +190 -0
  8. package/template/eslint.config.js +3 -0
  9. package/template/package.json +9 -6
  10. package/template/scripts/sync-agent-hooks.mjs +189 -0
  11. package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
  12. package/template/src/admin/pages/ContentPage.tsx +1 -1
  13. package/template/src/admin/pages/DisputesPage.tsx +1 -1
  14. package/template/src/admin/pages/OrdersPage.tsx +1 -1
  15. package/template/src/admin/pages/TicketsPage.tsx +1 -1
  16. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
  17. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
  18. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
  19. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
  20. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
  21. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
  22. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
  23. package/template/src/admin/services/apiClient.ts +2 -3
  24. package/template/src/cli/modules/content/index.ts +3 -3
  25. package/template/src/cli/modules/dispute/index.ts +3 -3
  26. package/template/src/cli/modules/ticket/index.ts +3 -3
  27. package/template/src/cli/modules/todo/index.ts +1 -1
  28. package/template/src/cli/rpc/client.ts +3 -4
  29. package/template/src/cli/rpc/index.ts +1 -1
  30. package/template/src/client/App.tsx +3 -39
  31. package/template/src/client/AppRoutes.tsx +53 -0
  32. package/template/src/client/entry-server.tsx +75 -0
  33. package/template/src/client/main.tsx +3 -1
  34. package/template/src/client/pages/SearchPage.tsx +1 -1
  35. package/template/src/client/services/apiClient.ts +12 -4
  36. package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
  37. package/template/src/client/stores/entry-stores.ts +36 -0
  38. package/template/src/client/stores/notificationStore.ts +4 -4
  39. package/template/src/client/stores/todoStore.ts +2 -2
  40. package/template/src/merchant/pages/DisputesPage.tsx +3 -3
  41. package/template/src/merchant/pages/OrdersPage.tsx +1 -1
  42. package/template/src/merchant/pages/ProductsPage.tsx +1 -1
  43. package/template/src/merchant/pages/SettingsPage.tsx +1 -1
  44. package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
  45. package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
  46. package/template/src/server/app.ts +6 -4
  47. package/template/src/server/core/__tests__/isr-cache-cf.test.ts +96 -0
  48. package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
  49. package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
  50. package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
  51. package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
  52. package/template/src/server/core/isr-cache.ts +61 -18
  53. package/template/src/server/core/isr-invalidation.ts +6 -12
  54. package/template/src/server/core/isr-registry.ts +104 -0
  55. package/template/src/server/core/isr-renderer.ts +75 -0
  56. package/template/src/server/db/schema/contents.ts +28 -20
  57. package/template/src/server/db/schema/disputes.ts +30 -22
  58. package/template/src/server/db/schema/notifications.ts +20 -14
  59. package/template/src/server/db/schema/orders.ts +23 -16
  60. package/template/src/server/db/schema/plugins.ts +56 -38
  61. package/template/src/server/db/schema/products.ts +24 -17
  62. package/template/src/server/db/schema/tickets.ts +44 -31
  63. package/template/src/server/db/schema/todos.ts +26 -18
  64. package/template/src/server/entries/cloudflare.ts +97 -21
  65. package/template/src/server/entries/node.ts +0 -2
  66. package/template/src/server/index.ts +0 -1
  67. package/template/src/server/isr-modules.ts +10 -0
  68. package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
  69. package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
  70. package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
  71. package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
  72. package/template/src/server/module-admin/services/admin-service.ts +57 -13
  73. package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
  74. package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
  75. package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
  76. package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
  77. package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
  78. package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
  79. package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
  80. package/template/src/server/module-content/isr.ts +63 -0
  81. package/template/src/server/module-content/routes/content-routes.ts +10 -10
  82. package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
  83. package/template/src/server/module-content/routes/topics-routes.ts +3 -0
  84. package/template/src/server/module-content/services/content-service.ts +23 -10
  85. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
  86. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
  87. package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
  88. package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
  89. package/template/src/server/module-file/routes/file-routes.ts +3 -0
  90. package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
  91. package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
  92. package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
  93. package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
  94. package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
  95. package/template/src/server/module-order/routes/cart-routes.ts +3 -0
  96. package/template/src/server/module-order/routes/order-routes.ts +9 -4
  97. package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
  98. package/template/src/server/module-order/services/order-service.ts +25 -13
  99. package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
  100. package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
  101. package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
  102. package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
  103. package/template/src/server/module-permission/routes/role-routes.ts +3 -0
  104. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
  105. package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
  106. package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
  107. package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
  108. package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
  109. package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
  110. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
  111. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
  112. package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
  113. package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
  114. package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
  115. package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
  116. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
  117. package/template/src/server/module-todos/isr.ts +41 -0
  118. package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
  119. package/template/src/server/module-todos/services/todo-service.ts +31 -10
  120. package/template/src/server/route-registry.ts +0 -4
  121. package/template/src/server/rpc-merge.ts +27 -0
  122. package/template/src/server/rpc-surface.ts +117 -0
  123. package/template/src/server/rpc-type-canary.ts +80 -0
  124. package/template/src/server/test-utils/test-client.ts +7 -9
  125. package/template/src/server/test-utils/test-isr-helper.ts +140 -0
  126. package/template/src/shared/modules/content/schemas.ts +14 -0
  127. package/template/src/shared/modules/dispute/schemas.ts +14 -0
  128. package/template/src/shared/modules/order/schemas.ts +9 -1
  129. package/template/src/shared/modules/ticket/schemas.ts +14 -0
  130. package/template/src/shared/modules/todos/index.ts +4 -0
  131. package/template/src/shared/modules/todos/schemas.ts +14 -0
  132. package/template/src/shared/schemas/index.ts +18 -0
  133. package/template/tsup.config.ts +48 -1
  134. package/template/vitest.config.ts +19 -3
  135. package/template/vitest.setup.ts +68 -5
  136. package/template/patches/typescript+5.9.3.patch +0 -24
  137. /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
- }): Promise<Content[]> {
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 rows =
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
- return rows.map(mapContentRow)
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(Array.isArray(data.data)).toBe(true)
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(Array.isArray(result)).toBe(true)
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, z } from '@hono/zod-openapi'
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
- DisputeListSchema,
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: z.object({
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(DisputeListSchema, 'List all disputes'),
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 { limit, offset } = c.req.valid('query')
121
- const result = await disputeService.getDisputes()
122
- return c.json(success(result.slice(offset, offset + limit)), 200)
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
- }): Promise<Dispute[]> {
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 rows =
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
- return rows.map(mapDisputeRow)
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> {
@@ -286,3 +286,6 @@ export const fileRoutes = new OpenAPIHono()
286
286
  200
287
287
  )
288
288
  })
289
+
290
+ /** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
291
+ export type FilesApiType = typeof fileRoutes
@@ -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 = `merchant-token-${merchant.id}-${Date.now()}`
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 (simplified - in production, query orders table)
125
- const totalOrders = 0 // TODO: Implement order stats
126
- const totalRevenue = 0 // TODO: Implement revenue stats
127
- const pendingOrders = 0 // TODO: Implement pending order count
128
- const thisMonthRevenue = 0 // TODO: Implement monthly revenue
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: 'password123',
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: 'password123',
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(Array.isArray(result)).toBe(true)
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
  })
@@ -101,3 +101,6 @@ export const cartRoutes = new OpenAPIHono()
101
101
  }
102
102
  return c.json({ success: true as const, data: { removedId: id } })
103
103
  })
104
+
105
+ /** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
106
+ export type CartApiType = typeof cartRoutes
@@ -9,7 +9,7 @@ import {
9
9
  OrderSchema,
10
10
  CreateOrderSchema,
11
11
  UpdateOrderSchema,
12
- OrderListSchema,
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(OrderListSchema, 'List all orders'),
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, limit, offset } = c.req.valid('query')
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.slice(offset, offset + limit)), 200)
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
- }): Promise<Order[]> {
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 rows =
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
- return rows.map((row: OrderTable) => ({
79
- id: `order-${row.id}`,
80
- orderNo: row.orderNo,
81
- customerName: row.customerName,
82
- customerEmail: row.customerEmail,
83
- productName: row.productName,
84
- amount: row.amount,
85
- status: row.status,
86
- createdAt: toISOString(row.createdAt),
87
- updatedAt: toISOString(row.updatedAt),
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> {