@geekron/hono 0.1.0 → 0.1.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 (180) hide show
  1. package/dist/contract.d.ts +9 -1
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +14 -3
  4. package/dist/core/api/common.d.ts +2 -0
  5. package/dist/core/api/common.d.ts.map +1 -0
  6. package/dist/core/api/common.js +38 -0
  7. package/dist/core/api/inquiries.d.ts +2 -0
  8. package/dist/core/api/inquiries.d.ts.map +1 -0
  9. package/dist/core/api/inquiries.js +116 -0
  10. package/dist/core/api/inquiry.d.ts +2 -0
  11. package/dist/core/api/inquiry.d.ts.map +1 -0
  12. package/dist/core/api/inquiry.js +33 -0
  13. package/dist/core/api/visitor.d.ts +2 -0
  14. package/dist/core/api/visitor.d.ts.map +1 -0
  15. package/dist/core/api/visitor.js +152 -0
  16. package/dist/core/components/InquiryEmailTemplate.d.ts +30 -0
  17. package/dist/core/components/InquiryEmailTemplate.d.ts.map +1 -0
  18. package/dist/core/components/InquiryEmailTemplate.js +13 -0
  19. package/dist/core/components/Seo.d.ts +97 -0
  20. package/dist/core/components/Seo.d.ts.map +1 -0
  21. package/dist/core/components/Seo.js +365 -0
  22. package/dist/core/components/index.d.ts +3 -0
  23. package/dist/core/components/index.d.ts.map +1 -0
  24. package/dist/core/components/index.js +2 -0
  25. package/dist/core/index.d.ts +4 -0
  26. package/dist/core/index.d.ts.map +1 -0
  27. package/dist/core/index.js +3 -0
  28. package/dist/core/lib/constants/inquiry.d.ts +52 -0
  29. package/dist/core/lib/constants/inquiry.d.ts.map +1 -0
  30. package/dist/core/lib/constants/inquiry.js +51 -0
  31. package/dist/core/lib/constants/visitor.d.ts +29 -0
  32. package/dist/core/lib/constants/visitor.d.ts.map +1 -0
  33. package/dist/core/lib/constants/visitor.js +28 -0
  34. package/dist/core/lib/database/constants/database.d.ts +15 -0
  35. package/dist/core/lib/database/constants/database.d.ts.map +1 -0
  36. package/dist/core/lib/database/constants/database.js +14 -0
  37. package/dist/core/lib/database/database.service.d.ts +63 -0
  38. package/dist/core/lib/database/database.service.d.ts.map +1 -0
  39. package/dist/core/lib/database/database.service.js +108 -0
  40. package/dist/core/lib/database/index.d.ts +8 -0
  41. package/dist/core/lib/database/index.d.ts.map +1 -0
  42. package/dist/core/lib/database/index.js +7 -0
  43. package/dist/core/lib/database/managers/sqlite.d.ts +29 -0
  44. package/dist/core/lib/database/managers/sqlite.d.ts.map +1 -0
  45. package/dist/core/lib/database/managers/sqlite.js +55 -0
  46. package/dist/core/lib/database/managers/visitor-shard.d.ts +29 -0
  47. package/dist/core/lib/database/managers/visitor-shard.d.ts.map +1 -0
  48. package/dist/core/lib/database/managers/visitor-shard.js +83 -0
  49. package/dist/core/lib/database/repositories/inquiry.repository.d.ts +33 -0
  50. package/dist/core/lib/database/repositories/inquiry.repository.d.ts.map +1 -0
  51. package/dist/core/lib/database/repositories/inquiry.repository.js +173 -0
  52. package/dist/core/lib/database/repositories/visitor.repository.d.ts +51 -0
  53. package/dist/core/lib/database/repositories/visitor.repository.d.ts.map +1 -0
  54. package/dist/core/lib/database/repositories/visitor.repository.js +559 -0
  55. package/dist/core/lib/database/schemas/inquiry.schema.d.ts +323 -0
  56. package/dist/core/lib/database/schemas/inquiry.schema.d.ts.map +1 -0
  57. package/dist/core/lib/database/schemas/inquiry.schema.js +52 -0
  58. package/dist/core/lib/database/schemas/visitor.schema.d.ts +623 -0
  59. package/dist/core/lib/database/schemas/visitor.schema.d.ts.map +1 -0
  60. package/dist/core/lib/database/schemas/visitor.schema.js +162 -0
  61. package/dist/core/lib/services/captcha.service.d.ts +13 -0
  62. package/dist/core/lib/services/captcha.service.d.ts.map +1 -0
  63. package/dist/core/lib/services/captcha.service.js +40 -0
  64. package/dist/core/lib/services/geoip.service.d.ts +79 -0
  65. package/dist/core/lib/services/geoip.service.d.ts.map +1 -0
  66. package/dist/core/lib/services/geoip.service.js +144 -0
  67. package/dist/core/lib/services/index.d.ts +9 -0
  68. package/dist/core/lib/services/index.d.ts.map +1 -0
  69. package/dist/core/lib/services/index.js +8 -0
  70. package/dist/core/lib/services/inquiry.service.d.ts +56 -0
  71. package/dist/core/lib/services/inquiry.service.d.ts.map +1 -0
  72. package/dist/core/lib/services/inquiry.service.js +129 -0
  73. package/dist/core/lib/services/mailer.service.d.ts +23 -0
  74. package/dist/core/lib/services/mailer.service.d.ts.map +1 -0
  75. package/dist/core/lib/services/mailer.service.js +96 -0
  76. package/dist/core/lib/types/inquiry.d.ts +95 -0
  77. package/dist/core/lib/types/inquiry.d.ts.map +1 -0
  78. package/dist/core/lib/types/inquiry.js +1 -0
  79. package/dist/core/lib/types/visitor.d.ts +135 -0
  80. package/dist/core/lib/types/visitor.d.ts.map +1 -0
  81. package/dist/core/lib/types/visitor.js +1 -0
  82. package/dist/core/lib/utils/api.d.ts +25 -0
  83. package/dist/core/lib/utils/api.d.ts.map +1 -0
  84. package/dist/core/lib/utils/api.js +48 -0
  85. package/dist/core/lib/utils/request.d.ts +18 -0
  86. package/dist/core/lib/utils/request.d.ts.map +1 -0
  87. package/dist/core/lib/utils/request.js +25 -0
  88. package/dist/core/middlewares/api-guard.d.ts +39 -0
  89. package/dist/core/middlewares/api-guard.d.ts.map +1 -0
  90. package/dist/core/middlewares/api-guard.js +154 -0
  91. package/dist/core/middlewares/html-minifier.d.ts +55 -0
  92. package/dist/core/middlewares/html-minifier.d.ts.map +1 -0
  93. package/dist/core/middlewares/html-minifier.js +140 -0
  94. package/dist/core/middlewares/index.d.ts +5 -0
  95. package/dist/core/middlewares/index.d.ts.map +1 -0
  96. package/dist/core/middlewares/index.js +4 -0
  97. package/dist/core/middlewares/initializer.d.ts +11 -0
  98. package/dist/core/middlewares/initializer.d.ts.map +1 -0
  99. package/dist/core/middlewares/initializer.js +13 -0
  100. package/dist/core/middlewares/visitor-tracker.d.ts +85 -0
  101. package/dist/core/middlewares/visitor-tracker.d.ts.map +1 -0
  102. package/dist/core/middlewares/visitor-tracker.js +253 -0
  103. package/dist/core/template.d.ts +4 -0
  104. package/dist/core/template.d.ts.map +1 -0
  105. package/dist/core/template.js +7 -0
  106. package/dist/core/utils/formatDate.d.ts +3 -0
  107. package/dist/core/utils/formatDate.d.ts.map +1 -0
  108. package/dist/core/utils/formatDate.js +2 -0
  109. package/dist/core/utils/fullUrl.d.ts +2 -0
  110. package/dist/core/utils/fullUrl.d.ts.map +1 -0
  111. package/dist/core/utils/fullUrl.js +9 -0
  112. package/dist/core/utils/index.d.ts +6 -0
  113. package/dist/core/utils/index.d.ts.map +1 -0
  114. package/dist/core/utils/index.js +5 -0
  115. package/dist/core/utils/markdownify.d.ts +2 -0
  116. package/dist/core/utils/markdownify.d.ts.map +1 -0
  117. package/dist/core/utils/markdownify.js +4 -0
  118. package/dist/core/utils/params.d.ts +5 -0
  119. package/dist/core/utils/params.d.ts.map +1 -0
  120. package/dist/core/utils/params.js +14 -0
  121. package/dist/core/utils/routeUrl.d.ts +20 -0
  122. package/dist/core/utils/routeUrl.d.ts.map +1 -0
  123. package/dist/core/utils/routeUrl.js +111 -0
  124. package/dist/route/methods.d.ts +15 -0
  125. package/dist/route/methods.d.ts.map +1 -1
  126. package/dist/route/methods.js +45 -18
  127. package/dist/route/setup.d.ts +11 -12
  128. package/dist/route/setup.d.ts.map +1 -1
  129. package/dist/route/setup.js +26 -16
  130. package/dist/strapi/api/site.d.ts +2 -0
  131. package/dist/strapi/api/site.d.ts.map +1 -0
  132. package/dist/strapi/api/site.js +153 -0
  133. package/dist/strapi/cms/cms.js +1 -1
  134. package/dist/strapi/cms/common.d.ts.map +1 -1
  135. package/dist/strapi/cms/common.js +3 -1
  136. package/dist/strapi/cms/menu.d.ts +0 -9
  137. package/dist/strapi/cms/menu.d.ts.map +1 -1
  138. package/dist/strapi/cms/menu.js +3 -62
  139. package/dist/strapi/cms/setup.d.ts +2 -1
  140. package/dist/strapi/cms/setup.d.ts.map +1 -1
  141. package/dist/strapi/cms/setup.js +5 -0
  142. package/dist/strapi/cms/site.d.ts.map +1 -1
  143. package/dist/strapi/cms/site.js +3 -1
  144. package/dist/strapi/cms/translations.d.ts.map +1 -1
  145. package/dist/strapi/cms/translations.js +6 -2
  146. package/dist/strapi/i18n.d.ts +3 -0
  147. package/dist/strapi/i18n.d.ts.map +1 -0
  148. package/dist/strapi/i18n.js +27 -0
  149. package/dist/strapi/index.d.ts +1 -0
  150. package/dist/strapi/index.d.ts.map +1 -1
  151. package/dist/strapi/index.js +1 -0
  152. package/dist/strapi/interfaces/index.d.ts +1 -0
  153. package/dist/strapi/interfaces/index.d.ts.map +1 -1
  154. package/dist/strapi/interfaces/index.js +1 -0
  155. package/dist/strapi/interfaces/sitemap.d.ts +19 -0
  156. package/dist/strapi/interfaces/sitemap.d.ts.map +1 -0
  157. package/dist/strapi/interfaces/sitemap.js +1 -0
  158. package/dist/strapi/pages/sitemap/index.d.ts +2 -0
  159. package/dist/strapi/pages/sitemap/index.d.ts.map +1 -0
  160. package/dist/strapi/pages/sitemap/index.js +50 -0
  161. package/dist/strapi/pages/sitemap/index.xml +11 -0
  162. package/dist/strapi/pages/sitemap/list.d.ts +2 -0
  163. package/dist/strapi/pages/sitemap/list.d.ts.map +1 -0
  164. package/dist/strapi/pages/sitemap/list.js +123 -0
  165. package/dist/strapi/pages/sitemap/list.xml +28 -0
  166. package/dist/strapi/pages/sitemap/robots.d.ts +2 -0
  167. package/dist/strapi/pages/sitemap/robots.d.ts.map +1 -0
  168. package/dist/strapi/pages/sitemap/robots.js +19 -0
  169. package/dist/strapi/pages/sitemap/robots.txt +7 -0
  170. package/dist/strapi/pages/sitemap/style.d.ts +2 -0
  171. package/dist/strapi/pages/sitemap/style.d.ts.map +1 -0
  172. package/dist/strapi/pages/sitemap/style.js +17 -0
  173. package/dist/strapi/pages/sitemap/style.xsl +602 -0
  174. package/dist/strapi/utils/index.d.ts +1 -0
  175. package/dist/strapi/utils/index.d.ts.map +1 -1
  176. package/dist/strapi/utils/index.js +1 -0
  177. package/dist/strapi/utils/trans.d.ts +5 -0
  178. package/dist/strapi/utils/trans.d.ts.map +1 -0
  179. package/dist/strapi/utils/trans.js +7 -0
  180. package/package.json +20 -2
