@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,162 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { index, integer, sqliteTable, text, unique, } from 'drizzle-orm/sqlite-core';
3
+ /**
4
+ * 表名定义
5
+ */
6
+ export const VISITOR_SESSIONS_TABLE = 'visitor_sessions'; // 访客会话表(UV)
7
+ export const PAGE_VIEWS_TABLE = 'page_views'; // 页面浏览表(PV)
8
+ export const DAILY_PAGE_STATS_TABLE = 'daily_page_stats'; // 每日页面统计表
9
+ export const IP_PAGE_STATS_TABLE = 'ip_page_stats'; // IP 页面统计表
10
+ /**
11
+ * Drizzle ORM 访客会话表定义
12
+ */
13
+ export const visitorSessions = sqliteTable(VISITOR_SESSIONS_TABLE, {
14
+ id: integer('id').primaryKey({ autoIncrement: true }),
15
+ sessionId: text('session_id', { length: 32 }).notNull().unique(),
16
+ ip: text('ip', { length: 64 }).notNull(),
17
+ userAgent: text('user_agent', { length: 512 }),
18
+ firstVisitAt: text('first_visit_at').notNull(),
19
+ lastVisitAt: text('last_visit_at').notNull(),
20
+ visitCount: integer('visit_count').notNull().default(1),
21
+ }, (table) => ({
22
+ sessionIdIdx: index('visitor_sessions_session_id_idx').on(table.sessionId),
23
+ ipIdx: index('visitor_sessions_ip_idx').on(table.ip),
24
+ firstVisitAtIdx: index('visitor_sessions_first_visit_at_idx').on(table.firstVisitAt),
25
+ }));
26
+ /**
27
+ * Drizzle ORM 页面浏览表定义
28
+ */
29
+ export const pageViews = sqliteTable(PAGE_VIEWS_TABLE, {
30
+ id: integer('id').primaryKey({ autoIncrement: true }),
31
+ sessionId: text('session_id', { length: 32 }).notNull(),
32
+ ip: text('ip', { length: 64 }).notNull(),
33
+ page: text('page', { length: 512 }).notNull(),
34
+ referrer: text('referrer', { length: 512 }),
35
+ visitedAt: text('visited_at').notNull(),
36
+ }, (table) => ({
37
+ sessionIdIdx: index('page_views_session_id_idx').on(table.sessionId),
38
+ ipIdx: index('page_views_ip_idx').on(table.ip),
39
+ pageIdx: index('page_views_page_idx').on(table.page),
40
+ visitedAtIdx: index('page_views_visited_at_idx').on(table.visitedAt),
41
+ ipPageIdx: index('page_views_ip_page_idx').on(table.ip, table.page),
42
+ }));
43
+ /**
44
+ * Drizzle ORM 每日页面统计表定义
45
+ */
46
+ export const dailyPageStats = sqliteTable(DAILY_PAGE_STATS_TABLE, {
47
+ id: integer('id').primaryKey({ autoIncrement: true }),
48
+ date: text('date', { length: 10 }).notNull(), // YYYY-MM-DD
49
+ page: text('page', { length: 512 }).notNull(),
50
+ viewCount: integer('view_count').notNull().default(0),
51
+ uniqueVisitors: integer('unique_visitors').notNull().default(0),
52
+ }, (table) => ({
53
+ datePageUnique: unique('daily_page_stats_date_page_unique').on(table.date, table.page),
54
+ dateIdx: index('daily_page_stats_date_idx').on(table.date),
55
+ pageIdx: index('daily_page_stats_page_idx').on(table.page),
56
+ }));
57
+ /**
58
+ * Drizzle ORM IP 页面统计表定义
59
+ */
60
+ export const ipPageStats = sqliteTable(IP_PAGE_STATS_TABLE, {
61
+ id: integer('id').primaryKey({ autoIncrement: true }),
62
+ ip: text('ip', { length: 64 }).notNull(),
63
+ page: text('page', { length: 512 }).notNull(),
64
+ viewCount: integer('view_count').notNull().default(0),
65
+ firstVisitAt: text('first_visit_at').notNull(),
66
+ lastVisitAt: text('last_visit_at').notNull(),
67
+ }, (table) => ({
68
+ ipPageUnique: unique('ip_page_stats_ip_page_unique').on(table.ip, table.page),
69
+ ipIdx: index('ip_page_stats_ip_idx').on(table.ip),
70
+ pageIdx: index('ip_page_stats_page_idx').on(table.page),
71
+ }));
72
+ /**
73
+ * 生成会话标识(基于 IP 和 User Agent)
74
+ */
75
+ export function generateSessionId(ip, userAgent) {
76
+ const hash = createHash('sha256');
77
+ hash.update(`${ip}::${userAgent}`);
78
+ return hash.digest('hex').substring(0, 32);
79
+ }
80
+ /**
81
+ * 初始化访客会话表(幂等)
82
+ */
83
+ export function ensureVisitorSessionSchema(db) {
84
+ db.run(`
85
+ CREATE TABLE IF NOT EXISTS ${VISITOR_SESSIONS_TABLE} (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ session_id TEXT NOT NULL UNIQUE,
88
+ ip TEXT NOT NULL,
89
+ user_agent TEXT,
90
+ first_visit_at TEXT NOT NULL,
91
+ last_visit_at TEXT NOT NULL,
92
+ visit_count INTEGER NOT NULL DEFAULT 1
93
+ )
94
+ `);
95
+ db.run(`CREATE INDEX IF NOT EXISTS visitor_sessions_session_id_idx ON ${VISITOR_SESSIONS_TABLE}(session_id)`);
96
+ db.run(`CREATE INDEX IF NOT EXISTS visitor_sessions_ip_idx ON ${VISITOR_SESSIONS_TABLE}(ip)`);
97
+ db.run(`CREATE INDEX IF NOT EXISTS visitor_sessions_first_visit_at_idx ON ${VISITOR_SESSIONS_TABLE}(first_visit_at)`);
98
+ }
99
+ /**
100
+ * 初始化页面浏览表(幂等)
101
+ */
102
+ export function ensurePageViewSchema(db) {
103
+ db.run(`
104
+ CREATE TABLE IF NOT EXISTS ${PAGE_VIEWS_TABLE} (
105
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
106
+ session_id TEXT NOT NULL,
107
+ ip TEXT NOT NULL,
108
+ page TEXT NOT NULL,
109
+ referrer TEXT,
110
+ visited_at TEXT NOT NULL
111
+ )
112
+ `);
113
+ db.run(`CREATE INDEX IF NOT EXISTS page_views_session_id_idx ON ${PAGE_VIEWS_TABLE}(session_id)`);
114
+ db.run(`CREATE INDEX IF NOT EXISTS page_views_ip_idx ON ${PAGE_VIEWS_TABLE}(ip)`);
115
+ db.run(`CREATE INDEX IF NOT EXISTS page_views_page_idx ON ${PAGE_VIEWS_TABLE}(page)`);
116
+ db.run(`CREATE INDEX IF NOT EXISTS page_views_visited_at_idx ON ${PAGE_VIEWS_TABLE}(visited_at)`);
117
+ db.run(`CREATE INDEX IF NOT EXISTS page_views_ip_page_idx ON ${PAGE_VIEWS_TABLE}(ip, page)`);
118
+ }
119
+ /**
120
+ * 初始化每日页面统计表(幂等)
121
+ */
122
+ export function ensureDailyPageStatsSchema(db) {
123
+ db.run(`
124
+ CREATE TABLE IF NOT EXISTS ${DAILY_PAGE_STATS_TABLE} (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ date TEXT NOT NULL,
127
+ page TEXT NOT NULL,
128
+ view_count INTEGER NOT NULL DEFAULT 0,
129
+ unique_visitors INTEGER NOT NULL DEFAULT 0,
130
+ UNIQUE(date, page)
131
+ )
132
+ `);
133
+ db.run(`CREATE INDEX IF NOT EXISTS daily_page_stats_date_idx ON ${DAILY_PAGE_STATS_TABLE}(date)`);
134
+ db.run(`CREATE INDEX IF NOT EXISTS daily_page_stats_page_idx ON ${DAILY_PAGE_STATS_TABLE}(page)`);
135
+ }
136
+ /**
137
+ * 初始化 IP 页面统计表(幂等)
138
+ */
139
+ export function ensureIpPageStatsSchema(db) {
140
+ db.run(`
141
+ CREATE TABLE IF NOT EXISTS ${IP_PAGE_STATS_TABLE} (
142
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
143
+ ip TEXT NOT NULL,
144
+ page TEXT NOT NULL,
145
+ view_count INTEGER NOT NULL DEFAULT 0,
146
+ first_visit_at TEXT NOT NULL,
147
+ last_visit_at TEXT NOT NULL,
148
+ UNIQUE(ip, page)
149
+ )
150
+ `);
151
+ db.run(`CREATE INDEX IF NOT EXISTS ip_page_stats_ip_idx ON ${IP_PAGE_STATS_TABLE}(ip)`);
152
+ db.run(`CREATE INDEX IF NOT EXISTS ip_page_stats_page_idx ON ${IP_PAGE_STATS_TABLE}(page)`);
153
+ }
154
+ /**
155
+ * 初始化所有访客相关表(幂等)
156
+ */
157
+ export function ensureVisitorSchema(db) {
158
+ ensureVisitorSessionSchema(db);
159
+ ensurePageViewSchema(db);
160
+ ensureDailyPageStatsSchema(db);
161
+ ensureIpPageStatsSchema(db);
162
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * hCaptcha 验证服务
3
+ * 提供人机验证相关功能
4
+ */
5
+ /**
6
+ * 验证 hCaptcha token
7
+ * @param token - hCaptcha 返回的 token
8
+ * @param sitekey - 站点密钥
9
+ * @param clientIp - 客户端 IP 地址(可选)
10
+ * @returns 验证是否成功
11
+ */
12
+ export declare function verifyHCaptcha(token: string, sitekey: string, clientIp?: string): Promise<boolean>;
13
+ //# sourceMappingURL=captcha.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha.service.d.ts","sourceRoot":"","sources":["../../../../src/core/lib/services/captcha.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;GAMG;AACH,wBAAsB,cAAc,CACnC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CA+BlB"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * hCaptcha 验证服务
3
+ * 提供人机验证相关功能
4
+ */
5
+ const HCAPTCHA_VERIFY_URL = 'https://hcaptcha.com/siteverify';
6
+ /**
7
+ * 验证 hCaptcha token
8
+ * @param token - hCaptcha 返回的 token
9
+ * @param sitekey - 站点密钥
10
+ * @param clientIp - 客户端 IP 地址(可选)
11
+ * @returns 验证是否成功
12
+ */
13
+ export async function verifyHCaptcha(token, sitekey, clientIp) {
14
+ const HCAPTCHA_SECRET = import.meta.env.HCAPTCHA_SECRET_KEY || '';
15
+ // 开发环境跳过验证(如果没有配置 secret key)
16
+ if (!HCAPTCHA_SECRET || HCAPTCHA_SECRET === '') {
17
+ console.warn('[hCaptcha] No secret key configured, skipping verification in development');
18
+ return true;
19
+ }
20
+ const formData = new URLSearchParams();
21
+ formData.append('secret', HCAPTCHA_SECRET);
22
+ formData.append('sitekey', sitekey);
23
+ formData.append('response', token);
24
+ if (clientIp)
25
+ formData.append('remoteip', clientIp);
26
+ try {
27
+ const response = await fetch(HCAPTCHA_VERIFY_URL, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
30
+ body: formData.toString(),
31
+ });
32
+ const data = await response.json();
33
+ console.log('[hCaptcha] Verification response:', data);
34
+ return data.success === true;
35
+ }
36
+ catch (error) {
37
+ console.error('[hCaptcha] Verification failed:', error);
38
+ return false;
39
+ }
40
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * IP 地理位置信息解析服务
3
+ * 使用 MaxMind GeoLite2 City 数据库解析 IP 地址的地理位置信息
4
+ */
5
+ /**
6
+ * GeoIP 信息接口
7
+ */
8
+ export interface GeoInfo {
9
+ /** 国家代码 (ISO 3166-1 alpha-2) */
10
+ countryCode?: string;
11
+ /** 国家名称 */
12
+ country?: string;
13
+ /** 城市名称 */
14
+ city?: string;
15
+ /** 纬度 */
16
+ latitude?: number;
17
+ /** 经度 */
18
+ longitude?: number;
19
+ /** 时区 */
20
+ timezone?: string;
21
+ /** 邮政编码 */
22
+ postalCode?: string;
23
+ /** 大陆/洲 */
24
+ continent?: string;
25
+ /** 省份/州 */
26
+ subdivision?: string;
27
+ }
28
+ /**
29
+ * GeoIP 服务类
30
+ */
31
+ declare class GeoIPService {
32
+ private reader;
33
+ private dbPath;
34
+ private initialized;
35
+ private initPromise;
36
+ constructor();
37
+ /**
38
+ * 初始化 GeoIP Reader
39
+ */
40
+ private initialize;
41
+ /**
42
+ * 确保服务已初始化
43
+ */
44
+ private ensureInitialized;
45
+ /**
46
+ * 查询 IP 地址的地理位置信息
47
+ * @param ip - IP 地址
48
+ * @returns GeoInfo 对象,如果查询失败则返回空对象
49
+ */
50
+ lookup(ip: string): Promise<GeoInfo>;
51
+ /**
52
+ * 批量查询 IP 地址
53
+ * @param ips - IP 地址数组
54
+ * @returns GeoInfo 对象数组
55
+ */
56
+ batchLookup(ips: string[]): Promise<GeoInfo[]>;
57
+ /**
58
+ * 关闭 GeoIP Reader
59
+ */
60
+ close(): Promise<void>;
61
+ }
62
+ /**
63
+ * 获取 GeoIP 服务实例(单例模式)
64
+ */
65
+ export declare function getGeoIPService(): GeoIPService;
66
+ /**
67
+ * 便捷函数:查询 IP 地址的地理位置信息
68
+ * @param ip - IP 地址
69
+ * @returns GeoInfo 对象
70
+ */
71
+ export declare function lookupIP(ip: string): Promise<GeoInfo>;
72
+ /**
73
+ * 便捷函数:批量查询 IP 地址
74
+ * @param ips - IP 地址数组
75
+ * @returns GeoInfo 对象数组
76
+ */
77
+ export declare function batchLookupIPs(ips: string[]): Promise<GeoInfo[]>;
78
+ export {};
79
+ //# sourceMappingURL=geoip.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geoip.service.d.ts","sourceRoot":"","sources":["../../../../src/core/lib/services/geoip.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS;IACT,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS;IACT,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS;IACT,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW;IACX,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,cAAM,YAAY;IACjB,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,WAAW,CAA6B;;IAUhD;;OAEG;YACW,UAAU;IAyBxB;;OAEG;YACW,iBAAiB;IAM/B;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2C1C;;;;OAIG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAIpD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAS5B;AAOD;;GAEG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAK9C;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3D;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAGtE"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * IP 地理位置信息解析服务
3
+ * 使用 MaxMind GeoLite2 City 数据库解析 IP 地址的地理位置信息
4
+ */
5
+ import path from 'node:path';
6
+ import * as maxmind from 'maxmind';
7
+ /**
8
+ * GeoIP 服务类
9
+ */
10
+ class GeoIPService {
11
+ constructor() {
12
+ this.reader = null;
13
+ this.initialized = false;
14
+ this.initPromise = null;
15
+ // 数据库文件路径
16
+ this.dbPath = path.resolve(process.cwd(), 'src/core/lib/geo/geolite2-city-ipv4.mmdb');
17
+ }
18
+ /**
19
+ * 初始化 GeoIP Reader
20
+ */
21
+ async initialize() {
22
+ if (this.initialized) {
23
+ return;
24
+ }
25
+ // 如果正在初始化,等待初始化完成
26
+ if (this.initPromise) {
27
+ return this.initPromise;
28
+ }
29
+ this.initPromise = (async () => {
30
+ try {
31
+ console.log(`[GeoIP Service] Loading database from: ${this.dbPath}`);
32
+ this.reader = await maxmind.open(this.dbPath);
33
+ this.initialized = true;
34
+ console.log('[GeoIP Service] Database loaded successfully');
35
+ }
36
+ catch (error) {
37
+ console.error('[GeoIP Service] Failed to load database:', error);
38
+ throw new Error(`Failed to initialize GeoIP service: ${error}`);
39
+ }
40
+ })();
41
+ return this.initPromise;
42
+ }
43
+ /**
44
+ * 确保服务已初始化
45
+ */
46
+ async ensureInitialized() {
47
+ if (!this.initialized) {
48
+ await this.initialize();
49
+ }
50
+ }
51
+ /**
52
+ * 查询 IP 地址的地理位置信息
53
+ * @param ip - IP 地址
54
+ * @returns GeoInfo 对象,如果查询失败则返回空对象
55
+ */
56
+ async lookup(ip) {
57
+ try {
58
+ // 如果 IP 为空或无效,返回空对象
59
+ if (!ip || ip === 'unknown' || ip === '::1' || ip === '127.0.0.1') {
60
+ return {};
61
+ }
62
+ // 确保服务已初始化
63
+ await this.ensureInitialized();
64
+ if (!this.reader) {
65
+ console.warn('[GeoIP Service] Reader not initialized');
66
+ return {};
67
+ }
68
+ // 查询 IP 地址
69
+ const response = this.reader.get(ip);
70
+ // 如果没有查询结果,返回空对象
71
+ if (!response) {
72
+ return {};
73
+ }
74
+ // 提取地理信息(适配自定义的 city ipv4 数据库格式)
75
+ const geoInfo = {
76
+ countryCode: response.country_code,
77
+ country: response.country_code, // 这个数据库只有国家代码,没有完整名称
78
+ city: response.city || undefined,
79
+ latitude: response.latitude,
80
+ longitude: response.longitude,
81
+ timezone: response.timezone,
82
+ postalCode: response.postcode || undefined,
83
+ subdivision: response.state1 || response.state2 || undefined,
84
+ };
85
+ return geoInfo;
86
+ }
87
+ catch (error) {
88
+ // 查询失败时记录错误但不抛出异常
89
+ console.error(`[GeoIP Service] Failed to lookup IP ${ip}:`, error);
90
+ return {};
91
+ }
92
+ }
93
+ /**
94
+ * 批量查询 IP 地址
95
+ * @param ips - IP 地址数组
96
+ * @returns GeoInfo 对象数组
97
+ */
98
+ async batchLookup(ips) {
99
+ return Promise.all(ips.map((ip) => this.lookup(ip)));
100
+ }
101
+ /**
102
+ * 关闭 GeoIP Reader
103
+ */
104
+ async close() {
105
+ if (this.reader) {
106
+ // @maxmind/geoip2-node 不需要显式关闭
107
+ this.reader = null;
108
+ this.initialized = false;
109
+ this.initPromise = null;
110
+ console.log('[GeoIP Service] Reader closed');
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * 全局 GeoIP 服务实例
116
+ */
117
+ let geoIPServiceInstance = null;
118
+ /**
119
+ * 获取 GeoIP 服务实例(单例模式)
120
+ */
121
+ export function getGeoIPService() {
122
+ if (!geoIPServiceInstance) {
123
+ geoIPServiceInstance = new GeoIPService();
124
+ }
125
+ return geoIPServiceInstance;
126
+ }
127
+ /**
128
+ * 便捷函数:查询 IP 地址的地理位置信息
129
+ * @param ip - IP 地址
130
+ * @returns GeoInfo 对象
131
+ */
132
+ export async function lookupIP(ip) {
133
+ const service = getGeoIPService();
134
+ return service.lookup(ip);
135
+ }
136
+ /**
137
+ * 便捷函数:批量查询 IP 地址
138
+ * @param ips - IP 地址数组
139
+ * @returns GeoInfo 对象数组
140
+ */
141
+ export async function batchLookupIPs(ips) {
142
+ const service = getGeoIPService();
143
+ return service.batchLookup(ips);
144
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 服务模块导出
3
+ * 统一导出所有服务模块,方便导入使用
4
+ */
5
+ export * from './captcha.service';
6
+ export * from './geoip.service';
7
+ export * from './inquiry.service';
8
+ export * from './mailer.service';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/lib/services/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 服务模块导出
3
+ * 统一导出所有服务模块,方便导入使用
4
+ */
5
+ export * from './captcha.service';
6
+ export * from './geoip.service';
7
+ export * from './inquiry.service';
8
+ export * from './mailer.service';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 询盘服务
3
+ * 提供询盘提交的核心业务逻辑
4
+ */
5
+ import type { InquiryCreate } from '../../../core/lib/database/schemas/inquiry.schema';
6
+ /**
7
+ * 询盘表单数据接口
8
+ */
9
+ export interface InquiryFormData {
10
+ email: string;
11
+ first_name: string;
12
+ last_name: string;
13
+ message: string;
14
+ phone?: string;
15
+ hcaptcha_token: string;
16
+ hcaptcha_sitekey: string;
17
+ referer?: string;
18
+ [key: string]: any;
19
+ }
20
+ /**
21
+ * 询盘验证结果
22
+ */
23
+ export interface InquiryValidationResult {
24
+ valid: boolean;
25
+ error?: string;
26
+ errorCode?: string;
27
+ }
28
+ /**
29
+ * 询盘提交上下文
30
+ */
31
+ export interface InquirySubmitContext {
32
+ userAgent?: string;
33
+ clientIp?: string;
34
+ referer?: string;
35
+ }
36
+ /**
37
+ * 验证询盘表单必填字段
38
+ */
39
+ export declare function validateInquiryFields(data: Partial<InquiryFormData>): InquiryValidationResult;
40
+ /**
41
+ * 验证 hCaptcha 参数
42
+ */
43
+ export declare function validateCaptchaParams(data: Partial<InquiryFormData>): InquiryValidationResult;
44
+ /**
45
+ * 转换表单数据为数据库实体
46
+ */
47
+ export declare function transformInquiryData(formData: InquiryFormData, context: InquirySubmitContext): Promise<InquiryCreate>;
48
+ /**
49
+ * 提交询盘(完整流程)
50
+ * @param formData - 表单数据
51
+ * @param context - 提交上下文
52
+ * @returns 询盘 ID
53
+ * @throws 验证失败或数据库错误时抛出异常
54
+ */
55
+ export declare function submitInquiry(formData: InquiryFormData, context: InquirySubmitContext): Promise<number>;
56
+ //# sourceMappingURL=inquiry.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inquiry.service.d.ts","sourceRoot":"","sources":["../../../../src/core/lib/services/inquiry.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4CAA4C,CAAA;AAK/E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,GAC5B,uBAAuB,CAYzB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACpC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,GAC5B,uBAAuB,CAYzB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACzC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA+CxB;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAClC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC,MAAM,CAAC,CA4DjB"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 询盘服务
3
+ * 提供询盘提交的核心业务逻辑
4
+ */
5
+ import dayjs from 'dayjs';
6
+ import { getDatabase } from '../../../core/lib/database/database.service';
7
+ import { verifyHCaptcha } from './captcha.service';
8
+ import { lookupIP } from './geoip.service';
9
+ import { sendInquiryEmailAsync } from './mailer.service';
10
+ /**
11
+ * 验证询盘表单必填字段
12
+ */
13
+ export function validateInquiryFields(data) {
14
+ const { email, first_name, last_name, message } = data;
15
+ if (!email || !first_name || !last_name || !message) {
16
+ return {
17
+ valid: false,
18
+ error: 'Missing required fields',
19
+ errorCode: 'MISSING_REQUIRED_FIELDS',
20
+ };
21
+ }
22
+ return { valid: true };
23
+ }
24
+ /**
25
+ * 验证 hCaptcha 参数
26
+ */
27
+ export function validateCaptchaParams(data) {
28
+ const { hcaptcha_token, hcaptcha_sitekey } = data;
29
+ if (!hcaptcha_token || !hcaptcha_sitekey) {
30
+ return {
31
+ valid: false,
32
+ error: 'Captcha params is missing',
33
+ errorCode: 'MISSING_CAPTCHA_PARAMS',
34
+ };
35
+ }
36
+ return { valid: true };
37
+ }
38
+ /**
39
+ * 转换表单数据为数据库实体
40
+ */
41
+ export async function transformInquiryData(formData, context) {
42
+ const { first_name, last_name, email, phone = '', message, referer: formReferer, } = formData;
43
+ // 提取额外数据
44
+ const data = {};
45
+ // 过滤掉系统字段
46
+ const systemFields = [
47
+ 'hcaptcha_token',
48
+ 'hcaptcha_sitekey',
49
+ 'first_name',
50
+ 'last_name',
51
+ 'email',
52
+ 'phone',
53
+ 'message',
54
+ 'referer',
55
+ ];
56
+ for (const [key, value] of Object.entries(formData)) {
57
+ if (!systemFields.includes(key)) {
58
+ data[key] = value;
59
+ }
60
+ }
61
+ // 解析 IP 地理位置信息
62
+ const clientIp = String(context.clientIp || '');
63
+ const geoInfo = clientIp ? await lookupIP(clientIp) : {};
64
+ return {
65
+ firstName: String(first_name || ''),
66
+ lastName: String(last_name || ''),
67
+ email: String(email || ''),
68
+ phone: String(phone || ''),
69
+ message: String(message || ''),
70
+ userAgent: String(context.userAgent || ''),
71
+ ip: clientIp,
72
+ data,
73
+ referer: String(context.referer || formReferer || ''),
74
+ geo: geoInfo,
75
+ };
76
+ }
77
+ /**
78
+ * 提交询盘(完整流程)
79
+ * @param formData - 表单数据
80
+ * @param context - 提交上下文
81
+ * @returns 询盘 ID
82
+ * @throws 验证失败或数据库错误时抛出异常
83
+ */
84
+ export async function submitInquiry(formData, context) {
85
+ // 1. 验证必填字段
86
+ const fieldsValidation = validateInquiryFields(formData);
87
+ if (!fieldsValidation.valid) {
88
+ throw new Error(fieldsValidation.error);
89
+ }
90
+ // 2. 验证 hCaptcha 参数
91
+ const captchaParamsValidation = validateCaptchaParams(formData);
92
+ if (!captchaParamsValidation.valid) {
93
+ throw new Error(captchaParamsValidation.error);
94
+ }
95
+ // 3. 验证 hCaptcha
96
+ const isValidCaptcha = await verifyHCaptcha(formData.hcaptcha_token, formData.hcaptcha_sitekey, context.clientIp);
97
+ if (!isValidCaptcha) {
98
+ throw new Error('Captcha verification failed');
99
+ }
100
+ // 4. 转换数据并保存到数据库
101
+ const inquiryData = await transformInquiryData(formData, context);
102
+ const db = await getDatabase();
103
+ const id = await db.addInquiry(inquiryData);
104
+ // 5. 异步发送邮件通知(非阻塞)
105
+ try {
106
+ const emailData = {
107
+ id,
108
+ firstName: inquiryData.firstName,
109
+ lastName: inquiryData.lastName,
110
+ email: inquiryData.email,
111
+ phone: inquiryData.phone,
112
+ message: inquiryData.message,
113
+ referer: inquiryData.referer,
114
+ ip: inquiryData.ip,
115
+ userAgent: inquiryData.userAgent,
116
+ geo: JSON.stringify(inquiryData.geo || {}),
117
+ data: inquiryData.data || {},
118
+ createdAt: dayjs(inquiryData.createdAt).format('YYYY-MM-DD HH:mm:ss'),
119
+ };
120
+ // 异步发送,不阻塞主流程
121
+ sendInquiryEmailAsync(emailData);
122
+ console.log('[Inquiry Service] Email notification queued for inquiry ID:', id);
123
+ }
124
+ catch (emailError) {
125
+ // 邮件发送失败不影响询盘提交
126
+ console.error('[Inquiry Service] Failed to queue email notification:', emailError);
127
+ }
128
+ return id;
129
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 邮件发送服务
3
+ * 使用 nodemailer 发送邮件
4
+ */
5
+ import type { InquiryEmailValues } from '../../../core/components';
6
+ /**
7
+ * 发送询盘邮件
8
+ * @param inquiryValues - 询盘数据
9
+ * @returns Promise<boolean> - 发送成功返回 true,失败返回 false
10
+ */
11
+ export declare function sendInquiryEmail(inquiryValues: InquiryEmailValues): Promise<boolean>;
12
+ /**
13
+ * 异步发送询盘邮件(非阻塞)
14
+ * 使用 Promise 但不等待结果,避免阻塞主流程
15
+ * @param inquiryValue - 询盘数据
16
+ */
17
+ export declare function sendInquiryEmailAsync(inquiryValue: InquiryEmailValues): void;
18
+ /**
19
+ * 验证邮件服务连接
20
+ * @returns Promise<boolean> - 连接成功返回 true
21
+ */
22
+ export declare function verifyMailerConnection(): Promise<boolean>;
23
+ //# sourceMappingURL=mailer.service.d.ts.map