@windrun-huaiin/diaomao 2.3.6 → 2.5.0
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/.env.local.txt +20 -9
- package/messages/en.json +0 -1
- package/package.json +3 -3
- package/patches/fumadocs-ui@15.3.3.patch +66 -23
- package/src/mdx/blog/async-architecture.mdx +179 -0
- package/src/mdx/blog/index.mdx +14 -3
- package/src/mdx/blog/ioc.mdx +8 -2
- package/src/mdx/blog/paid-system-design.mdx +1979 -0
package/.env.local.txt
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 域名(必须配置)
|
|
2
2
|
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
|
3
|
+
# 测试(线上需要删除)
|
|
4
|
+
EXAMPLE_TEST_X=sk-dev-7C021EA0-386B-4908-BFDD-3ACC55B2BD6F
|
|
3
5
|
|
|
6
|
+
# Github在菜单上的筛选按钮链接配置(可选)
|
|
4
7
|
NEXT_PUBLIC_GITHUB=https://github.com/PowerZCY/next-ai-build/
|
|
8
|
+
# Blog页面的EditOnGithub按钮的链接base-url配置(可选, 若开放编辑则必须配置)
|
|
5
9
|
NEXT_PUBLIC_GITHUB_BASE_URL=https://github.com/PowerZCY/next-ai-build/blob/main/apps/diaomao
|
|
10
|
+
# R2上的图片下载代理服务器配置(可选, 如果需要下载图片则必须配置, 否则会出现跨域报错)
|
|
6
11
|
NEXT_PUBLIC_STYLE_CDN_PROXY_URL=https://r2-explorer-template.zcy777et.workers.dev/proxy
|
|
7
12
|
|
|
8
|
-
#
|
|
13
|
+
# 网站icon图标统一颜色(可选), 紫色
|
|
9
14
|
NEXT_PUBLIC_STYLE_ICON_COLOR=text-purple-500
|
|
15
|
+
# 网站svg图标统一颜色(可选), 紫色
|
|
10
16
|
NEXT_PUBLIC_STYLE_SVG_ICON_COLOR=#AC62FD
|
|
17
|
+
# 网站svg图标大小适配(可选)
|
|
11
18
|
NEXT_PUBLIC_STYLE_SVG_ICON_SIZE=18
|
|
12
19
|
|
|
20
|
+
# 网站Banner展示开关, 默认展示
|
|
13
21
|
NEXT_PUBLIC_STYLE_SHOW_BANNER=true
|
|
22
|
+
# 网站Clerk页面Banner展示开关(可选), 默认不展示
|
|
14
23
|
NEXT_PUBLIC_STYLE_CLERK_PAGE_BANNER=false
|
|
24
|
+
# 网站Clerk登录与注册功能展示的方式(可选), true是以弹窗展示; false则是以页面展示, 默认是true
|
|
15
25
|
NEXT_PUBLIC_STYLE_CLERK_AUTH_IN_MODAL=false
|
|
16
|
-
|
|
26
|
+
# MDX页面中mermaid图是否开启图片水印(可选), 默认开启
|
|
17
27
|
NEXT_PUBLIC_STYLE_WATERMARK_ENABLED=true
|
|
28
|
+
# MDX页面中mermaid图的水印txt文字
|
|
18
29
|
NEXT_PUBLIC_STYLE_WATERMARK_TEXT=Windrun·Huaiin
|
|
19
30
|
|
|
20
|
-
# GoogleID
|
|
31
|
+
# GoogleID分析ID(线上必须配置)
|
|
21
32
|
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=
|
|
22
|
-
# MicrosoftID
|
|
33
|
+
# MicrosoftID分析ID(线上必须配置)
|
|
23
34
|
NEXT_PUBLIC_MICROSOFT_CLARITY_ID=
|
|
24
35
|
|
|
25
36
|
# Only-Use-In-Server, !!DO NOT USE IN CLIENT!!
|
|
37
|
+
# Clerk组件的日志调试开关(可选)
|
|
26
38
|
CLERK_DEBUG=false
|
|
39
|
+
# Clerk组件的公钥, 在Clerk后台大盘配置里, 接入Clerk后线上必须配置
|
|
27
40
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
|
41
|
+
# Clerk组件的私钥, 在Clerk后台大盘配置里, 接入Clerk后线上必须配置
|
|
28
42
|
CLERK_SECRET_KEY=
|
|
29
43
|
|
|
30
|
-
#
|
|
44
|
+
# Clerk组件登录/注册/邀请白名单, url配置(可选)
|
|
31
45
|
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
|
32
46
|
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
|
|
33
|
-
# signupurl
|
|
34
47
|
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
|
35
48
|
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
|
|
36
|
-
|
|
37
|
-
# waitlist-url
|
|
38
49
|
NEXT_PUBLIC_CLERK_WAITLIST_URL=/waitlist
|
package/messages/en.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"home": {
|
|
3
3
|
"title": "Diaomao",
|
|
4
|
-
"slug": "Rethink | Redefine | Rebuild",
|
|
5
4
|
"banner": "Rethink | Redefine | Rebuild",
|
|
6
5
|
"webTitle": "Diaomao Directory - Showcase Beautiful Images & Tips",
|
|
7
6
|
"webDescription": "Discover stunning AI-generated images and learn expert tips for creating beautiful visuals with Diaomao's powerful text-to-image technology.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windrun-huaiin/diaomao",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"@radix-ui/react-slot": "^1.2.2",
|
|
26
26
|
"@tailwindcss/typography": "latest",
|
|
27
27
|
"@types/mdx": "^2.0.13",
|
|
28
|
-
"@windrun-huaiin/base-ui": "^6.0.
|
|
28
|
+
"@windrun-huaiin/base-ui": "^6.0.2",
|
|
29
29
|
"@windrun-huaiin/lib": "^6.2.0",
|
|
30
|
-
"@windrun-huaiin/third-ui": "^5.13.
|
|
30
|
+
"@windrun-huaiin/third-ui": "^5.13.4",
|
|
31
31
|
"autoprefixer": "^10.4.21",
|
|
32
32
|
"class-variance-authority": "^0.7.1",
|
|
33
33
|
"clsx": "^2.1.1",
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
diff --git a/dist/components/layout/toc-clerk.d.ts.map b/dist/components/layout/toc-clerk.d.ts.map
|
|
2
|
-
index 30dfe1ce45819999661e37005f3c0238a596b678..
|
|
2
|
+
index 30dfe1ce45819999661e37005f3c0238a596b678..6b9c00aef82311612b7e549c419b9534c01a03a7 100644
|
|
3
3
|
--- a/dist/components/layout/toc-clerk.d.ts.map
|
|
4
4
|
+++ b/dist/components/layout/toc-clerk.d.ts.map
|
|
5
5
|
@@ -1 +1 @@
|
|
6
6
|
-{"version":3,"file":"toc-clerk.d.ts","sourceRoot":"","sources":["../../../src/components/layout/toc-clerk.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAOxD,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,2CA2FxE"}
|
|
7
7
|
|
|
8
|
-
+{"version":3,"file":"toc-clerk.d.ts","sourceRoot":"","sources":["../../../src/components/layout/toc-clerk.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
8
|
+
+{"version":3,"file":"toc-clerk.d.ts","sourceRoot":"","sources":["../../../src/components/layout/toc-clerk.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAuDxD,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE,2CAyFxE"}
|
|
9
9
|
|
|
10
10
|
diff --git a/dist/components/layout/toc-clerk.js b/dist/components/layout/toc-clerk.js
|
|
11
|
-
index 7c128748acf238d7fd63a446876f35d5a0679fed..
|
|
11
|
+
index 7c128748acf238d7fd63a446876f35d5a0679fed..d90e60ef967bc3e5f82ffb87f0ff6624a9d54e46 100644
|
|
12
12
|
--- a/dist/components/layout/toc-clerk.js
|
|
13
13
|
+++ b/dist/components/layout/toc-clerk.js
|
|
14
|
-
@@ -1,10 +1,
|
|
14
|
+
@@ -1,10 +1,55 @@
|
|
15
15
|
'use client';
|
|
16
16
|
-import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
17
|
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -34,18 +34,41 @@ index 7c128748acf238d7fd63a446876f35d5a0679fed..58bd8c8effdf362d62acd20d2910b610
|
|
|
34
34
|
+ // e.g., if depth >=3, line is at 10px from the start of the content box of TOCItem
|
|
35
35
|
+ return depth >= 3 ? 10 : 0;
|
|
36
36
|
+}
|
|
37
|
-
+//
|
|
38
|
-
+function
|
|
39
|
-
+ const
|
|
40
|
-
+ if (
|
|
41
|
-
+ return
|
|
37
|
+
+// Fallback: extract leading digits from URL like "#12-title"
|
|
38
|
+
+function _getDigitsFromUrl(url) {
|
|
39
|
+
+ const m = /^#(\d+)-/.exec(url);
|
|
40
|
+
+ if (!m)
|
|
41
|
+
+ return null;
|
|
42
|
+
+ const n = Number.parseInt(m[1], 10);
|
|
43
|
+
+ return Number.isNaN(n) ? null : n;
|
|
44
|
+
+}
|
|
45
|
+
+// (Deprecated) URL-based step detection and mapping removed in favor of title-based rules
|
|
46
|
+
+// Extract step info from the ORIGINAL title for h3-only rendering.
|
|
47
|
+
+// Rules:
|
|
48
|
+
+// - Title must match: number(.number)* <space> <non-empty text>
|
|
49
|
+
+// - Display step is the last numeric segment, clamped to 0..19
|
|
50
|
+
+function getStepInfoFromTitle(title) {
|
|
51
|
+
+ const trimmed = title.trim();
|
|
52
|
+
+ // Trailing dot is optional: e.g., "1 ", "1. ", "1.1 ", "1.1. "
|
|
53
|
+
+ const match = trimmed.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
|
|
54
|
+
+ if (!match)
|
|
55
|
+
+ return { isStep: false, displayStep: null, content: null };
|
|
56
|
+
+ const content = (match[2] ?? '').trim();
|
|
57
|
+
+ if (content.length === 0)
|
|
58
|
+
+ return { isStep: false, displayStep: null, content: null };
|
|
59
|
+
+ const numericPart = match[1].replace(/\.$/, '');
|
|
60
|
+
+ const parts = numericPart.split('.').map((p) => Number.parseInt(p, 10));
|
|
61
|
+
+ const lastPart = parts.at(-1);
|
|
62
|
+
+ if (lastPart == null || Number.isNaN(lastPart)) {
|
|
63
|
+
+ return { isStep: false, displayStep: null, content: null };
|
|
42
64
|
+ }
|
|
43
|
-
+
|
|
65
|
+
+ const clamped = Math.max(0, Math.min(19, lastPart));
|
|
66
|
+
+ return { isStep: true, displayStep: clamped, content };
|
|
44
67
|
+}
|
|
45
68
|
export default function ClerkTOCItems({ items }) {
|
|
46
69
|
const containerRef = useRef(null);
|
|
47
70
|
const [svg, setSvg] = useState();
|
|
48
|
-
@@ -18,21 +
|
|
71
|
+
@@ -18,21 +63,25 @@ export default function ClerkTOCItems({ items }) {
|
|
49
72
|
let w = 0, h = 0;
|
|
50
73
|
const d = [];
|
|
51
74
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -77,7 +100,7 @@ index 7c128748acf238d7fd63a446876f35d5a0679fed..58bd8c8effdf362d62acd20d2910b610
|
|
|
77
100
|
height: h,
|
|
78
101
|
});
|
|
79
102
|
}
|
|
80
|
-
@@ -45,29 +
|
|
103
|
+
@@ -45,29 +94,51 @@ export default function ClerkTOCItems({ items }) {
|
|
81
104
|
}, [items]);
|
|
82
105
|
if (items.length === 0)
|
|
83
106
|
return _jsx(TocItemsEmpty, {});
|
|
@@ -89,7 +112,9 @@ index 7c128748acf238d7fd63a446876f35d5a0679fed..58bd8c8effdf362d62acd20d2910b610
|
|
|
89
112
|
- // Inline SVG
|
|
90
113
|
- encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`)}")`,
|
|
91
114
|
- }, children: _jsx(TocThumb, { containerRef: containerRef, className: "mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all" }) })) : null, _jsx("div", { className: "flex flex-col", ref: containerRef, children: items.map((item, i) => (_jsx(TOCItem, { item: item, upper: items[i - 1]?.depth, lower: items[i + 1]?.depth }, item.url))) })] }));
|
|
92
|
-
-}
|
|
115
|
+
+ maskImage: `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" /></svg>`)}")`,
|
|
116
|
+
+ }, children: _jsx(TocThumb, { containerRef: containerRef, className: "bg-fd-primary transition-all duration-500 ease-in-out" }) })) : null, _jsx("div", { className: "flex flex-col", ref: containerRef, children: items.map((item, i) => (_jsx(EnhancedClerkTOCItemInternal, { item: item, upperDepth: items[i - 1]?.depth, lowerDepth: items[i + 1]?.depth }, item.url))) })] }));
|
|
117
|
+
}
|
|
93
118
|
-function getItemOffset(depth) {
|
|
94
119
|
- if (depth <= 2)
|
|
95
120
|
- return 14;
|
|
@@ -99,13 +124,35 @@ index 7c128748acf238d7fd63a446876f35d5a0679fed..58bd8c8effdf362d62acd20d2910b610
|
|
|
99
124
|
-}
|
|
100
125
|
-function getLineOffset(depth) {
|
|
101
126
|
- return depth >= 3 ? 10 : 0;
|
|
102
|
-
|
|
103
|
-
+ }, children: _jsx(TocThumb, { containerRef: containerRef, className: "bg-fd-primary transition-all duration-500 ease-in-out" }) })) : null, _jsx("div", { className: "flex flex-col", ref: containerRef, children: items.map((item, i) => (_jsx(EnhancedClerkTOCItemInternal, { item: item, upperDepth: items[i - 1]?.depth, lowerDepth: items[i + 1]?.depth }, item.url))) })] }));
|
|
104
|
-
}
|
|
127
|
+
-}
|
|
105
128
|
-function TOCItem({ item, upper = item.depth, lower = item.depth, }) {
|
|
106
129
|
- const offset = getLineOffset(item.depth), upperOffset = getLineOffset(upper), lowerOffset = getLineOffset(lower);
|
|
107
130
|
+function EnhancedClerkTOCItemInternal({ item, upperDepth = item.depth, lowerDepth = item.depth, }) {
|
|
108
|
-
+
|
|
131
|
+
+ // New rule: only h3 and based on ORIGINAL title pattern
|
|
132
|
+
+ const isH3 = item.depth === 3;
|
|
133
|
+
+ const rawTitle = typeof item.title === 'string' ? item.title : '';
|
|
134
|
+
+ const { isStep, displayStep, content } = getStepInfoFromTitle(rawTitle);
|
|
135
|
+
+ let stepNumber = isH3 && isStep ? String(displayStep) : null;
|
|
136
|
+
+ let resolvedContent = item.title;
|
|
137
|
+
+ if (isH3 && isStep) {
|
|
138
|
+
+ resolvedContent = content ?? item.title;
|
|
139
|
+
+ }
|
|
140
|
+
+ // Fallback to URL-based digits if title-based detection fails unexpectedly
|
|
141
|
+
+ if (isH3 && !stepNumber) {
|
|
142
|
+
+ const urlNum = _getDigitsFromUrl(item.url);
|
|
143
|
+
+ if (urlNum != null) {
|
|
144
|
+
+ const clamped = Math.max(0, Math.min(19, urlNum));
|
|
145
|
+
+ stepNumber = String(clamped);
|
|
146
|
+
+ // Try to strip numeric prefix from title even if title-based regex failed
|
|
147
|
+
+ if (typeof rawTitle === 'string') {
|
|
148
|
+
+ const m = rawTitle.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
|
|
149
|
+
+ if (m && m[2]) {
|
|
150
|
+
+ resolvedContent = m[2];
|
|
151
|
+
+ }
|
|
152
|
+
+ }
|
|
153
|
+
+ }
|
|
154
|
+
+ }
|
|
155
|
+
+ const shouldRenderCircle = isH3 && stepNumber !== null;
|
|
109
156
|
+ const lineOffsetWithinItem = getLineOffset(item.depth);
|
|
110
157
|
+ const upperLineOffsetWithinItem = getLineOffset(upperDepth);
|
|
111
158
|
+ const lowerLineOffsetWithinItem = getLineOffset(lowerDepth);
|
|
@@ -119,13 +166,9 @@ index 7c128748acf238d7fd63a446876f35d5a0679fed..58bd8c8effdf362d62acd20d2910b610
|
|
|
119
166
|
+ }, className: "prose group relative py-1.5 text-sm text-fd-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-fd-primary", children: [lineOffsetWithinItem !== upperLineOffsetWithinItem ? (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", className: "absolute -top-1.5 size-4 rtl:-scale-x-100 pointer-events-none", style: { insetInlineStart: Math.min(lineOffsetWithinItem, upperLineOffsetWithinItem), zIndex: 1 }, children: _jsx("line", { x1: upperLineOffsetWithinItem - Math.min(lineOffsetWithinItem, upperLineOffsetWithinItem), y1: "0", x2: lineOffsetWithinItem - Math.min(lineOffsetWithinItem, upperLineOffsetWithinItem), y2: "12", className: cn('stroke-fd-foreground/10', 'group-data-[active=true]:stroke-fd-primary'), strokeWidth: "1", strokeLinecap: "round" }) })) : null, _jsx("div", { className: cn('absolute inset-y-0 w-px pointer-events-none', 'bg-fd-foreground/10 group-data-[active=true]:bg-fd-primary', lineOffsetWithinItem !== upperLineOffsetWithinItem && 'top-1.5', lineOffsetWithinItem !== lowerLineOffsetWithinItem && 'bottom-1.5'), style: {
|
|
120
167
|
+ insetInlineStart: lineOffsetWithinItem,
|
|
121
168
|
+ zIndex: 1,
|
|
122
|
-
+ } }),
|
|
169
|
+
+ } }), shouldRenderCircle && stepNumber && (_jsx("span", { className: cn('absolute z-10 flex size-[14px] rounded-full justify-center items-center', 'bg-black text-white dark:bg-white dark:text-black', 'group-data-[active=true]:bg-fd-primary group-data-[active=true]:text-white dark:group-data-[active=true]:text-black', 'font-medium text-xs'), style: {
|
|
123
170
|
+ left: lineOffsetWithinItem - CIRCLE_RADIUS_PX,
|
|
124
171
|
+ top: '50%',
|
|
125
172
|
+ transform: 'translateY(-50%)',
|
|
126
|
-
+ }, children: stepNumber })), _jsx("span", { style: {
|
|
127
|
-
+ position: 'relative',
|
|
128
|
-
+ zIndex: 1,
|
|
129
|
-
+ // marginLeft: isStep ? lineOffsetWithinItem : undefined,
|
|
130
|
-
+ }, children: item.title })] }));
|
|
173
|
+
+ }, children: stepNumber })), _jsx("span", { style: { position: 'relative', zIndex: 1 }, children: resolvedContent })] }));
|
|
131
174
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Next.js 异步处理架构升级方案
|
|
3
|
+
description: NextJS全栈技术中间件之异步处理
|
|
4
|
+
date: 2025-07-31
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 项目背景
|
|
9
|
+
- **环境**: Next.js 后端服务部署在 Vercel, 无服务器架构, 需处理异步任务(如用户状态更新、数据清理、订单检查)。
|
|
10
|
+
- **目标**: 从轻量级事件驱动逐步升级到高性能分布式消息队列, 适应项目规模增长。
|
|
11
|
+
- **约束**: Vercel 函数超时(免费层 10s, Pro 层 300s), 不支持持久化进程, 需外部服务或无服务器方案。
|
|
12
|
+
|
|
13
|
+
## 升级路径
|
|
14
|
+
1. **Node.js EventEmitter**: 轻量级事件驱动, 适合初期快速开发。
|
|
15
|
+
2. **Redis + Bull**: 引入任务队列, 支持持久化和重试, 适合中小型项目。
|
|
16
|
+
3. **Cloudflare Queues**: 全球分布式消息队列, 低延迟, 适合高并发。
|
|
17
|
+
4. **Vercel Queues**: 原生无服务器队列, 简化集成, 适合 Vercel 生态。
|
|
18
|
+
5. **Apache Pulsar**: 分布式流处理, 适合大规模、复杂场景。
|
|
19
|
+
|
|
20
|
+
## 各阶段方案
|
|
21
|
+
|
|
22
|
+
### 1. Node.js EventEmitter
|
|
23
|
+
- **描述**: 使用 Node.js 内置 `EventEmitter` 在内存中处理事件, API 路由触发异步任务。
|
|
24
|
+
- **部署**: Vercel API 路由内实现, 任务处理在同一函数调用中完成。
|
|
25
|
+
|
|
26
|
+
<Mermaid
|
|
27
|
+
title="EventEmitter时序图"
|
|
28
|
+
chart={`
|
|
29
|
+
sequenceDiagram
|
|
30
|
+
actor Client
|
|
31
|
+
participant API as Vercel API Route
|
|
32
|
+
participant EE as EventEmitter
|
|
33
|
+
Client->>API: POST /api/update-user
|
|
34
|
+
API->>EE: emit('updateUserStatus', { userId })
|
|
35
|
+
EE->>EE: Process task (e.g., update DB)
|
|
36
|
+
API-->>Client: 200 OK ('Event triggered')
|
|
37
|
+
|
|
38
|
+
`}/>
|
|
39
|
+
|
|
40
|
+
- **优缺点**:
|
|
41
|
+
- **优点**: 简单, 无外部依赖, 实时性高。
|
|
42
|
+
- **缺点**: 无持久化, Vercel 无状态限制长任务, 扩展性差。
|
|
43
|
+
- **升级原因**: 任务量增加需持久化、重试机制。
|
|
44
|
+
|
|
45
|
+
### 2. Redis Bull Queue
|
|
46
|
+
- **描述**: 使用 Redis(Upstash Redis)+ Bull 实现任务队列, API 路由入队, 外部 Worker 消费。
|
|
47
|
+
- **部署**:
|
|
48
|
+
- Vercel: API 路由推送任务。
|
|
49
|
+
- 外部: Worker 部署在 Railway/Dokku/AWS EC2。
|
|
50
|
+
|
|
51
|
+
<Mermaid
|
|
52
|
+
title="Redis·Bull时序图"
|
|
53
|
+
chart={`
|
|
54
|
+
sequenceDiagram
|
|
55
|
+
actor Client
|
|
56
|
+
participant API as Vercel API Route
|
|
57
|
+
participant Redis as Redis (Bull)
|
|
58
|
+
participant Worker as External Worker
|
|
59
|
+
participant DB as Database
|
|
60
|
+
Client->>API: POST /api/queue-task
|
|
61
|
+
API->>Redis: Add task
|
|
62
|
+
API-->>Client: 200 OK ('Task queued')
|
|
63
|
+
Redis->>Worker: Poll task
|
|
64
|
+
Worker->>DB: Execute task
|
|
65
|
+
Worker->>Redis: Mark complete
|
|
66
|
+
`}/>
|
|
67
|
+
|
|
68
|
+
- **优缺点**:
|
|
69
|
+
- **优点**: 支持持久化(RDB/AOF)、重试, 易集成。
|
|
70
|
+
- **缺点**: 持久化较弱, Worker 需独立部署, 高并发需 Redis 集群。
|
|
71
|
+
- **升级原因**: 需全球分布式、低延迟、强持久化。
|
|
72
|
+
|
|
73
|
+
### 3. Cloudflare Queues
|
|
74
|
+
- **描述**: Cloudflare Queues 提供全球分布式消息队列, Vercel API 路由通过 HTTP 调用 Cloudflare Worker 入队, Worker 消费任务。
|
|
75
|
+
- **部署**:
|
|
76
|
+
- Vercel: API 路由发送任务到 Cloudflare Worker。
|
|
77
|
+
- Cloudflare: Workers 实现生产者和消费者, 绑定队列。
|
|
78
|
+
|
|
79
|
+
<Mermaid
|
|
80
|
+
title="Cloudflare Queues时序图"
|
|
81
|
+
chart={`
|
|
82
|
+
sequenceDiagram
|
|
83
|
+
actor Client
|
|
84
|
+
participant API as Vercel API Route
|
|
85
|
+
participant Worker as Cloudflare Worker
|
|
86
|
+
participant Queue as Cloudflare Queue
|
|
87
|
+
participant Consumer as Cloudflare Consumer Worker
|
|
88
|
+
participant DB as Database
|
|
89
|
+
Client->>API: POST /api/queue-task
|
|
90
|
+
API->>Worker: POST / (task data)
|
|
91
|
+
Worker->>Queue: Send task
|
|
92
|
+
Worker-->>API: 200 OK
|
|
93
|
+
API-->>Client: 200 OK ('Task queued')
|
|
94
|
+
Queue->>Consumer: Poll task
|
|
95
|
+
Consumer->>DB: Update data
|
|
96
|
+
Consumer->>Queue: Acknowledge
|
|
97
|
+
`}/>
|
|
98
|
+
|
|
99
|
+
- **优缺点**:
|
|
100
|
+
- **优点**: 全球分布式, 低延迟, 强持久化(4 天保留), 支持死信队列。
|
|
101
|
+
- **缺点**: 跨平台集成复杂, 消费者需 Cloudflare Workers。
|
|
102
|
+
- **升级原因**: 需 Vercel 原生集成, 简化架构。
|
|
103
|
+
|
|
104
|
+
### 4. Vercel Queues
|
|
105
|
+
- **描述**: Vercel Queues(Limited Beta)提供无服务器消息队列, API 路由入队, Vercel 函数消费。
|
|
106
|
+
- **部署**: 全流程在 Vercel 环境, 需 Beta 访问权限, 替代方案为 Upstash QStash。
|
|
107
|
+
- **时序图**:
|
|
108
|
+
|
|
109
|
+
<Mermaid
|
|
110
|
+
title="Vercel Queues时序图"
|
|
111
|
+
chart={`
|
|
112
|
+
sequenceDiagram
|
|
113
|
+
actor Client
|
|
114
|
+
participant API as Vercel API Route
|
|
115
|
+
participant Queue as Vercel Queue
|
|
116
|
+
participant Consumer as Vercel Consumer Function
|
|
117
|
+
participant DB as Database
|
|
118
|
+
Client->>API: POST /api/queue-task
|
|
119
|
+
API->>Queue: Enqueue task
|
|
120
|
+
API-->>Client: 200 OK ('Task queued')
|
|
121
|
+
Queue->>Consumer: Poll task
|
|
122
|
+
Consumer->>DB: Update data
|
|
123
|
+
Consumer->>Queue: Acknowledge
|
|
124
|
+
`}/>
|
|
125
|
+
|
|
126
|
+
- **优缺点**:
|
|
127
|
+
- **优点**: Vercel 原生, 配置简单, 自动扩展。
|
|
128
|
+
- **缺点**: Beta 阶段, 功能未知, 函数超时限制。
|
|
129
|
+
- **升级原因**: 需多租户、流处理支持。
|
|
130
|
+
|
|
131
|
+
### 5. Apache Pulsar
|
|
132
|
+
- **描述**: Pulsar 提供分布式消息队列和流处理, API 路由入队, 消费者运行在外部服务器。
|
|
133
|
+
- **部署**:
|
|
134
|
+
- Vercel: API 路由通过 `pulsar-client` 推送任务。
|
|
135
|
+
- 外部: 消费者部署在 AWS EC2/Kubernetes 或托管服务(DataStax)。
|
|
136
|
+
- **时序图**:
|
|
137
|
+
|
|
138
|
+
<Mermaid
|
|
139
|
+
title="Pulsar时序图"
|
|
140
|
+
chart={`
|
|
141
|
+
sequenceDiagram
|
|
142
|
+
actor Client
|
|
143
|
+
participant API as Vercel API Route
|
|
144
|
+
participant Broker as Pulsar Broker
|
|
145
|
+
participant Consumer as External Consumer
|
|
146
|
+
participant DB as Database
|
|
147
|
+
Client->>API: POST /api/queue-task
|
|
148
|
+
API->>Broker: Send task
|
|
149
|
+
API-->>Client: 200 OK ('Task queued')
|
|
150
|
+
Broker->>Consumer: Deliver task
|
|
151
|
+
Consumer->>DB: Update data
|
|
152
|
+
Consumer->>Broker: Acknowledge
|
|
153
|
+
`}/>
|
|
154
|
+
|
|
155
|
+
- **优缺点**:
|
|
156
|
+
- **优点**: 支持多租户、流处理, 持久化强(BookKeeper), 死信主题。
|
|
157
|
+
- **缺点**: 部署复杂, Node.js 客户端需 C++ 环境, 运维成本高。
|
|
158
|
+
- **升级原因**: 满足大规模、复杂场景。
|
|
159
|
+
|
|
160
|
+
## 升级路径分析
|
|
161
|
+
- **阶段 1 -> 2**: EventEmitter 无持久化, 升级到 Redis + Bull 提供队列和重试。
|
|
162
|
+
- **阶段 2 -> 3**: Redis 持久化弱, Cloudflare Queues 提供全球分布式和强持久化。
|
|
163
|
+
- **阶段 3 -> 4**: 跨平台复杂, Vercel Queues 原生集成更简单。
|
|
164
|
+
- **阶段 4 -> 5**: Vercel Queues 功能有限, Pulsar 支持大规模分布式需求。
|
|
165
|
+
|
|
166
|
+
## 注意事项
|
|
167
|
+
- **Vercel 限制**: 无状态函数, 需外部 Worker(Redis、Pulsar)或云服务(Cloudflare、Vercel Queues)。
|
|
168
|
+
- **跨平台集成**: Cloudflare Queues 需 HTTP 桥接, 确保 CORS 和安全(API Token/OIDC)。
|
|
169
|
+
- **成本**:
|
|
170
|
+
- Redis (Upstash): 按使用量(免费层 1 万命令/天)。
|
|
171
|
+
- Cloudflare Queues: $1.20/百万条消息。
|
|
172
|
+
- Vercel Queues: 未知(Beta)。
|
|
173
|
+
- Pulsar: 自托管高成本, 托管按使用量。
|
|
174
|
+
- **监控**: 使用 Vercel Logs、Cloudflare Analytics、Sentry 跟踪任务。
|
|
175
|
+
|
|
176
|
+
## 推荐
|
|
177
|
+
- **中小型项目**: Upstash QStash(代替 Vercel Queues)或 Cloudflare Queues, 简单低成本。
|
|
178
|
+
- **大规模项目**: Pulsar, 优先托管服务(DataStax)降低运维负担。
|
|
179
|
+
- **Cloudflare 集成**: Vercel API 调用 Cloudflare Worker, 确保安全和监控。
|
package/src/mdx/blog/index.mdx
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
title: Blog
|
|
3
3
|
description: Articles and thoughts about various topics.
|
|
4
4
|
icon: Rss
|
|
5
|
-
date: 2025-
|
|
5
|
+
date: 2025-08-08
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
8
|
+
## Past List
|
|
9
|
+
|
|
10
|
+
<Cards>
|
|
11
|
+
<ZiaCard href="blog/async-architecture" title="Next.js 异步处理架构升级方案">
|
|
12
|
+
2025-07-31
|
|
13
|
+
</ZiaCard>
|
|
14
|
+
<ZiaCard href="blog/paid-system-design" title="订阅与积分系统产品设计文档 v1.0">
|
|
15
|
+
2025-07-31
|
|
16
|
+
</ZiaCard>
|
|
17
|
+
<ZiaCard icon={<Test />} href="blog/test" title="MDX Test">
|
|
18
|
+
2025-06-23
|
|
19
|
+
</ZiaCard>
|
|
20
|
+
</Cards>
|
package/src/mdx/blog/ioc.mdx
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Monthly Summary
|
|
3
3
|
description: Index and Summary
|
|
4
|
-
date: 2025-
|
|
4
|
+
date: 2025-08-08
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
<Files>
|
|
10
|
-
<
|
|
10
|
+
<ZiaFolder name="2025-07(2)" defaultOpen>
|
|
11
|
+
<ZiaFile name="2025-07-31(Next.js 异步处理架构升级方案)" href="./async-architecture" />
|
|
12
|
+
<ZiaFile name="2025-07-31(订阅与积分系统产品设计文档 v1.0)" href="./paid-system-design" />
|
|
13
|
+
</ZiaFolder>
|
|
14
|
+
<ZiaFolder name="2025-06(1)">
|
|
15
|
+
<ZiaFile name="2025-06-23(MDX Test)" href="./test" />
|
|
16
|
+
</ZiaFolder>
|
|
11
17
|
</Files>
|
|
12
18
|
|