@@ -0,0 +1,559 @@
1
+ import dayjs from 'dayjs';
2
+ import { and, count, countDistinct, eq, gte, lt, sql } from 'drizzle-orm';
3
+ import { dailyPageStats, ensureVisitorSchema, generateSessionId, ipPageStats, pageViews, visitorSessions, } from '../../../../core/lib/database';
4
+ import { VisitorShard } from '../managers/visitor-shard';
5
+ /**
6
+ * 访客仓储:封装跨分片的读写与统计。
7
+ */
8
+ export class VisitorRepository {
9
+ constructor(manager) {
10
+ this.manager = manager;
11
+ this.shardManager = new VisitorShard(manager);
12
+ }
13
+ /** 初始化已有分片的表结构(幂等),若无分片则创建当前分片 */
14
+ async initShardSchemas() {
15
+ const shards = this.shardManager.getAllDbs();
16
+ for (const { path } of shards) {
17
+ const rawDb = this.manager.getRaw(path);
18
+ ensureVisitorSchema(rawDb);
19
+ }
20
+ }
21
+ /**
22
+ * 写入访客记录(事务),并在必要时触发分片旋转。
23
+ */
24
+ async insert(data) {
25
+ const db = this.shardManager.getCurrentDb();
26
+ const rawDb = this.manager.getRaw(this.shardManager.currentShardPath());
27
+ const shardPath = this.shardManager.currentShardPath();
28
+ const visitedAt = data.visitedAt
29
+ ? dayjs(data.visitedAt).toISOString()
30
+ : dayjs().toISOString();
31
+ const date = dayjs(visitedAt).format('YYYY-MM-DD');
32
+ const userAgent = data.userAgent ?? '';
33
+ const sessionId = generateSessionId(data.ip, userAgent);
34
+ // 开始事务
35
+ rawDb.run('BEGIN');
36
+ try {
37
+ // 1. 更新或创建访客会话
38
+ const existingSession = db
39
+ .select()
40
+ .from(visitorSessions)
41
+ .where(eq(visitorSessions.sessionId, sessionId))
42
+ .get();
43
+ if (existingSession) {
44
+ db.update(visitorSessions)
45
+ .set({
46
+ lastVisitAt: visitedAt,
47
+ visitCount: sql `${visitorSessions.visitCount} + 1`,
48
+ })
49
+ .where(eq(visitorSessions.sessionId, sessionId))
50
+ .run();
51
+ }
52
+ else {
53
+ db.insert(visitorSessions)
54
+ .values({
55
+ sessionId,
56
+ ip: data.ip,
57
+ userAgent: userAgent || null,
58
+ firstVisitAt: visitedAt,
59
+ lastVisitAt: visitedAt,
60
+ visitCount: 1,
61
+ })
62
+ .run();
63
+ }
64
+ // 2. 创建页面浏览记录
65
+ const pvResult = db
66
+ .insert(pageViews)
67
+ .values({
68
+ sessionId,
69
+ ip: data.ip,
70
+ page: data.page,
71
+ referrer: data.referrer ?? null,
72
+ visitedAt,
73
+ })
74
+ .returning({ id: pageViews.id });
75
+ const pvRows = pvResult.all();
76
+ const pvId = pvRows[0]?.id ?? 0;
77
+ // 3. 更新每日页面统计
78
+ const existingDailyStats = db
79
+ .select()
80
+ .from(dailyPageStats)
81
+ .where(and(eq(dailyPageStats.date, date), eq(dailyPageStats.page, data.page)))
82
+ .get();
83
+ if (existingDailyStats) {
84
+ db.update(dailyPageStats)
85
+ .set({
86
+ viewCount: sql `${dailyPageStats.viewCount} + 1`,
87
+ })
88
+ .where(and(eq(dailyPageStats.date, date), eq(dailyPageStats.page, data.page)))
89
+ .run();
90
+ const dayStart = `${date} 00:00:00`;
91
+ const dayEnd = `${date} 23:59:59.999`;
92
+ const todayViewCount = db
93
+ .select({ count: count() })
94
+ .from(pageViews)
95
+ .where(and(eq(pageViews.sessionId, sessionId), eq(pageViews.page, data.page), gte(pageViews.visitedAt, dayStart), sql `${pageViews.visitedAt} <= ${dayEnd}`))
96
+ .get();
97
+ if (todayViewCount && todayViewCount.count === 1) {
98
+ db.update(dailyPageStats)
99
+ .set({
100
+ uniqueVisitors: sql `${dailyPageStats.uniqueVisitors} + 1`,
101
+ })
102
+ .where(and(eq(dailyPageStats.date, date), eq(dailyPageStats.page, data.page)))
103
+ .run();
104
+ }
105
+ }
106
+ else {
107
+ db.insert(dailyPageStats)
108
+ .values({
109
+ date,
110
+ page: data.page,
111
+ viewCount: 1,
112
+ uniqueVisitors: 1,
113
+ })
114
+ .run();
115
+ }
116
+ // 4. 更新 IP 页面统计
117
+ const existingIpStats = db
118
+ .select()
119
+ .from(ipPageStats)
120
+ .where(and(eq(ipPageStats.ip, data.ip), eq(ipPageStats.page, data.page)))
121
+ .get();
122
+ if (existingIpStats) {
123
+ db.update(ipPageStats)
124
+ .set({
125
+ viewCount: sql `${ipPageStats.viewCount} + 1`,
126
+ lastVisitAt: visitedAt,
127
+ })
128
+ .where(and(eq(ipPageStats.ip, data.ip), eq(ipPageStats.page, data.page)))
129
+ .run();
130
+ }
131
+ else {
132
+ db.insert(ipPageStats)
133
+ .values({
134
+ ip: data.ip,
135
+ page: data.page,
136
+ viewCount: 1,
137
+ firstVisitAt: visitedAt,
138
+ lastVisitAt: visitedAt,
139
+ })
140
+ .run();
141
+ }
142
+ rawDb.run('COMMIT');
143
+ this.shardManager.rotateIfNeeded(shardPath);
144
+ return { id: pvId, shardPath };
145
+ }
146
+ catch (e) {
147
+ rawDb.run('ROLLBACK');
148
+ throw e;
149
+ }
150
+ }
151
+ /**
152
+ * 统计独立访客数(UV)
153
+ */
154
+ async getUVStats(filter = {}, byDate = false) {
155
+ const shards = this.shardManager.getAllDbs();
156
+ if (!byDate) {
157
+ const tasks = shards.map(({ db }) => {
158
+ let query = db
159
+ .select({ c: countDistinct(visitorSessions.sessionId) })
160
+ .from(visitorSessions)
161
+ .$dynamic();
162
+ const conditions = [];
163
+ if (filter.start) {
164
+ conditions.push(gte(visitorSessions.firstVisitAt, dayjs(filter.start).toISOString()));
165
+ }
166
+ if (filter.end) {
167
+ conditions.push(lt(visitorSessions.firstVisitAt, dayjs(filter.end).toISOString()));
168
+ }
169
+ if (filter.ip) {
170
+ conditions.push(eq(visitorSessions.ip, filter.ip));
171
+ }
172
+ if (conditions.length > 0) {
173
+ query = query.where(and(...conditions));
174
+ }
175
+ return query.get();
176
+ });
177
+ const results = await Promise.all(tasks);
178
+ const total = results.reduce((sum, r) => sum + (r?.c ?? 0), 0);
179
+ return { total };
180
+ }
181
+ else {
182
+ const tasks = shards.map(({ db }) => {
183
+ let query = db
184
+ .select({
185
+ date: sql `substr(${visitorSessions.firstVisitAt}, 1, 10)`,
186
+ c: countDistinct(visitorSessions.sessionId),
187
+ })
188
+ .from(visitorSessions)
189
+ .$dynamic();
190
+ const conditions = [];
191
+ if (filter.start) {
192
+ conditions.push(gte(visitorSessions.firstVisitAt, dayjs(filter.start).toISOString()));
193
+ }
194
+ if (filter.end) {
195
+ conditions.push(lt(visitorSessions.firstVisitAt, dayjs(filter.end).toISOString()));
196
+ }
197
+ if (filter.ip) {
198
+ conditions.push(eq(visitorSessions.ip, filter.ip));
199
+ }
200
+ if (conditions.length > 0) {
201
+ query = query.where(and(...conditions));
202
+ }
203
+ return query
204
+ .groupBy(sql `substr(${visitorSessions.firstVisitAt}, 1, 10)`)
205
+ .all();
206
+ });
207
+ const results = await Promise.all(tasks);
208
+ const merged = {};
209
+ for (const arr of results) {
210
+ for (const row of arr) {
211
+ const date = row.date;
212
+ const c = Number(row.c ?? 0);
213
+ merged[date] = (merged[date] ?? 0) + c;
214
+ }
215
+ }
216
+ const byDate = Object.entries(merged)
217
+ .map(([date, count]) => ({ date, count }))
218
+ .sort((a, b) => a.date.localeCompare(b.date));
219
+ const total = byDate.reduce((sum, item) => sum + item.count, 0);
220
+ return { total, byDate };
221
+ }
222
+ }
223
+ /**
224
+ * 统计页面总浏览量(PV)
225
+ */
226
+ async getPVStats(filter = {}, groupBy) {
227
+ const shards = this.shardManager.getAllDbs();
228
+ if (!groupBy) {
229
+ const tasks = shards.map(({ db }) => {
230
+ let query = db.select({ c: count() }).from(pageViews).$dynamic();
231
+ const conditions = [];
232
+ if (filter.start) {
233
+ conditions.push(gte(pageViews.visitedAt, dayjs(filter.start).toISOString()));
234
+ }
235
+ if (filter.end) {
236
+ conditions.push(lt(pageViews.visitedAt, dayjs(filter.end).toISOString()));
237
+ }
238
+ if (filter.ip) {
239
+ conditions.push(eq(pageViews.ip, filter.ip));
240
+ }
241
+ if (filter.page) {
242
+ conditions.push(eq(pageViews.page, filter.page));
243
+ }
244
+ if (conditions.length > 0) {
245
+ query = query.where(and(...conditions));
246
+ }
247
+ return query.get();
248
+ });
249
+ const results = await Promise.all(tasks);
250
+ const total = results.reduce((sum, r) => sum + (r?.c ?? 0), 0);
251
+ return { total };
252
+ }
253
+ else if (groupBy === 'page') {
254
+ const tasks = shards.map(({ db }) => {
255
+ let query = db
256
+ .select({
257
+ page: pageViews.page,
258
+ c: count(),
259
+ })
260
+ .from(pageViews)
261
+ .$dynamic();
262
+ const conditions = [];
263
+ if (filter.start) {
264
+ conditions.push(gte(pageViews.visitedAt, dayjs(filter.start).toISOString()));
265
+ }
266
+ if (filter.end) {
267
+ conditions.push(lt(pageViews.visitedAt, dayjs(filter.end).toISOString()));
268
+ }
269
+ if (filter.ip) {
270
+ conditions.push(eq(pageViews.ip, filter.ip));
271
+ }
272
+ if (filter.page) {
273
+ conditions.push(eq(pageViews.page, filter.page));
274
+ }
275
+ if (conditions.length > 0) {
276
+ query = query.where(and(...conditions));
277
+ }
278
+ return query.groupBy(pageViews.page).all();
279
+ });
280
+ const results = await Promise.all(tasks);
281
+ const merged = {};
282
+ for (const arr of results) {
283
+ for (const row of arr) {
284
+ const page = row.page;
285
+ const c = Number(row.c ?? 0);
286
+ merged[page] = (merged[page] ?? 0) + c;
287
+ }
288
+ }
289
+ const byPage = Object.entries(merged)
290
+ .map(([page, count]) => ({ page, count }))
291
+ .sort((a, b) => b.count - a.count);
292
+ const total = byPage.reduce((sum, item) => sum + item.count, 0);
293
+ return { total, byPage };
294
+ }
295
+ else {
296
+ const tasks = shards.map(({ db }) => {
297
+ let query = db
298
+ .select({
299
+ date: sql `substr(${pageViews.visitedAt}, 1, 10)`,
300
+ c: count(),
301
+ })
302
+ .from(pageViews)
303
+ .$dynamic();
304
+ const conditions = [];
305
+ if (filter.start) {
306
+ conditions.push(gte(pageViews.visitedAt, dayjs(filter.start).toISOString()));
307
+ }
308
+ if (filter.end) {
309
+ conditions.push(lt(pageViews.visitedAt, dayjs(filter.end).toISOString()));
310
+ }
311
+ if (filter.ip) {
312
+ conditions.push(eq(pageViews.ip, filter.ip));
313
+ }
314
+ if (filter.page) {
315
+ conditions.push(eq(pageViews.page, filter.page));
316
+ }
317
+ if (conditions.length > 0) {
318
+ query = query.where(and(...conditions));
319
+ }
320
+ return query.groupBy(sql `substr(${pageViews.visitedAt}, 1, 10)`).all();
321
+ });
322
+ const results = await Promise.all(tasks);
323
+ const merged = {};
324
+ for (const arr of results) {
325
+ for (const row of arr) {
326
+ const date = row.date;
327
+ const c = Number(row.c ?? 0);
328
+ merged[date] = (merged[date] ?? 0) + c;
329
+ }
330
+ }
331
+ const byDate = Object.entries(merged)
332
+ .map(([date, count]) => ({ date, count }))
333
+ .sort((a, b) => a.date.localeCompare(b.date));
334
+ const total = byDate.reduce((sum, item) => sum + item.count, 0);
335
+ return { total, byDate };
336
+ }
337
+ }
338
+ /**
339
+ * 统计每个页面每日的访问次数(从预聚合表查询)
340
+ */
341
+ async getDailyPageViewStats(filter = {}) {
342
+ const shards = this.shardManager.getAllDbs();
343
+ const tasks = shards.map(({ db }) => {
344
+ let query = db.select().from(dailyPageStats).$dynamic();
345
+ const conditions = [];
346
+ if (filter.start) {
347
+ conditions.push(gte(dailyPageStats.date, dayjs(filter.start).format('YYYY-MM-DD')));
348
+ }
349
+ if (filter.end) {
350
+ conditions.push(lt(dailyPageStats.date, dayjs(filter.end).format('YYYY-MM-DD')));
351
+ }
352
+ if (filter.page) {
353
+ conditions.push(eq(dailyPageStats.page, filter.page));
354
+ }
355
+ if (conditions.length > 0) {
356
+ query = query.where(and(...conditions));
357
+ }
358
+ return query.all();
359
+ });
360
+ const results = await Promise.all(tasks);
361
+ const merged = {};
362
+ for (const arr of results) {
363
+ for (const row of arr) {
364
+ const key = `${row.date}::${row.page}`;
365
+ if (!merged[key]) {
366
+ merged[key] = {
367
+ date: row.date,
368
+ page: row.page,
369
+ viewCount: 0,
370
+ uniqueVisitors: 0,
371
+ };
372
+ }
373
+ merged[key].viewCount += Number(row.viewCount ?? 0);
374
+ merged[key].uniqueVisitors += Number(row.uniqueVisitors ?? 0);
375
+ }
376
+ }
377
+ return Object.values(merged).sort((a, b) => {
378
+ const dateCompare = b.date.localeCompare(a.date);
379
+ if (dateCompare !== 0)
380
+ return dateCompare;
381
+ return b.viewCount - a.viewCount;
382
+ });
383
+ }
384
+ /**
385
+ * 统计基于 IP 的页面访问次数(从预聚合表查询)
386
+ */
387
+ async getIpPageViewStats(filter = {}) {
388
+ const shards = this.shardManager.getAllDbs();
389
+ const tasks = shards.map(({ db }) => {
390
+ let query = db.select().from(ipPageStats).$dynamic();
391
+ const conditions = [];
392
+ if (filter.ip) {
393
+ conditions.push(eq(ipPageStats.ip, filter.ip));
394
+ }
395
+ if (filter.page) {
396
+ conditions.push(eq(ipPageStats.page, filter.page));
397
+ }
398
+ if (conditions.length > 0) {
399
+ query = query.where(and(...conditions));
400
+ }
401
+ return query.all();
402
+ });
403
+ const results = await Promise.all(tasks);
404
+ const merged = {};
405
+ for (const arr of results) {
406
+ for (const row of arr) {
407
+ const key = `${row.ip}::${row.page}`;
408
+ if (!merged[key]) {
409
+ merged[key] = {
410
+ ip: row.ip,
411
+ page: row.page,
412
+ viewCount: 0,
413
+ firstVisitAt: row.firstVisitAt,
414
+ lastVisitAt: row.lastVisitAt,
415
+ };
416
+ }
417
+ merged[key].viewCount += Number(row.viewCount ?? 0);
418
+ if (row.firstVisitAt < merged[key].firstVisitAt) {
419
+ merged[key].firstVisitAt = row.firstVisitAt;
420
+ }
421
+ if (row.lastVisitAt > merged[key].lastVisitAt) {
422
+ merged[key].lastVisitAt = row.lastVisitAt;
423
+ }
424
+ }
425
+ }
426
+ return Object.values(merged).sort((a, b) => b.viewCount - a.viewCount);
427
+ }
428
+ /**
429
+ * 跨分片查询页面浏览记录
430
+ */
431
+ async findPageViews(filter = {}) {
432
+ const shards = this.shardManager.getAllDbs();
433
+ const tasks = shards.map(({ db }) => {
434
+ let query = db.select().from(pageViews).$dynamic();
435
+ const conditions = [];
436
+ if (filter.start) {
437
+ conditions.push(gte(pageViews.visitedAt, dayjs(filter.start).toISOString()));
438
+ }
439
+ if (filter.end) {
440
+ conditions.push(lt(pageViews.visitedAt, dayjs(filter.end).toISOString()));
441
+ }
442
+ if (filter.ip) {
443
+ conditions.push(eq(pageViews.ip, filter.ip));
444
+ }
445
+ if (filter.page) {
446
+ conditions.push(eq(pageViews.page, filter.page));
447
+ }
448
+ if (conditions.length > 0) {
449
+ query = query.where(and(...conditions));
450
+ }
451
+ return query.all();
452
+ });
453
+ const results = await Promise.all(tasks);
454
+ const rows = [].concat(...results);
455
+ const sorted = rows.sort((a, b) => (a.visitedAt < b.visitedAt ? 1 : -1));
456
+ const start = filter.offset ?? 0;
457
+ const end = filter.limit ? start + filter.limit : sorted.length;
458
+ const paginatedRows = sorted.slice(start, end);
459
+ return paginatedRows.map((row) => ({
460
+ id: row.id,
461
+ sessionId: row.sessionId,
462
+ ip: row.ip,
463
+ page: row.page,
464
+ referrer: row.referrer ?? null,
465
+ visitedAt: row.visitedAt,
466
+ }));
467
+ }
468
+ /**
469
+ * 跨分片查询访客会话
470
+ */
471
+ async findVisitorSessions(filter = {}) {
472
+ const shards = this.shardManager.getAllDbs();
473
+ const tasks = shards.map(({ db }) => {
474
+ let query = db.select().from(visitorSessions).$dynamic();
475
+ const conditions = [];
476
+ if (filter.start) {
477
+ conditions.push(gte(visitorSessions.firstVisitAt, dayjs(filter.start).toISOString()));
478
+ }
479
+ if (filter.end) {
480
+ conditions.push(lt(visitorSessions.firstVisitAt, dayjs(filter.end).toISOString()));
481
+ }
482
+ if (filter.ip) {
483
+ conditions.push(eq(visitorSessions.ip, filter.ip));
484
+ }
485
+ if (conditions.length > 0) {
486
+ query = query.where(and(...conditions));
487
+ }
488
+ return query.all();
489
+ });
490
+ const results = await Promise.all(tasks);
491
+ const rows = [].concat(...results);
492
+ const sorted = rows.sort((a, b) => (a.lastVisitAt < b.lastVisitAt ? 1 : -1));
493
+ const start = filter.offset ?? 0;
494
+ const end = filter.limit ? start + filter.limit : sorted.length;
495
+ const paginatedRows = sorted.slice(start, end);
496
+ return paginatedRows.map((row) => ({
497
+ id: row.id,
498
+ sessionId: row.sessionId,
499
+ ip: row.ip,
500
+ userAgent: row.userAgent ?? null,
501
+ firstVisitAt: row.firstVisitAt,
502
+ lastVisitAt: row.lastVisitAt,
503
+ visitCount: row.visitCount,
504
+ }));
505
+ }
506
+ /** 跨分片计数 */
507
+ async count(filter = {}) {
508
+ const shards = this.shardManager.getAllDbs();
509
+ const tasks = shards.map(({ db }) => {
510
+ let query = db.select({ c: count() }).from(pageViews).$dynamic();
511
+ const conditions = [];
512
+ if (filter.start) {
513
+ conditions.push(gte(pageViews.visitedAt, dayjs(filter.start).toISOString()));
514
+ }
515
+ if (filter.end) {
516
+ conditions.push(lt(pageViews.visitedAt, dayjs(filter.end).toISOString()));
517
+ }
518
+ if (filter.ip) {
519
+ conditions.push(eq(pageViews.ip, filter.ip));
520
+ }
521
+ if (filter.page) {
522
+ conditions.push(eq(pageViews.page, filter.page));
523
+ }
524
+ if (conditions.length > 0) {
525
+ query = query.where(and(...conditions));
526
+ }
527
+ return query.get();
528
+ });
529
+ const results = await Promise.all(tasks);
530
+ return results.reduce((sum, r) => sum + (r?.c ?? 0), 0);
531
+ }
532
+ /** 跨分片统计(向后兼容) */
533
+ async stats(by, filter = {}) {
534
+ if (by === 'day') {
535
+ const stats = await this.getPVStats(filter, 'date');
536
+ return (stats.byDate ?? []).map((item) => ({
537
+ key: item.date,
538
+ count: item.count,
539
+ }));
540
+ }
541
+ else if (by === 'page') {
542
+ const stats = await this.getPVStats(filter, 'page');
543
+ return (stats.byPage ?? []).map((item) => ({
544
+ key: item.page,
545
+ count: item.count,
546
+ }));
547
+ }
548
+ else {
549
+ const results = await this.getIpPageViewStats(filter);
550
+ const merged = {};
551
+ for (const item of results) {
552
+ merged[item.ip] = (merged[item.ip] ?? 0) + item.viewCount;
553
+ }
554
+ return Object.entries(merged)
555
+ .map(([key, count]) => ({ key, count }))
556
+ .sort((a, b) => b.count - a.count);
557
+ }
558
+ }
559
+ }