From 07c61dd3e25fe9264a242028334efa21efdd1130 Mon Sep 17 00:00:00 2001 From: "Yangkai.Shen" <237497819@qq.com> Date: Sat, 17 Jan 2026 00:09:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=89=98=E7=AE=A1=E7=BB=9F=E4=B8=80=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=20SSL=20=E4=BC=98=E5=8C=96=20proposal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要内容 1. 图片托管统一化 - 将 129 张图片从七牛云/相对路径迁移至阿里云 OSS - 统一使用 static.xkcoding.com 域名 - 配置阿里云 CDN 加速 2. SSL 证书管理优化 - 重构 ssl-manage.yml: HTTP-01 → DNS-01 验证 - 新增 cdn-ssl-renew.yml: CDN 证书自动续期 - 实现零停机证书续期 Co-Authored-By: Claude Opus 4.5 --- .../changes/unify-image-hosting/design.md | 633 ++++++++++++++++++ .../changes/unify-image-hosting/proposal.md | 164 +++++ openspec/changes/unify-image-hosting/tasks.md | 161 +++++ 3 files changed, 958 insertions(+) create mode 100644 openspec/changes/unify-image-hosting/design.md create mode 100644 openspec/changes/unify-image-hosting/proposal.md create mode 100644 openspec/changes/unify-image-hosting/tasks.md diff --git a/openspec/changes/unify-image-hosting/design.md b/openspec/changes/unify-image-hosting/design.md new file mode 100644 index 00000000..d1b1a9db --- /dev/null +++ b/openspec/changes/unify-image-hosting/design.md @@ -0,0 +1,633 @@ +# Design: 统一博客图片托管至阿里云 OSS + SSL 证书管理优化 + +## 架构概览 + +``` + ┌────────────────────────────────────────────┐ + │ GitHub Actions │ + │ acme.sh + dns_ali (证书自动续期) │ + └─────────────────────┬──────────────────────┘ + │ 部署证书 + ▼ + ┌────────────────────────────────────────────┐ + │ 阿里云 CDN (HTTPS) │ + │ static.xkcoding.com │ + └─────────────────────┬──────────────────────┘ + │ 回源 + ▼ + ┌────────────────────────────────────────────┐ + │ 阿里云 OSS │ + │ xkcoding-blog.oss-cn-hangzhou.aliyuncs.com│ + │ │ + │ /blog/2019-08-09-xxx.png │ + │ /blog/2020-05-06-xxx.png │ + │ /resources/xxx.png │ + │ /2018-01-03-xxx.jpg │ + └────────────────────────────────────────────┘ +``` + +## 域名方案详细设计 + +### 采用方案:阿里云 CDN + acme.sh 自动证书续期 + +**架构:** +``` +用户请求 → static.xkcoding.com → 阿里云 CDN (HTTPS) → 阿里云 OSS + ↑ + acme.sh 自动部署证书 +``` + +**费用预估(博客场景):** +- CDN 流量:约 ¥0.24/GB(国内) +- 博客图片月流量一般 < 1GB,费用 < ¥1/月 + +--- + +## 阿里云 CDN + acme.sh 配置指南 + +### Step 1: 开通阿里云 CDN + +1. 登录阿里云控制台 → CDN → 开通服务 +2. 选择「按流量计费」(更适合小流量场景) + +### Step 2: 添加 CDN 加速域名 + +1. CDN 控制台 → 域名管理 → 添加域名 +2. 配置: + ``` + 加速域名: static.xkcoding.com + 业务类型: 图片小文件 + 源站信息: + - 类型: OSS 域名 + - 域名: xkcoding-blog.oss-cn-hangzhou.aliyuncs.com + 端口: 443 + ``` +3. 等待审核通过(通常几分钟) + +### Step 3: 配置 DNS 解析 + +在阿里云 DNS 控制台添加 CNAME 记录: +``` +记录类型: CNAME +主机记录: static +记录值: static.xkcoding.com.w.cdngslb.com # CDN 分配的 CNAME +``` + +### Step 4: 安装配置 acme.sh + +**DNS-01 验证原理**(无需修改 CNAME): + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ DNS 记录(两者独立,互不影响) │ +├─────────────────────────────────────────────────────────────────┤ +│ static.xkcoding.com │ +│ └─ CNAME → xxx.cdngslb.com ← 一直存在,指向 CDN │ +│ │ +│ _acme-challenge.static.xkcoding.com │ +│ └─ TXT → "xxxxxx验证码" ← 临时,验证后自动删除 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +acme.sh 使用 `--dns dns_ali` 参数会: +1. 调用阿里云 DNS API 自动添加 TXT 记录 +2. 等待 Let's Encrypt 验证 +3. 验证通过后自动删除 TXT 记录 +4. 整个过程不影响 CNAME 记录 + +**本地安装(用于调试)**: + +```bash +# 1. 安装 acme.sh +curl https://get.acme.sh | sh -s email=237497819@qq.com +source ~/.zshrc # 或 ~/.bashrc + +# 2. 配置阿里云 API 密钥(用于 DNS-01 验证) +export Ali_Key="你的 AccessKey ID" +export Ali_Secret="你的 AccessKey Secret" + +# 3. 申请证书(DNS-01 验证,自动操作 TXT 记录) +acme.sh --issue --dns dns_ali -d static.xkcoding.com + +# 证书文件位置:~/.acme.sh/static.xkcoding.com/ +``` + +### Step 5: 部署证书到阿里云 CDN + +**方法 A: 使用 acme.sh 内置 deploy hook(推荐)** + +```bash +# 配置阿里云 CDN 部署参数 +export Chinese_Region="cn-hangzhou" +export Ali_CDN_Domain="static.xkcoding.com" + +# 部署证书 +acme.sh --deploy -d static.xkcoding.com --deploy-hook ali_cdn +``` + +**方法 B: 手动部署(首次或调试用)** + +1. 查看证书文件: + ```bash + ls ~/.acme.sh/static.xkcoding.com/ + # static.xkcoding.com.cer - 证书文件 + # static.xkcoding.com.key - 私钥文件 + # fullchain.cer - 完整证书链 + ``` + +2. CDN 控制台 → 域名管理 → static.xkcoding.com → HTTPS 配置 +3. 上传证书: + - 证书内容:`fullchain.cer` 的内容 + - 私钥:`static.xkcoding.com.key` 的内容 + +### Step 6: 配置自动续期(GitHub Actions 方案,推荐) + +使用 GitHub Actions 替代本地 cron job,更加可靠: + +**创建 `.github/workflows/cdn-ssl-renew.yml`:** + +```yaml +name: CDN SSL Certificate Renewal + +on: + # 每月 1 号和 15 号检查续期 + schedule: + - cron: "0 2 1,15 * *" + # 支持手动触发 + workflow_dispatch: + +jobs: + renew-cert: + runs-on: ubuntu-latest + steps: + - name: Install acme.sh + run: | + curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }} + + - name: Issue/Renew Certificate + env: + Ali_Key: ${{ secrets.ALI_KEY }} + Ali_Secret: ${{ secrets.ALI_SECRET }} + run: | + ~/.acme.sh/acme.sh --issue --dns dns_ali -d static.xkcoding.com --force + + - name: Deploy to Aliyun CDN + env: + Ali_Key: ${{ secrets.ALI_KEY }} + Ali_Secret: ${{ secrets.ALI_SECRET }} + CHINESE_REGION: cn-hangzhou + ALI_CDN_DOMAIN: static.xkcoding.com + run: | + ~/.acme.sh/acme.sh --deploy -d static.xkcoding.com --deploy-hook ali_cdn + + - name: Summary + run: | + echo "## 🔐 CDN SSL 证书续期完成" >> $GITHUB_STEP_SUMMARY + echo "- **域名**: static.xkcoding.com" >> $GITHUB_STEP_SUMMARY + echo "- **时间**: $(date)" >> $GITHUB_STEP_SUMMARY +``` + +**配置 GitHub Secrets:** +- `ACME_EMAIL`: 237497819@qq.com +- `ALI_KEY`: 阿里云 AccessKey ID +- `ALI_SECRET`: 阿里云 AccessKey Secret + +**优势:** +- 无需本地服务器运行 +- GitHub Actions 免费 +- 与现有 SSL 管理工作流统一管理 +- 执行日志可追溯 + +--- + +### 备选:本地 acme.sh cron job + +如果不想用 GitHub Actions,也可以在本地配置: + +```bash +# 查看 cron job +crontab -l | grep acme + +# 手动测试续期(不会真正续期,只是测试) +acme.sh --renew -d static.xkcoding.com --force --dry-run + +# 证书续期后会自动调用 deploy hook 部署到 CDN +``` + +### Step 7: 验证配置 + +```bash +# 检查 HTTPS 是否生效 +curl -I https://static.xkcoding.com/ + +# 检查证书信息 +echo | openssl s_client -servername static.xkcoding.com -connect static.xkcoding.com:443 2>/dev/null | openssl x509 -noout -dates +``` + +--- + +## 阿里云 AccessKey 安全配置 + +**强烈建议使用 RAM 子账号,仅授予必要权限:** + +1. 创建 RAM 子账号 + - 控制台 → RAM 访问控制 → 用户 → 创建用户 + - 勾选「OpenAPI 调用访问」 + +2. 授予权限(最小权限原则): + ```json + { + "Version": "1", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "alidns:AddDomainRecord", + "alidns:DeleteDomainRecord", + "alidns:DescribeDomainRecords" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "cdn:SetDomainServerCertificate", + "cdn:DescribeDomainCertificateInfo" + ], + "Resource": "*" + } + ] + } + ``` + +3. 创建 AccessKey 并记录 + +--- + +## SSL 工作流重构设计 + +### 重构后的 ssl-manage.yml(博客域名证书) + +```yaml +name: SSL Certificate Management + +on: + schedule: + - cron: "0 2 1,15 * *" # 每月 1 号和 15 号 + workflow_dispatch: + inputs: + force_renew: + description: "强制重新申请证书" + required: false + type: boolean + default: false + +env: + DOMAINS: "xkcoding.com,www.xkcoding.com,blog.xkcoding.com" + +jobs: + renew-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Install acme.sh + run: | + curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }} + + - name: Issue/Renew Certificate (DNS-01) + env: + Ali_Key: ${{ secrets.ALI_KEY }} + Ali_Secret: ${{ secrets.ALI_SECRET }} + run: | + # 构建域名参数 + DOMAIN_ARGS="" + IFS=',' read -ra DOMAIN_ARRAY <<< "$DOMAINS" + for domain in "${DOMAIN_ARRAY[@]}"; do + DOMAIN_ARGS="$DOMAIN_ARGS -d $domain" + done + + # 申请/续期证书(DNS-01 验证,自动操作 TXT 记录) + if [ "${{ github.event.inputs.force_renew }}" = "true" ]; then + ~/.acme.sh/acme.sh --issue --dns dns_ali $DOMAIN_ARGS --force + else + ~/.acme.sh/acme.sh --issue --dns dns_ali $DOMAIN_ARGS + fi + + - name: Deploy Certificate to Server + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.ALIYUN_HOST }} + username: ${{ secrets.ALIYUN_USER }} + key: ${{ secrets.ALIYUN_SSH_KEY }} + port: ${{ secrets.ALIYUN_PORT || 22 }} + script: | + echo "🔐 部署证书到服务器..." + + # 证书目录 + CERT_DIR="/opt/myblog/certbot/etc/live/xkcoding.com" + mkdir -p $CERT_DIR + + # 证书内容通过环境变量传递(见下一步) + + - name: Upload Certificate Files + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.ALIYUN_HOST }} + username: ${{ secrets.ALIYUN_USER }} + key: ${{ secrets.ALIYUN_SSH_KEY }} + port: ${{ secrets.ALIYUN_PORT || 22 }} + source: "~/.acme.sh/xkcoding.com/*" + target: "/opt/myblog/certbot/etc/live/xkcoding.com/" + strip_components: 3 + + - name: Reload Nginx + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.ALIYUN_HOST }} + username: ${{ secrets.ALIYUN_USER }} + key: ${{ secrets.ALIYUN_SSH_KEY }} + port: ${{ secrets.ALIYUN_PORT || 22 }} + script: | + echo "🔄 重载 nginx 配置..." + docker exec myblog-nginx nginx -s reload + echo "✅ 证书部署完成!" + + - name: Summary + run: | + echo "## 🔐 SSL 证书管理完成" >> $GITHUB_STEP_SUMMARY + echo "- **域名**: ${{ env.DOMAINS }}" >> $GITHUB_STEP_SUMMARY + echo "- **验证方式**: DNS-01 (零停机)" >> $GITHUB_STEP_SUMMARY + echo "- **时间**: $(date)" >> $GITHUB_STEP_SUMMARY +``` + +**关键改进**: +1. 使用 `acme.sh --dns dns_ali` 进行 DNS-01 验证 +2. 不再需要停止 nginx +3. 证书申请在 GitHub Actions 完成 +4. 通过 SCP 上传证书文件到服务器 +5. 仅需 `nginx -s reload` 重载配置 + +### 新增 cdn-ssl-renew.yml(CDN 域名证书) + +```yaml +name: CDN SSL Certificate Renewal + +on: + schedule: + - cron: "0 3 1,15 * *" # 每月 1 号和 15 号,在博客证书后执行 + workflow_dispatch: + +jobs: + renew-cdn-cert: + runs-on: ubuntu-latest + steps: + - name: Install acme.sh + run: | + curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }} + + - name: Issue/Renew Certificate + env: + Ali_Key: ${{ secrets.ALI_KEY }} + Ali_Secret: ${{ secrets.ALI_SECRET }} + run: | + ~/.acme.sh/acme.sh --issue --dns dns_ali -d static.xkcoding.com + + - name: Deploy to Aliyun CDN + env: + Ali_Key: ${{ secrets.ALI_KEY }} + Ali_Secret: ${{ secrets.ALI_SECRET }} + CHINESE_REGION: cn-hangzhou + ALI_CDN_DOMAIN: static.xkcoding.com + run: | + ~/.acme.sh/acme.sh --deploy -d static.xkcoding.com --deploy-hook ali_cdn + + - name: Summary + run: | + echo "## 🔐 CDN SSL 证书续期完成" >> $GITHUB_STEP_SUMMARY + echo "- **域名**: static.xkcoding.com" >> $GITHUB_STEP_SUMMARY + echo "- **部署目标**: 阿里云 CDN" >> $GITHUB_STEP_SUMMARY + echo "- **时间**: $(date)" >> $GITHUB_STEP_SUMMARY +``` + +### 对比:重构前后 + +| 对比项 | 重构前 (certbot standalone) | 重构后 (acme.sh dns_ali) | +|--------|----------------------------|--------------------------| +| 验证方式 | HTTP-01 | DNS-01 | +| 服务中断 | 需要停止 nginx 约 30 秒 | 零停机 | +| 执行位置 | SSH 到服务器 | GitHub Actions | +| 端口依赖 | 需要 80 端口 | 无需任何端口 | +| 复杂度 | 高(端口冲突处理) | 低(纯 API 调用) | +| 证书部署 | 本地生成,本地使用 | 远程生成,SCP 上传 | + +--- + +## 迁移脚本设计 + +### 脚本架构 + +``` +scripts/ +├── migrate-images/ +│ ├── index.ts # 主入口 +│ ├── scanner.ts # Markdown 扫描器 +│ ├── downloader.ts # 图片下载器 +│ ├── uploader.ts # OSS 上传器 +│ ├── replacer.ts # URL 替换器 +│ └── types.ts # 类型定义 +├── migrate-images.config.ts # 配置文件 +└── temp/ # 临时下载目录(gitignore) +``` + +### 核心数据结构 + +```typescript +// types.ts +interface ImageRecord { + // 原始信息 + sourceUrl: string; // 原始 URL + sourceType: 'qiniu' | 'aliyun-old' | 'relative'; + markdownFile: string; // 所在 Markdown 文件 + lineNumber: number; // 行号 + + // 迁移状态 + localPath?: string; // 本地下载路径 + targetUrl?: string; // 目标 OSS URL + status: 'pending' | 'downloaded' | 'uploaded' | 'replaced' | 'failed'; + error?: string; // 错误信息 +} + +interface MigrationConfig { + // 源配置 + sourcePatterns: { + qiniu: RegExp; // /static\.xkcoding\.com/ + aliyunOld: RegExp; // /static\.aliyun\.xkcoding\.com/ + relative: RegExp; // /^\/resources\// + }; + + // 目标配置 + target: { + domain: string; // 'static.xkcoding.com' + bucket: string; // 'xkcoding-blog' + region: string; // 'oss-cn-hangzhou' + }; + + // 路径配置 + paths: { + tempDir: string; // './scripts/temp' + reportDir: string; // './scripts/reports' + }; +} +``` + +### URL 转换规则 + +```typescript +// URL 转换逻辑 +function transformUrl(sourceUrl: string, config: MigrationConfig): string { + const targetDomain = config.target.domain; + + // 七牛云: static.xkcoding.com/xxx → static.xkcoding.com/xxx + // 路径保持不变,只是实际存储位置变了 + if (sourceUrl.includes('static.xkcoding.com')) { + return sourceUrl; // URL 不变,因为域名会指向新的 OSS + } + + // 阿里云旧域名: static.aliyun.xkcoding.com/2021/09/03/xxx + // → static.xkcoding.com/2021/09/03/xxx + if (sourceUrl.includes('static.aliyun.xkcoding.com')) { + return sourceUrl.replace('static.aliyun.xkcoding.com', targetDomain); + } + + // 相对路径: /resources/xxx.png → static.xkcoding.com/resources/xxx.png + if (sourceUrl.startsWith('/resources/')) { + return `https://${targetDomain}${sourceUrl}`; + } + + return sourceUrl; +} +``` + +### 迁移流程伪代码 + +```typescript +async function migrate() { + // 1. 扫描 + const images = await scanMarkdownFiles('./src/data/blog'); + console.log(`Found ${images.length} images to migrate`); + + // 2. 下载 + for (const image of images) { + try { + await downloadImage(image); + image.status = 'downloaded'; + } catch (e) { + image.status = 'failed'; + image.error = e.message; + } + } + + // 3. 上传 + const downloaded = images.filter(i => i.status === 'downloaded'); + await uploadToOSS(downloaded); // 使用 ossutil 批量上传 + + // 4. 替换(先 dry-run) + const mapping = generateUrlMapping(images); + await previewReplacements(mapping); + + // 用户确认后执行 + if (await confirm('Proceed with replacements?')) { + await executeReplacements(mapping); + } + + // 5. 生成报告 + await generateReport(images); +} +``` + +## OSS 目录结构 + +``` +xkcoding-blog/ +├── blog/ +│ ├── 2019-08-09-abstractfactory-uml.png +│ ├── 2019-08-24-034026.png +│ ├── 2020-05-06-053453.png +│ └── ... +├── resources/ +│ ├── coding-standards-1.png +│ ├── volley-demo-20160326012642.png +│ └── ... +├── 2017-07-12-14998277900006.jpg +├── 2018-01-03-15149489321563.jpg +└── ... +``` + +## 错误处理策略 + +### 图片下载失败 + +1. **404 错误**: 记录到 `missing-images.json`,需手动处理 +2. **网络超时**: 自动重试 3 次,间隔递增 +3. **其他错误**: 记录错误详情,继续处理下一张 + +### OSS 上传失败 + +1. **认证失败**: 提示检查 ossutil 配置 +2. **权限不足**: 提示检查 Bucket 权限 +3. **文件已存在**: 跳过或覆盖(可配置) + +### URL 替换失败 + +1. **文件无法写入**: 提示检查文件权限 +2. **替换后格式错误**: 保留原文件备份 + +## 回滚方案 + +迁移前自动创建 Git 分支: +```bash +git checkout -b backup/before-image-migration +git checkout -b feature/unify-image-hosting +``` + +如需回滚: +```bash +git checkout backup/before-image-migration +``` + +## 工具安装指南 + +### ossutil 安装 + +**macOS:** +```bash +# 下载 +curl -o ossutil64 https://gosspublic.alicdn.com/ossutil/1.7.14/ossutil64 + +# 授权 +chmod 755 ossutil64 + +# 移动到 PATH +sudo mv ossutil64 /usr/local/bin/ossutil + +# 配置 +ossutil config +# 输入 Endpoint: oss-cn-hangzhou.aliyuncs.com +# 输入 AccessKey ID +# 输入 AccessKey Secret +``` + +**验证:** +```bash +ossutil ls oss://xkcoding-blog/ +``` + +### 获取阿里云 AccessKey + +1. 登录阿里云控制台 +2. 右上角头像 → AccessKey 管理 +3. 创建 AccessKey(建议使用 RAM 子账号) +4. 记录 AccessKey ID 和 AccessKey Secret + +**安全建议:** +- 使用 RAM 子账号,仅授予 OSS 相关权限 +- 不要将 AccessKey 提交到 Git diff --git a/openspec/changes/unify-image-hosting/proposal.md b/openspec/changes/unify-image-hosting/proposal.md new file mode 100644 index 00000000..7ceb4aeb --- /dev/null +++ b/openspec/changes/unify-image-hosting/proposal.md @@ -0,0 +1,164 @@ +# Proposal: 统一博客图片托管至阿里云 OSS + SSL 证书管理优化 + +## 概述 + +1. 将博客中分散在多个来源的图片统一迁移至阿里云 OSS,实现图片资源的集中管理 +2. 优化现有 SSL 证书管理工作流,从 HTTP-01 验证改为 DNS-01 验证,实现零停机续期 + +## 动机 + +当前博客图片存储分散在多个位置: +- **七牛云** (`static.xkcoding.com`): 116 处引用 +- **阿里云 OSS** (`static.aliyun.xkcoding.com`): 5 处引用 +- **相对路径** (`/resources/`): 8 处引用(_archive 目录) + +**图片托管问题**: +1. 多平台管理增加维护成本 +2. 七牛云免费额度可能受限 +3. 相对路径图片可能加载失败 +4. 难以统一进行图片优化(压缩、CDN 等) + +**SSL 证书管理问题**(现有 ssl-manage.yml): +1. 使用 HTTP-01 验证需要停止 nginx,有服务中断 +2. 必须 SSH 到服务器执行,增加复杂度 +3. 依赖 80 端口可被外部访问 +4. 续期时若端口被占用会失败 + +## 目标 + +**图片托管**: +1. 将所有博客图片统一托管到阿里云 OSS +2. 创建自动化迁移脚本,减少手动操作 +3. 批量更新 Markdown 文件中的图片链接 +4. 保持图片 URL 结构的一致性 + +**SSL 证书管理优化**: +5. 改用 DNS-01 验证,实现零停机证书续期 +6. 统一使用 acme.sh + 阿里云 DNS API +7. 证书管理完全在 GitHub Actions 中完成 +8. 新增 CDN SSL 证书自动续期工作流 + +## 非目标 + +- 不迁移第三方徽章图片(如 maven-badges.herokuapp.com) +- 不迁移占位图(如 dummyimage.com) +- 不处理 hexo-backup 目录中的历史文件 + +## 技术方案 + +### 1. 目标域名选择 + +**采用方案**:`static.xkcoding.com` + 阿里云 CDN + acme.sh 自动证书续期 + +``` +用户请求 → static.xkcoding.com → 阿里云 CDN (HTTPS) → 阿里云 OSS + ↑ + acme.sh 自动部署证书 +``` + +**优势**: +- 自定义域名,URL 简洁 +- acme.sh 自动申请和续期 Let's Encrypt 证书 +- 阿里云 CDN 加速(费用极低,博客场景 < ¥1/月) +- 域名 DNS 保持在阿里云,无需迁移 + +### 2. 图片迁移流程 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 扫描 Markdown │ ──▶ │ 下载源图片 │ ──▶ │ 上传至 OSS │ +│ 提取图片 URL │ │ 到本地临时目录 │ │ 保持路径结构 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 验证迁移结果 │ ◀── │ 更新 Markdown │ ◀── │ 生成 URL 映射 │ +│ 检查图片可访问 │ │ 替换图片链接 │ │ 新旧对照表 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 3. URL 映射规则 + +| 来源 | 示例 | 目标 | +|------|------|------| +| 七牛云 | `static.xkcoding.com/blog/2019-08-09-xxx.png` | `static.xkcoding.com/blog/2019-08-09-xxx.png` | +| 七牛云 | `static.xkcoding.com/2018-01-03-xxx.jpg` | `static.xkcoding.com/2018-01-03-xxx.jpg` | +| 阿里云旧 | `static.aliyun.xkcoding.com/2021/09/03/xxx.jpg` | `static.xkcoding.com/2021/09/03/xxx.jpg` | +| 相对路径 | `/resources/xxx.png` | `static.xkcoding.com/resources/xxx.png` | + +### 4. 工具依赖 + +- **ossutil**: 阿里云 OSS 命令行工具,用于批量上传 +- **Node.js 脚本**: 扫描 Markdown、下载图片、生成映射、批量替换 +- **acme.sh**: Let's Encrypt 证书申请工具,支持 DNS-01 验证 + +### 5. SSL 证书管理优化 + +**现有方案 vs 优化方案对比**: + +| 对比项 | 现有 ssl-manage.yml | 优化后 | +|--------|---------------------|--------| +| 验证方式 | HTTP-01 (standalone) | DNS-01 (dns_ali) | +| 服务中断 | 需要停止 nginx | 零停机 | +| 执行位置 | SSH 到服务器 | GitHub Actions | +| 端口依赖 | 需要 80 端口 | 无需任何端口 | +| 证书工具 | certbot | acme.sh | + +**优化后的架构**: + +``` +GitHub Actions + │ + ├─ 1. acme.sh --dns dns_ali 申请/续期证书 + │ └─ 自动操作阿里云 DNS TXT 记录 + │ + ├─ 2. 部署到阿里云 CDN (static.xkcoding.com) + │ └─ acme.sh --deploy-hook ali_cdn + │ + └─ 3. 部署到阿里云服务器 (xkcoding.com) + └─ SSH 上传证书 + nginx -s reload +``` + +**新增/修改的工作流**: + +| 工作流 | 用途 | 改动 | +|--------|------|------| +| `ssl-manage.yml` | 博客域名证书 | 重构:HTTP-01 → DNS-01 | +| `cdn-ssl-renew.yml` | CDN 域名证书 | 新增 | + +## 风险与缓解 + +| 风险 | 缓解措施 | +|------|----------| +| 图片下载失败 | 脚本记录失败列表,支持断点续传 | +| OSS 上传失败 | 重试机制 + 失败日志 | +| URL 替换出错 | 先生成预览报告,人工确认后执行 | +| 七牛云图片已失效 | 记录 404 图片,手动处理 | + +## 迁移统计 + +| 来源 | 数量 | 说明 | +|------|------|------| +| 七牛云 (`static.xkcoding.com`) | 116 | 主要迁移目标 | +| 相对路径 (`/resources/`) | 8 | _archive 目录 | +| 阿里云旧域名 | 5 | 仅需替换域名 | +| **总计** | **129** | | + +## 时间线 + +- Phase 1: 环境准备(RAM 账号、CDN、DNS、ossutil) +- Phase 2: SSL 工作流优化 +- Phase 3: 图片迁移脚本开发 +- Phase 4: 执行图片迁移 +- Phase 5: 验证与清理 + +## 相关文件 + +**新增/修改**: +- `.github/workflows/ssl-manage.yml` - 重构为 DNS-01 验证 +- `.github/workflows/cdn-ssl-renew.yml` - 新增 CDN 证书管理 +- `scripts/migrate-images.ts` - 图片迁移脚本(待创建) + +**需要更新**: +- `src/data/blog/*.md` - 需要更新的 Markdown 文件 +- `src/data/blog/_archive/*.md` - 归档目录 Markdown 文件 diff --git a/openspec/changes/unify-image-hosting/tasks.md b/openspec/changes/unify-image-hosting/tasks.md new file mode 100644 index 00000000..92178ee1 --- /dev/null +++ b/openspec/changes/unify-image-hosting/tasks.md @@ -0,0 +1,161 @@ +# Tasks: 统一博客图片托管至阿里云 OSS + SSL 证书管理优化 + +## Phase 1: 环境准备 + +- [ ] 1.1 创建阿里云 RAM 子账号 + - 登录阿里云控制台 → RAM 访问控制 + - 创建子账号,勾选「OpenAPI 调用访问」 + - 授予权限:DNS (alidns) + CDN + OSS + - 创建并记录 AccessKey + +- [ ] 1.2 配置 GitHub Secrets + - `ALI_KEY`: 阿里云 AccessKey ID + - `ALI_SECRET`: 阿里云 AccessKey Secret + - `ACME_EMAIL`: 证书申请邮箱 + +- [ ] 1.3 开通并配置阿里云 CDN + - 开通 CDN 服务(按流量计费) + - 添加加速域名 `static.xkcoding.com` + - 源站设置为 OSS: `xkcoding-blog.oss-cn-hangzhou.aliyuncs.com` + - 记录 CDN 分配的 CNAME 地址 + +- [ ] 1.4 配置 DNS 解析 + - 在阿里云 DNS 添加 CNAME 记录 + - `static` → CDN 分配的 CNAME 地址 + - 验证解析生效 + +- [ ] 1.5 安装配置 ossutil(本地) + - 下载 ossutil 命令行工具 + - 使用 RAM 子账号的 AccessKey 配置 + - 验证连接到 OSS bucket + +- [ ] 1.6 OSS Bucket 配置 + - 确认 Bucket 公开读权限 + - 配置跨域规则(CORS)允许博客域名访问 + +## Phase 2: SSL 工作流优化 + +- [ ] 2.1 创建 CDN SSL 证书工作流 + - 创建 `.github/workflows/cdn-ssl-renew.yml` + - 使用 acme.sh + dns_ali 申请证书 + - 配置 deploy hook 部署到阿里云 CDN + - 手动触发测试,验证证书申请成功 + +- [ ] 2.2 重构博客 SSL 证书工作流 + - 修改 `.github/workflows/ssl-manage.yml` + - 从 HTTP-01 (certbot standalone) 改为 DNS-01 (acme.sh dns_ali) + - 证书申请在 GitHub Actions 完成 + - 通过 SSH 部署证书到服务器 + reload nginx + - 删除停止/启动 nginx 的逻辑 + +- [ ] 2.3 验证 SSL 工作流 + - 手动触发两个工作流 + - 验证 `https://xkcoding.com` 证书正常 + - 验证 `https://static.xkcoding.com` 证书正常 + +## Phase 3: 图片迁移脚本开发 + +- [ ] 3.1 创建图片扫描脚本 + - 扫描所有 Markdown 文件 + - 提取图片 URL(七牛云、阿里云旧域名、相对路径) + - 生成图片清单 JSON 文件 + +- [ ] 3.2 创建图片下载脚本 + - 批量下载图片到本地临时目录 + - 保持原有路径结构 + - 记录下载失败的图片列表 + - 支持断点续传 + +- [ ] 3.3 创建 OSS 上传脚本 + - 使用 ossutil 批量上传图片 + - 保持路径结构一致 + - 生成上传报告 + +- [ ] 3.4 创建 URL 替换脚本 + - 生成新旧 URL 映射表 + - 预览替换结果(dry-run 模式) + - 执行批量替换 + - 生成替换报告 + +## Phase 4: 执行图片迁移 + +- [ ] 4.1 下载七牛云图片(116 张) + - 执行下载脚本 + - 检查下载完整性 + - 处理失败项 + +- [ ] 4.2 下载相对路径图片(8 张) + - 从本地 hexo-backup 或网络下载 + - 如无法获取,标记为需手动处理 + +- [ ] 4.3 上传图片到 OSS + - 批量上传所有图片 + - 验证上传成功 + - 记录最终 URL + +- [ ] 4.4 更新 Markdown 文件 + - 执行 dry-run 预览变更 + - 确认无误后执行替换 + - 生成变更报告 + +## Phase 5: 验证与清理 + +- [ ] 5.1 验证图片可访问 + - 批量检查新 URL 返回 200 + - 记录任何 404 或错误 + +- [ ] 5.2 本地构建测试 + - 执行 `pnpm build` + - 确认无图片加载错误 + +- [ ] 5.3 部署预发环境验证 + - 推送到 GitHub 触发 preview 部署 + - 手动检查页面图片显示 + +- [ ] 5.4 部署生产环境验证 + - 触发生产部署 + - 验证所有图片正常加载 + +- [ ] 5.5 清理临时文件 + - 删除本地下载的临时图片 + - 清理迁移脚本生成的中间文件 + +## 验收标准 + +**图片托管**: +- [ ] 所有博客图片从统一域名 `static.xkcoding.com` 加载 +- [ ] 图片通过 HTTPS 访问,无混合内容警告 +- [ ] 本地构建无 Shiki/图片相关警告 +- [ ] 预发和生产环境图片正常显示 + +**SSL 证书管理**: +- [ ] `ssl-manage.yml` 使用 DNS-01 验证,零停机续期 +- [ ] `cdn-ssl-renew.yml` 自动管理 CDN 证书 +- [ ] 两个工作流都可手动触发和定时执行 +- [ ] 证书续期不再需要停止 nginx + +## 依赖关系 + +``` +Phase 1 (环境准备) +1.1 ──▶ 1.2 ──▶ 2.1, 2.2 +1.3 ──▶ 1.4 ──▶ 2.1 +1.1 ──▶ 1.5 ──▶ 3.3 +1.6 ──▶ 3.3 + +Phase 2 (SSL 优化) - 可与 Phase 3 并行 +2.1 ──▶ 2.3 +2.2 ──▶ 2.3 + +Phase 3 (脚本开发) +3.1 ──▶ 3.2 ──▶ 3.3 + │ +3.4 ◀─────────┘ + +Phase 4 (执行迁移) +4.1 ──┬──▶ 4.3 ──▶ 4.4 +4.2 ──┘ + +Phase 5 (验证) +4.4 ──▶ 5.1 ──▶ 5.2 ──▶ 5.3 ──▶ 5.4 +``` From ec80a9f4dc1854b7b13142e49f3079eb5a90fa25 Mon Sep 17 00:00:00 2001 From: "Yangkai.Shen" <237497819@qq.com> Date: Mon, 19 Jan 2026 14:37:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=8D=9A?= =?UTF-8?q?=E5=AE=A2=E5=9B=BE=E7=89=87=E6=89=98=E7=AE=A1=E5=88=B0=E9=98=BF?= =?UTF-8?q?=E9=87=8C=E4=BA=91=20OSS=20+=20CDN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移图片:139 张图片从七牛云/旧阿里云域名迁移到新 OSS - 新域名:cdn.xkcoding.com(阿里云 CDN 加速) - 迁移脚本:scanner/downloader/uploader/replacer - SSL 工作流:cdn-ssl-renew.yml 自动续期 CDN 证书(DNS-01 验证) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/cdn-ssl-renew.yml | 142 ++++++++++ .gitignore | 7 + openspec/changes/unify-image-hosting/tasks.md | 90 +++++-- scripts/migrate-images/downloader.ts | 232 ++++++++++++++++ scripts/migrate-images/index.ts | 41 +++ scripts/migrate-images/replacer.ts | 252 ++++++++++++++++++ scripts/migrate-images/scanner.ts | 204 ++++++++++++++ scripts/migrate-images/types.ts | 85 ++++++ scripts/migrate-images/uploader.ts | 223 ++++++++++++++++ .../blog/2017-07-12.jdk-map-source-explore.md | 2 +- .../2017-07-19.spring-boot-mybatis-logback.md | 2 +- .../2017-08-02.customize-blocked-queue.md | 4 +- src/data/blog/2017-08-31.mysql-start-error.md | 2 +- .../blog/2017-11-08.dubbo-admin-install.md | 10 +- ...-11-14.mysql-chinese-character-encoding.md | 2 +- src/data/blog/2018-01-03.devops-gitlab.md | 8 +- .../blog/2018-01-12.elasticsearch-note.md | 18 +- src/data/blog/2018-11-07.spring-boot-start.md | 18 +- src/data/blog/2019-01-01.bye2018-hello2019.md | 2 +- ...1-14.solve-mac-install-minikube-problem.md | 2 +- .../2019-01-17.solve-nginx-413-problem.md | 2 +- ...30.spring-boot-request-use-enums-params.md | 10 +- ...04.use-maven-manage-spring-boot-profile.md | 2 +- ...019-02-13.design-pattern-simple-factory.md | 4 +- ...019-02-15.design-pattern-factory-method.md | 2 +- src/data/blog/2019-03-12.scaffold-doc.md | 22 +- ...use-vagrant-build-linux-virtual-machine.md | 4 +- ...2019-05-22.spring-boot-login-with-oauth.md | 24 +- ...-justauth-integration-wechat-enterprise.md | 20 +- ...9-08-10.design-pattern-abstract-factory.md | 2 +- .../2019-08-14.design-pattern-prototype.md | 4 +- .../blog/2019-08-20.design-pattern-proxy.md | 10 +- .../2019-08-27.design-pattern-delegate.md | 2 +- ...9.early-experience-about-github-actions.md | 10 +- .../2019-09-03.design-pattern-strategy.md | 2 +- .../blog/2019-09-13.design-pattern-adapter.md | 2 +- .../2019-10-16.design-pattern-template.md | 2 +- .../2019-10-25.design-pattern-observer.md | 2 +- .../2019-11-11.design-pattern-decorator.md | 2 +- src/data/blog/2020-01-01.bye2019-hello2020.md | 8 +- ....how-to-design-your-own-rpc-framework-1.md | 4 +- ....how-to-design-your-own-rpc-framework-2.md | 2 +- ...0-06-01.how-does-my-blog-integrate-cicd.md | 10 +- .../2020-11-02.quick-run-you-own-yiyan.md | 2 +- .../2021-01-18.Q4-2020-and-year-summary.md | 2 +- ...about-python-package-install-on-big-sur.md | 2 +- ...021-03-15.run-local-k8s-cluster-quickly.md | 4 +- ...run-local-zookeeper-cluster-with-docker.md | 2 +- .../2021-04-22.when-lombok-met-mapstruct.md | 2 +- ...21-05-01.happy-dev-run-local-dns-server.md | 6 +- ...01-04.fix-error-about-jd-gui-on-big-sur.md | 4 +- .../2016-03-25.android-open-project.md | 2 +- .../2016-03-26.first-use-volley-demo.md | 2 +- .../2016-03-27.eclipse-tips-variable.md | 2 +- .../2016-04-03.android-svn-attention.md | 2 +- .../_archive/2016-04-05.coding-standards.md | 8 +- .../_archive/2017-07-18.win10-download.md | 12 +- .../2020-09-13.what-i-like-to-do-0.md | 4 +- 58 files changed, 1388 insertions(+), 166 deletions(-) create mode 100644 .github/workflows/cdn-ssl-renew.yml create mode 100644 scripts/migrate-images/downloader.ts create mode 100644 scripts/migrate-images/index.ts create mode 100644 scripts/migrate-images/replacer.ts create mode 100644 scripts/migrate-images/scanner.ts create mode 100644 scripts/migrate-images/types.ts create mode 100644 scripts/migrate-images/uploader.ts diff --git a/.github/workflows/cdn-ssl-renew.yml b/.github/workflows/cdn-ssl-renew.yml new file mode 100644 index 00000000..70ce9bc7 --- /dev/null +++ b/.github/workflows/cdn-ssl-renew.yml @@ -0,0 +1,142 @@ +name: CDN SSL Certificate Management + +on: + # 每月 1 号和 15 号凌晨 4 点执行(错开博客 SSL 续期时间) + schedule: + - cron: "0 20 1,15 * *" # UTC 20:00 = 北京时间 04:00 + # 支持手动触发 + workflow_dispatch: + inputs: + force_renew: + description: "强制重新申请证书" + required: false + type: boolean + default: false + +env: + DOMAIN: "cdn.xkcoding.com" + EMAIL: "237497819@qq.com" + +jobs: + cdn-ssl-manage: + runs-on: ubuntu-latest + + steps: + - name: Install acme.sh + run: | + curl https://get.acme.sh | sh -s email=${{ env.EMAIL }} + echo "$HOME/.acme.sh" >> $GITHUB_PATH + + - name: Configure Aliyun DNS API + run: | + export Ali_Key="${{ secrets.ALI_ACCESS_KEY_ID }}" + export Ali_Secret="${{ secrets.ALI_ACCESS_KEY_SECRET }}" + + # 保存到 acme.sh 账户配置 + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + + # 写入环境变量供后续步骤使用 + echo "Ali_Key=${{ secrets.ALI_ACCESS_KEY_ID }}" >> $GITHUB_ENV + echo "Ali_Secret=${{ secrets.ALI_ACCESS_KEY_SECRET }}" >> $GITHUB_ENV + + - name: Issue/Renew Certificate + run: | + FORCE_RENEW="${{ github.event.inputs.force_renew }}" + + # 检查是否需要强制续期 + FORCE_FLAG="" + if [ "$FORCE_RENEW" = "true" ]; then + echo "🔄 强制重新申请证书..." + FORCE_FLAG="--force" + fi + + # 使用 DNS-01 验证申请/续期证书 + echo "🔐 申请证书: ${{ env.DOMAIN }}" + ~/.acme.sh/acme.sh --issue \ + --dns dns_ali \ + -d ${{ env.DOMAIN }} \ + $FORCE_FLAG \ + --debug || { + # 如果证书已存在且未到期,--issue 会失败,这是正常的 + echo "ℹ️ 证书可能已存在且未到期" + } + + - name: Install Aliyun CLI + run: | + # 下载并安装阿里云 CLI + curl -Lo aliyun-cli.tgz https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz + tar -xzf aliyun-cli.tgz + sudo mv aliyun /usr/local/bin/ + + # 配置 CLI + aliyun configure set \ + --profile default \ + --mode AK \ + --region cn-hangzhou \ + --access-key-id ${{ secrets.ALI_ACCESS_KEY_ID }} \ + --access-key-secret ${{ secrets.ALI_ACCESS_KEY_SECRET }} + + - name: Deploy Certificate to CDN + run: | + CERT_PATH="$HOME/.acme.sh/${{ env.DOMAIN }}_ecc" + + # 检查证书是否存在 + if [ ! -f "$CERT_PATH/${{ env.DOMAIN }}.cer" ]; then + # 尝试非 ECC 路径 + CERT_PATH="$HOME/.acme.sh/${{ env.DOMAIN }}" + fi + + if [ ! -f "$CERT_PATH/${{ env.DOMAIN }}.cer" ]; then + echo "❌ 证书文件不存在" + ls -la $HOME/.acme.sh/ + exit 1 + fi + + echo "📜 证书路径: $CERT_PATH" + + # 读取证书内容 + CERT_CONTENT=$(cat "$CERT_PATH/${{ env.DOMAIN }}.cer" "$CERT_PATH/ca.cer" 2>/dev/null || cat "$CERT_PATH/fullchain.cer") + KEY_CONTENT=$(cat "$CERT_PATH/${{ env.DOMAIN }}.key") + + # 生成证书名称(包含时间戳) + CERT_NAME="cdn-xkcoding-$(date +%Y%m%d%H%M%S)" + + echo "🚀 部署证书到 CDN..." + echo " 证书名称: $CERT_NAME" + echo " 域名: ${{ env.DOMAIN }}" + + # 使用阿里云 CDN API 设置域名证书 + aliyun cdn SetDomainServerCertificate \ + --DomainName ${{ env.DOMAIN }} \ + --CertName "$CERT_NAME" \ + --CertType upload \ + --ServerCertificateStatus on \ + --ServerCertificate "$CERT_CONTENT" \ + --PrivateKey "$KEY_CONTENT" \ + --ForceSet 1 + + echo "✅ 证书部署成功!" + + - name: Verify HTTPS + run: | + echo "🔍 等待 CDN 配置生效..." + sleep 30 + + echo "🔍 验证 HTTPS 访问..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ env.DOMAIN }}/2017-07-12-14998277900006.jpg" || echo "000") + + if [ "$HTTP_CODE" = "200" ]; then + echo "✅ HTTPS 验证成功!" + else + echo "⚠️ HTTPS 返回状态码: $HTTP_CODE" + echo " CDN 可能需要更多时间生效,请稍后手动验证" + fi + + - name: Summary + if: always() + run: | + echo "## 🔐 CDN SSL 证书管理" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **时间**: $(date +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + echo "- **域名**: \`${{ env.DOMAIN }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **强制续期**: ${{ github.event.inputs.force_renew || 'false' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 9cad2b6a..bbf2d227 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,10 @@ hexo-backup/ .specstory/ .cursorindexingignore resources/ + +# image migration temp files +scripts/migrate-images/temp/ +scripts/migrate-images/reports/ + +# ossutil output +ossutil_output/ diff --git a/openspec/changes/unify-image-hosting/tasks.md b/openspec/changes/unify-image-hosting/tasks.md index 92178ee1..66db93c9 100644 --- a/openspec/changes/unify-image-hosting/tasks.md +++ b/openspec/changes/unify-image-hosting/tasks.md @@ -1,6 +1,9 @@ # Tasks: 统一博客图片托管至阿里云 OSS + SSL 证书管理优化 -## Phase 1: 环境准备 +> **执行顺序调整**:由于 `static.xkcoding.com` 域名目前在七牛云使用, +> 需要先完成图片下载和上传,再切换域名,避免迁移期间图片无法访问。 + +## Phase 1A: 环境准备(先行) - [ ] 1.1 创建阿里云 RAM 子账号 - 登录阿里云控制台 → RAM 访问控制 @@ -13,17 +16,6 @@ - `ALI_SECRET`: 阿里云 AccessKey Secret - `ACME_EMAIL`: 证书申请邮箱 -- [ ] 1.3 开通并配置阿里云 CDN - - 开通 CDN 服务(按流量计费) - - 添加加速域名 `static.xkcoding.com` - - 源站设置为 OSS: `xkcoding-blog.oss-cn-hangzhou.aliyuncs.com` - - 记录 CDN 分配的 CNAME 地址 - -- [ ] 1.4 配置 DNS 解析 - - 在阿里云 DNS 添加 CNAME 记录 - - `static` → CDN 分配的 CNAME 地址 - - 验证解析生效 - - [ ] 1.5 安装配置 ossutil(本地) - 下载 ossutil 命令行工具 - 使用 RAM 子账号的 AccessKey 配置 @@ -33,7 +25,21 @@ - 确认 Bucket 公开读权限 - 配置跨域规则(CORS)允许博客域名访问 -## Phase 2: SSL 工作流优化 +## Phase 1B: 域名切换(图片上传后执行) + +- [ ] 1.3 开通并配置阿里云 CDN + - 开通 CDN 服务(按流量计费) + - **先在七牛云删除 `static.xkcoding.com` 域名** + - 在阿里云 CDN 添加加速域名 `static.xkcoding.com` + - 源站设置为 OSS: `xkcoding-blog.oss-cn-hangzhou.aliyuncs.com` + - 记录 CDN 分配的 CNAME 地址 + +- [ ] 1.4 配置 DNS 解析 + - 在阿里云 DNS 添加/修改 CNAME 记录 + - `static` → CDN 分配的 CNAME 地址 + - 验证解析生效 + +## Phase 2: SSL 工作流优化(域名切换后执行) - [ ] 2.1 创建 CDN SSL 证书工作流 - 创建 `.github/workflows/cdn-ssl-renew.yml` @@ -134,28 +140,58 @@ - [ ] 两个工作流都可手动触发和定时执行 - [ ] 证书续期不再需要停止 nginx -## 依赖关系 +## 依赖关系(调整后) ``` -Phase 1 (环境准备) -1.1 ──▶ 1.2 ──▶ 2.1, 2.2 -1.3 ──▶ 1.4 ──▶ 2.1 +Phase 1A (环境准备 - 先行) +1.1 ──▶ 1.2 1.1 ──▶ 1.5 ──▶ 3.3 1.6 ──▶ 3.3 -Phase 2 (SSL 优化) - 可与 Phase 3 并行 -2.1 ──▶ 2.3 -2.2 ──▶ 2.3 +Phase 3 (脚本开发) ← 当前阶段 +3.1 ──▶ 3.2 ──▶ 3.3 ──▶ 3.4 -Phase 3 (脚本开发) -3.1 ──▶ 3.2 ──▶ 3.3 - │ -3.4 ◀─────────┘ +Phase 4A (下载和上传) +3.2 ──▶ 4.1 ──┬──▶ 4.3 +3.2 ──▶ 4.2 ──┘ -Phase 4 (执行迁移) -4.1 ──┬──▶ 4.3 ──▶ 4.4 -4.2 ──┘ +Phase 1B (域名切换) ← 图片上传完成后 +4.3 ──▶ 1.3 ──▶ 1.4 + +Phase 2 (SSL 优化) ← 域名切换后 +1.4 ──▶ 2.1 ──▶ 2.3 +1.2 ──▶ 2.2 ──▶ 2.3 + +Phase 4B (URL 替换) +1.4 ──▶ 4.4 Phase 5 (验证) 4.4 ──▶ 5.1 ──▶ 5.2 ──▶ 5.3 ──▶ 5.4 ``` + +### 执行流程图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Phase 1A + Phase 3: 环境准备 + 脚本开发 │ +│ (域名仍指向七牛云,图片可正常访问) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. Phase 4A: 下载七牛云图片 → 上传到阿里云 OSS → 验证 │ +│ (域名仍指向七牛云,双份图片保障) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. Phase 1B: 七牛云删除域名 → 阿里云 CDN 添加 → DNS 切换 │ +│ (切换窗口期,尽量快速完成) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. Phase 2: 申请 SSL 证书,部署到 CDN │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. Phase 4B + 5: 更新 Markdown URL + 验证 + 清理 │ +└─────────────────────────────────────────────────────────────────┘ +``` diff --git a/scripts/migrate-images/downloader.ts b/scripts/migrate-images/downloader.ts new file mode 100644 index 00000000..ad24a3c2 --- /dev/null +++ b/scripts/migrate-images/downloader.ts @@ -0,0 +1,232 @@ +/** + * 图片下载脚本 + * 批量下载图片到本地临时目录 + */ + +import fs from "node:fs"; +import path from "node:path"; +import https from "node:https"; +import http from "node:http"; +import type { ImageRecord, ScanResult } from "./types.js"; +import { defaultConfig } from "./types.js"; + +/** + * 从 URL 提取存储路径 + */ +function getStoragePath(url: string, sourceType: ImageRecord["sourceType"]): string { + if (sourceType === "qiniu") { + // https://static.xkcoding.com/2017-07-12-xxx.jpg → 2017-07-12-xxx.jpg + // https://static.xkcoding.com/blog/2019-08-09-xxx.png → blog/2019-08-09-xxx.png + const match = url.match(/static\.xkcoding\.com\/(.+?)(?:\?.*)?$/); + return match ? match[1] : path.basename(url); + } + + if (sourceType === "aliyun-old") { + // https://static.aliyun.xkcoding.com/2021/09/03/xxx.jpg → 2021/09/03/xxx.jpg + const match = url.match(/static\.aliyun\.xkcoding\.com\/(.+?)(?:\?.*)?$/); + return match ? match[1] : path.basename(url); + } + + if (sourceType === "relative") { + // /resources/xxx.png → resources/xxx.png + return url.replace(/^\//, ""); + } + + return path.basename(url); +} + +/** + * 下载单个文件 + */ +async function downloadFile(url: string, destPath: string): Promise { + return new Promise((resolve, reject) => { + const dir = path.dirname(destPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const protocol = url.startsWith("https") ? https : http; + const file = fs.createWriteStream(destPath); + + const request = protocol.get(url, (response) => { + // 处理重定向 + if (response.statusCode === 301 || response.statusCode === 302) { + const redirectUrl = response.headers.location; + if (redirectUrl) { + file.close(); + fs.unlinkSync(destPath); + downloadFile(redirectUrl, destPath).then(resolve).catch(reject); + return; + } + } + + if (response.statusCode !== 200) { + file.close(); + fs.unlinkSync(destPath); + reject(new Error(`HTTP ${response.statusCode}`)); + return; + } + + response.pipe(file); + + file.on("finish", () => { + file.close(); + resolve(); + }); + }); + + request.on("error", (err) => { + file.close(); + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + reject(err); + }); + + request.setTimeout(30000, () => { + request.destroy(); + reject(new Error("Timeout")); + }); + }); +} + +/** + * 批量下载图片 + */ +async function downloadImages( + images: ImageRecord[], + tempDir: string, + concurrency: number = 5 +): Promise<{ success: ImageRecord[]; failed: ImageRecord[] }> { + const success: ImageRecord[] = []; + const failed: ImageRecord[] = []; + + // 过滤掉相对路径图片(需要单独处理) + const downloadable = images.filter((img) => img.sourceType !== "relative"); + + console.log(`\n📥 开始下载 ${downloadable.length} 张图片(并发数: ${concurrency})...\n`); + + // 分批下载 + for (let i = 0; i < downloadable.length; i += concurrency) { + const batch = downloadable.slice(i, i + concurrency); + const promises = batch.map(async (img) => { + const storagePath = getStoragePath(img.sourceUrl, img.sourceType); + const localPath = path.join(tempDir, storagePath); + + // 断点续传:如果文件已存在且大小 > 0,跳过 + if (fs.existsSync(localPath) && fs.statSync(localPath).size > 0) { + console.log(` ⏭️ 跳过(已存在): ${storagePath}`); + img.localPath = localPath; + img.status = "downloaded"; + success.push(img); + return; + } + + try { + await downloadFile(img.sourceUrl, localPath); + img.localPath = localPath; + img.status = "downloaded"; + success.push(img); + console.log(` ✅ ${storagePath}`); + } catch (error) { + img.status = "failed"; + img.error = error instanceof Error ? error.message : String(error); + failed.push(img); + console.log(` ❌ ${storagePath} - ${img.error}`); + } + }); + + await Promise.all(promises); + } + + return { success, failed }; +} + +/** + * 保存下载结果 + */ +function saveDownloadResult( + success: ImageRecord[], + failed: ImageRecord[], + reportDir: string +): void { + if (!fs.existsSync(reportDir)) { + fs.mkdirSync(reportDir, { recursive: true }); + } + + const report = { + downloadTime: new Date().toISOString(), + totalDownloaded: success.length, + totalFailed: failed.length, + success, + failed, + }; + + fs.writeFileSync( + path.join(reportDir, "download-result.json"), + JSON.stringify(report, null, 2), + "utf-8" + ); +} + +/** + * 主函数 + */ +async function main() { + const reportDir = path.resolve(process.cwd(), defaultConfig.paths.reportDir); + const tempDir = path.resolve(process.cwd(), defaultConfig.paths.tempDir); + const scanResultPath = path.join(reportDir, "scan-result.json"); + + // 检查扫描结果是否存在 + if (!fs.existsSync(scanResultPath)) { + console.error("❌ 请先运行 scanner.ts 生成扫描结果"); + process.exit(1); + } + + const scanResult: ScanResult = JSON.parse(fs.readFileSync(scanResultPath, "utf-8")); + + console.log("📊 扫描结果统计:"); + console.log(` 总图片数: ${scanResult.totalImages}`); + console.log(` ├─ 七牛云: ${scanResult.bySource.qiniu}`); + console.log(` ├─ 阿里云旧域名: ${scanResult.bySource.aliyunOld}`); + console.log(` └─ 相对路径: ${scanResult.bySource.relative}(需手动处理)`); + + // 创建临时目录 + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // 下载图片(不包括相对路径) + const { success, failed } = await downloadImages(scanResult.images, tempDir); + + // 保存结果 + saveDownloadResult(success, failed, reportDir); + + // 输出统计 + console.log("\n📊 下载结果:"); + console.log(` 成功: ${success.length}`); + console.log(` 失败: ${failed.length}`); + + if (failed.length > 0) { + console.log("\n❌ 下载失败的图片:"); + failed.forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 错误: ${img.error}`); + }); + } + + // 提示相对路径图片 + const relativeImages = scanResult.images.filter((img) => img.sourceType === "relative"); + if (relativeImages.length > 0) { + console.log("\n⚠️ 相对路径图片需要手动处理:"); + relativeImages.forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 文件: ${path.relative(process.cwd(), img.markdownFile)}:${img.lineNumber}`); + }); + } + + console.log(`\n📄 下载结果已保存到: ${path.join(reportDir, "download-result.json")}`); + console.log(`📁 图片已保存到: ${tempDir}`); +} + +main().catch(console.error); diff --git a/scripts/migrate-images/index.ts b/scripts/migrate-images/index.ts new file mode 100644 index 00000000..dff318e8 --- /dev/null +++ b/scripts/migrate-images/index.ts @@ -0,0 +1,41 @@ +/** + * 图片迁移脚本 - 主入口 + * + * 使用方法: + * pnpm migrate:scan # 扫描所有图片 + * pnpm migrate:download # 下载图片到本地 + * pnpm migrate:upload # 上传到 OSS + * pnpm migrate:replace # 替换 URL(预览) + * pnpm migrate:replace:exec # 替换 URL(执行) + * + * 或直接运行: + * pnpm tsx scripts/migrate-images/scanner.ts + * pnpm tsx scripts/migrate-images/downloader.ts + * pnpm tsx scripts/migrate-images/uploader.ts + * pnpm tsx scripts/migrate-images/replacer.ts --dry-run + * pnpm tsx scripts/migrate-images/replacer.ts --execute + */ + +console.log(` +╔══════════════════════════════════════════════════════════════╗ +║ 博客图片迁移工具 - 阿里云 OSS ║ +╠══════════════════════════════════════════════════════════════╣ +║ ║ +║ 步骤 1: 扫描图片 ║ +║ pnpm tsx scripts/migrate-images/scanner.ts ║ +║ ║ +║ 步骤 2: 下载图片 ║ +║ pnpm tsx scripts/migrate-images/downloader.ts ║ +║ ║ +║ 步骤 3: 上传到 OSS(需先配置 ossutil) ║ +║ pnpm tsx scripts/migrate-images/uploader.ts ║ +║ ║ +║ 步骤 4: 替换 URL ║ +║ pnpm tsx scripts/migrate-images/replacer.ts --dry-run ║ +║ pnpm tsx scripts/migrate-images/replacer.ts --execute ║ +║ ║ +╚══════════════════════════════════════════════════════════════╝ + +📁 报告输出目录: scripts/migrate-images/reports/ +📁 临时文件目录: scripts/migrate-images/temp/ +`); diff --git a/scripts/migrate-images/replacer.ts b/scripts/migrate-images/replacer.ts new file mode 100644 index 00000000..69d9ec89 --- /dev/null +++ b/scripts/migrate-images/replacer.ts @@ -0,0 +1,252 @@ +/** + * URL 替换脚本 + * 批量替换 Markdown 文件中的图片 URL + */ + +import fs from "node:fs"; +import path from "node:path"; +import type { ImageRecord } from "./types.js"; +import { defaultConfig } from "./types.js"; + +interface UploadResult { + uploadTime: string; + totalUploaded: number; + totalFailed: number; + images: ImageRecord[]; +} + +interface ReplacementRecord { + file: string; + lineNumber: number; + oldUrl: string; + newUrl: string; +} + +/** + * 生成 URL 映射表 + */ +function generateUrlMapping(images: ImageRecord[]): Map { + const mapping = new Map(); + const targetDomain = defaultConfig.target.domain; + + for (const img of images) { + if (img.status !== "uploaded" || !img.ossPath) continue; + + const newUrl = `https://${targetDomain}/${img.ossPath}`; + + if (img.sourceType === "qiniu") { + // 七牛云 URL 可能是 http 或 https + mapping.set(img.sourceUrl, newUrl); + // 同时处理 http 版本 + if (img.sourceUrl.startsWith("https://")) { + mapping.set(img.sourceUrl.replace("https://", "http://"), newUrl); + } + } else if (img.sourceType === "aliyun-old") { + // 阿里云旧域名,需要去掉查询参数后再映射 + mapping.set(img.sourceUrl, newUrl); + } else if (img.sourceType === "relative") { + // 相对路径转为绝对路径 + mapping.set(img.sourceUrl, newUrl); + } + } + + return mapping; +} + +/** + * 预览替换(dry-run) + */ +function previewReplacements( + images: ImageRecord[], + urlMapping: Map +): ReplacementRecord[] { + const replacements: ReplacementRecord[] = []; + + // 按文件分组 + const fileGroups = new Map(); + for (const img of images) { + if (!fileGroups.has(img.markdownFile)) { + fileGroups.set(img.markdownFile, []); + } + fileGroups.get(img.markdownFile)!.push(img); + } + + // 遍历每个文件 + for (const [filePath, fileImages] of fileGroups) { + for (const img of fileImages) { + const newUrl = urlMapping.get(img.sourceUrl); + if (newUrl && newUrl !== img.sourceUrl) { + replacements.push({ + file: filePath, + lineNumber: img.lineNumber, + oldUrl: img.sourceUrl, + newUrl, + }); + } + } + } + + return replacements; +} + +/** + * 执行替换 + */ +function executeReplacements( + replacements: ReplacementRecord[], + urlMapping: Map +): { success: number; failed: number } { + let success = 0; + let failed = 0; + + // 按文件分组 + const fileGroups = new Map(); + for (const r of replacements) { + if (!fileGroups.has(r.file)) { + fileGroups.set(r.file, []); + } + fileGroups.get(r.file)!.push(r); + } + + // 遍历每个文件 + for (const [filePath, fileReplacements] of fileGroups) { + try { + let content = fs.readFileSync(filePath, "utf-8"); + let modified = false; + + for (const r of fileReplacements) { + if (content.includes(r.oldUrl)) { + content = content.split(r.oldUrl).join(r.newUrl); + modified = true; + success++; + } else { + console.log(` ⚠️ 未找到: ${r.oldUrl}`); + console.log(` 文件: ${path.relative(process.cwd(), filePath)}`); + failed++; + } + } + + if (modified) { + fs.writeFileSync(filePath, content, "utf-8"); + console.log(` ✅ 已更新: ${path.relative(process.cwd(), filePath)}`); + } + } catch (error) { + console.error(` ❌ 处理失败: ${filePath}`); + console.error(` ${error instanceof Error ? error.message : String(error)}`); + failed += fileReplacements.length; + } + } + + return { success, failed }; +} + +/** + * 保存替换报告 + */ +function saveReplacementReport( + replacements: ReplacementRecord[], + result: { success: number; failed: number }, + reportDir: string +): void { + const report = { + replaceTime: new Date().toISOString(), + totalReplacements: replacements.length, + success: result.success, + failed: result.failed, + replacements, + }; + + fs.writeFileSync( + path.join(reportDir, "replacement-result.json"), + JSON.stringify(report, null, 2), + "utf-8" + ); +} + +/** + * 主函数 + */ +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run") || args.includes("-d"); + + const reportDir = path.resolve(process.cwd(), defaultConfig.paths.reportDir); + const uploadResultPath = path.join(reportDir, "upload-result.json"); + + // 检查上传结果是否存在 + if (!fs.existsSync(uploadResultPath)) { + console.error("❌ 请先运行 uploader.ts 上传图片"); + process.exit(1); + } + + const uploadResult: UploadResult = JSON.parse( + fs.readFileSync(uploadResultPath, "utf-8") + ); + + console.log("📊 上传结果统计:"); + console.log(` 已上传: ${uploadResult.totalUploaded}`); + console.log(` 失败: ${uploadResult.totalFailed}`); + + // 生成 URL 映射 + const urlMapping = generateUrlMapping(uploadResult.images); + console.log(`\n🔗 URL 映射数量: ${urlMapping.size}`); + + // 预览替换 + const replacements = previewReplacements(uploadResult.images, urlMapping); + console.log(`📝 需要替换的 URL: ${replacements.length}`); + + if (replacements.length === 0) { + console.log("\n✅ 没有需要替换的 URL"); + return; + } + + if (dryRun) { + console.log("\n🔍 预览模式(dry-run):\n"); + + // 按文件分组显示 + const fileGroups = new Map(); + for (const r of replacements) { + if (!fileGroups.has(r.file)) { + fileGroups.set(r.file, []); + } + fileGroups.get(r.file)!.push(r); + } + + for (const [filePath, fileReplacements] of fileGroups) { + console.log(`📄 ${path.relative(process.cwd(), filePath)}:`); + for (const r of fileReplacements) { + console.log(` L${r.lineNumber}: ${r.oldUrl}`); + console.log(` → ${r.newUrl}`); + } + console.log(); + } + + console.log("💡 使用 --execute 或 -e 参数执行实际替换"); + console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute"); + + // 保存预览报告 + saveReplacementReport(replacements, { success: 0, failed: 0 }, reportDir); + console.log(`\n📄 预览报告已保存到: ${path.join(reportDir, "replacement-result.json")}`); + } else if (args.includes("--execute") || args.includes("-e")) { + console.log("\n🔄 执行替换...\n"); + + const result = executeReplacements(replacements, urlMapping); + + console.log("\n📊 替换结果:"); + console.log(` 成功: ${result.success}`); + console.log(` 失败: ${result.failed}`); + + // 保存替换报告 + saveReplacementReport(replacements, result, reportDir); + console.log(`\n📄 替换报告已保存到: ${path.join(reportDir, "replacement-result.json")}`); + } else { + console.log("\n用法:"); + console.log(" --dry-run, -d 预览替换(不修改文件)"); + console.log(" --execute, -e 执行替换"); + console.log("\n示例:"); + console.log(" pnpm tsx scripts/migrate-images/replacer.ts --dry-run"); + console.log(" pnpm tsx scripts/migrate-images/replacer.ts --execute"); + } +} + +main().catch(console.error); diff --git a/scripts/migrate-images/scanner.ts b/scripts/migrate-images/scanner.ts new file mode 100644 index 00000000..00afd967 --- /dev/null +++ b/scripts/migrate-images/scanner.ts @@ -0,0 +1,204 @@ +/** + * 图片扫描脚本 + * 扫描所有 Markdown 文件,提取图片 URL + */ + +import fs from "node:fs"; +import path from "node:path"; +import type { ImageRecord, ScanResult, MigrationConfig } from "./types.js"; +import { defaultConfig } from "./types.js"; + +/** + * 递归获取目录下所有 Markdown 文件 + */ +function getMarkdownFiles(dir: string): string[] { + const files: string[] = []; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...getMarkdownFiles(fullPath)); + } else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) { + files.push(fullPath); + } + } + + return files; +} + +/** + * 从 Markdown 文件中提取图片 URL + */ +function extractImagesFromFile( + filePath: string, + config: MigrationConfig +): ImageRecord[] { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + const images: ImageRecord[] = []; + const seenUrls = new Set(); + + lines.forEach((line, index) => { + // 七牛云图片 + const qiniuMatches = line.matchAll(config.sourcePatterns.qiniu); + for (const match of qiniuMatches) { + const url = match[0]; + if (!seenUrls.has(url)) { + seenUrls.add(url); + images.push({ + sourceUrl: url, + sourceType: "qiniu", + markdownFile: filePath, + lineNumber: index + 1, + context: line.trim().substring(0, 200), + status: "pending", + }); + } + } + + // 阿里云旧域名图片 + const aliyunMatches = line.matchAll(config.sourcePatterns.aliyunOld); + for (const match of aliyunMatches) { + const url = match[0]; + if (!seenUrls.has(url)) { + seenUrls.add(url); + images.push({ + sourceUrl: url, + sourceType: "aliyun-old", + markdownFile: filePath, + lineNumber: index + 1, + context: line.trim().substring(0, 200), + status: "pending", + }); + } + } + + // 相对路径图片 + const relativeMatches = line.matchAll(config.sourcePatterns.relative); + for (const match of relativeMatches) { + const url = match[0]; + if (!seenUrls.has(url)) { + seenUrls.add(url); + images.push({ + sourceUrl: url, + sourceType: "relative", + markdownFile: filePath, + lineNumber: index + 1, + context: line.trim().substring(0, 200), + status: "pending", + }); + } + } + }); + + return images; +} + +/** + * 扫描所有 Markdown 文件 + */ +export function scanImages(config: MigrationConfig = defaultConfig): ScanResult { + const blogDir = path.resolve(process.cwd(), config.paths.blogDir); + + if (!fs.existsSync(blogDir)) { + throw new Error(`Blog directory not found: ${blogDir}`); + } + + const markdownFiles = getMarkdownFiles(blogDir); + const allImages: ImageRecord[] = []; + + for (const file of markdownFiles) { + const images = extractImagesFromFile(file, config); + allImages.push(...images); + } + + // 统计 + const bySource = { + qiniu: allImages.filter((img) => img.sourceType === "qiniu").length, + aliyunOld: allImages.filter((img) => img.sourceType === "aliyun-old").length, + relative: allImages.filter((img) => img.sourceType === "relative").length, + }; + + return { + totalImages: allImages.length, + bySource, + images: allImages, + scannedFiles: markdownFiles.length, + scanTime: new Date().toISOString(), + }; +} + +/** + * 保存扫描结果到 JSON 文件 + */ +export function saveScanResult(result: ScanResult, outputPath: string): void { + const dir = path.dirname(outputPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8"); +} + +/** + * 主函数 + */ +function main() { + console.log("🔍 开始扫描 Markdown 文件中的图片...\n"); + + const result = scanImages(); + + // 保存结果 + const reportDir = path.resolve(process.cwd(), defaultConfig.paths.reportDir); + const outputPath = path.join(reportDir, "scan-result.json"); + saveScanResult(result, outputPath); + + // 输出统计 + console.log("📊 扫描结果:"); + console.log(` 扫描文件数: ${result.scannedFiles}`); + console.log(` 图片总数: ${result.totalImages}`); + console.log(` ├─ 七牛云: ${result.bySource.qiniu}`); + console.log(` ├─ 阿里云旧域名: ${result.bySource.aliyunOld}`); + console.log(` └─ 相对路径: ${result.bySource.relative}`); + console.log(`\n📄 结果已保存到: ${outputPath}`); + + // 按来源分组显示示例 + console.log("\n📋 图片列表预览:\n"); + + const qiniuImages = result.images.filter((img) => img.sourceType === "qiniu"); + if (qiniuImages.length > 0) { + console.log("【七牛云图片】(前 5 条):"); + qiniuImages.slice(0, 5).forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 文件: ${path.relative(process.cwd(), img.markdownFile)}:${img.lineNumber}`); + }); + if (qiniuImages.length > 5) { + console.log(` ... 还有 ${qiniuImages.length - 5} 条\n`); + } else { + console.log(); + } + } + + const aliyunImages = result.images.filter((img) => img.sourceType === "aliyun-old"); + if (aliyunImages.length > 0) { + console.log("【阿里云旧域名图片】:"); + aliyunImages.forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 文件: ${path.relative(process.cwd(), img.markdownFile)}:${img.lineNumber}`); + }); + console.log(); + } + + const relativeImages = result.images.filter((img) => img.sourceType === "relative"); + if (relativeImages.length > 0) { + console.log("【相对路径图片】:"); + relativeImages.forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 文件: ${path.relative(process.cwd(), img.markdownFile)}:${img.lineNumber}`); + }); + console.log(); + } +} + +// 直接运行时执行 main +main(); diff --git a/scripts/migrate-images/types.ts b/scripts/migrate-images/types.ts new file mode 100644 index 00000000..888b830a --- /dev/null +++ b/scripts/migrate-images/types.ts @@ -0,0 +1,85 @@ +/** + * 图片记录类型 + */ +export interface ImageRecord { + // 原始信息 + sourceUrl: string; + sourceType: "qiniu" | "aliyun-old" | "relative"; + markdownFile: string; + lineNumber: number; + context: string; // 图片所在的 markdown 行 + + // 迁移状态 + localPath?: string; + targetUrl?: string; + ossPath?: string; + status: "pending" | "downloaded" | "uploaded" | "replaced" | "failed"; + error?: string; +} + +/** + * 扫描结果 + */ +export interface ScanResult { + totalImages: number; + bySource: { + qiniu: number; + aliyunOld: number; + relative: number; + }; + images: ImageRecord[]; + scannedFiles: number; + scanTime: string; +} + +/** + * 迁移配置 + */ +export interface MigrationConfig { + // 源模式 + sourcePatterns: { + qiniu: RegExp; + aliyunOld: RegExp; + relative: RegExp; + }; + + // 目标配置 + target: { + domain: string; + bucket: string; + region: string; + }; + + // 路径配置 + paths: { + blogDir: string; + tempDir: string; + reportDir: string; + }; +} + +/** + * 默认配置 + */ +export const defaultConfig: MigrationConfig = { + sourcePatterns: { + // 七牛云: http(s)://static.xkcoding.com/xxx + qiniu: /https?:\/\/static\.xkcoding\.com\/[^\s\)\"\']+/g, + // 阿里云旧域名: http(s)://static.aliyun.xkcoding.com/xxx + aliyunOld: /https?:\/\/static\.aliyun\.xkcoding\.com\/[^\s\)\"\']+/g, + // 相对路径: /resources/xxx + relative: /(? img.status === "uploaded").length, + totalFailed: images.filter((img) => img.status === "failed").length, + images, + }; + + fs.writeFileSync( + path.join(reportDir, "upload-result.json"), + JSON.stringify(report, null, 2), + "utf-8" + ); +} + +/** + * 主函数 + */ +async function main() { + const reportDir = path.resolve(process.cwd(), defaultConfig.paths.reportDir); + const tempDir = path.resolve(process.cwd(), defaultConfig.paths.tempDir); + const downloadResultPath = path.join(reportDir, "download-result.json"); + + // 检查 ossutil + if (!checkOssutil()) { + console.error("❌ ossutil 未安装或未配置"); + console.error("\n请先安装并配置 ossutil:"); + console.error(" brew install aliyun-cli"); + console.error(" ossutil config"); + console.error("\n或参考文档: https://help.aliyun.com/document_detail/120075.html"); + process.exit(1); + } + + console.log("✅ ossutil 已安装\n"); + + // 检查下载结果是否存在 + if (!fs.existsSync(downloadResultPath)) { + console.error("❌ 请先运行 downloader.ts 下载图片"); + process.exit(1); + } + + const downloadResult: DownloadResult = JSON.parse( + fs.readFileSync(downloadResultPath, "utf-8") + ); + + console.log("📊 下载结果统计:"); + console.log(` 已下载: ${downloadResult.totalDownloaded}`); + console.log(` 失败: ${downloadResult.totalFailed}`); + + // 检查临时目录是否有文件 + if (!fs.existsSync(tempDir)) { + console.error(`\n❌ 临时目录不存在: ${tempDir}`); + process.exit(1); + } + + const { bucket, region } = defaultConfig.target; + + // 批量上传整个目录 + const uploadResult = uploadDirectory(tempDir, bucket); + + if (uploadResult.failed !== 0) { + console.log("\n⚠️ 部分文件上传可能失败,请检查上面的错误信息"); + } + + // 验证上传结果 + const failedVerify = verifyUpload(downloadResult.success, bucket, region); + + // 保存上传结果 + saveUploadResult(downloadResult.success, reportDir); + + // 输出统计 + const uploaded = downloadResult.success.filter((img) => img.status === "uploaded").length; + console.log("\n📊 验证结果:"); + console.log(` 成功: ${uploaded}`); + console.log(` 失败: ${failedVerify.length}`); + + if (failedVerify.length > 0) { + console.log("\n❌ 验证失败的图片:"); + failedVerify.forEach((img, i) => { + console.log(` ${i + 1}. ${img.sourceUrl}`); + console.log(` 本地: ${img.localPath}`); + }); + } + + console.log(`\n📄 上传结果已保存到: ${path.join(reportDir, "upload-result.json")}`); + + // 输出 OSS 访问示例 + console.log("\n🔗 OSS 访问示例:"); + const exampleImage = downloadResult.success.find((img) => img.status === "uploaded"); + if (exampleImage && exampleImage.ossPath) { + console.log(` OSS 直接访问: https://${bucket}.${region}.aliyuncs.com/${exampleImage.ossPath}`); + console.log(` CDN 访问(切换后): https://${defaultConfig.target.domain}/${exampleImage.ossPath}`); + } +} + +main().catch(console.error); diff --git a/src/data/blog/2017-07-12.jdk-map-source-explore.md b/src/data/blog/2017-07-12.jdk-map-source-explore.md index 433fd31c..eaa3c23c 100644 --- a/src/data/blog/2017-07-12.jdk-map-source-explore.md +++ b/src/data/blog/2017-07-12.jdk-map-source-explore.md @@ -33,7 +33,7 @@ for (Object object : set) { 1. 首先,我们看 `map` 对象,这个 `map` 对象是 `HashMap` 的一个实例,然后下面的 `Set set = map.entrySet();` 可以知道这其实用的 `HashMap` 实现的 `entrySet()` 方法,然后我们可以查看 `HashMap` 里 `entrySet()` 的源码 - ![](https://static.xkcoding.com/2017-07-12-14998277900006.jpg) + ![](https://cdn.xkcoding.com/2017-07-12-14998277900006.jpg) 从源码可以看出,这里的返回了一个 `EntrySet` 对象,但是需要注意的是这个 `EntrySet` 是 `HashMap` 里的一个内部类,源码如下: diff --git a/src/data/blog/2017-07-19.spring-boot-mybatis-logback.md b/src/data/blog/2017-07-19.spring-boot-mybatis-logback.md index 2252aa53..15e53c28 100644 --- a/src/data/blog/2017-07-19.spring-boot-mybatis-logback.md +++ b/src/data/blog/2017-07-19.spring-boot-mybatis-logback.md @@ -130,7 +130,7 @@ description: "Spring Boot 整合 MyBatis 后,SQL 语句死活打印不出来 ``` 7. 在 idea 里安装 `lombok` 的相应插件 - ![lombok插件](https://static.xkcoding.com/2017-07-19-15004348921948.jpg) + ![lombok插件](https://cdn.xkcoding.com/2017-07-19-15004348921948.jpg) 8. 然后就可以在类名上方添加 `@Slf4j` 注解,这样就可以直接在代码中使用 `log.info("....")` 打印日志了。 diff --git a/src/data/blog/2017-08-02.customize-blocked-queue.md b/src/data/blog/2017-08-02.customize-blocked-queue.md index d35c1c09..3e4d940a 100644 --- a/src/data/blog/2017-08-02.customize-blocked-queue.md +++ b/src/data/blog/2017-08-02.customize-blocked-queue.md @@ -158,9 +158,9 @@ description: "想理解 Java 并发编程里的 wait() 和 notify()?最好的 3. 启动测试类,查看运行结果。控制台如果应该出现的效果是,队列先初始化完成,然后休眠2s,接下来先取数据,再插入数据,则证明阻塞队列生效。下面是控制台运行的效果: - 初始化队列 - ![初始化队列](https://static.xkcoding.com/2017-08-02-15016611637131.jpg) + ![初始化队列](https://cdn.xkcoding.com/2017-08-02-15016611637131.jpg) - 休眠2s后取队首元素,再插入队尾元素 - ![取队首元素,插入队尾元素](https://static.xkcoding.com/2017-08-02-15016613283795.jpg) + ![取队首元素,插入队尾元素](https://cdn.xkcoding.com/2017-08-02-15016613283795.jpg) - 此时我们会发现,程序还未停止,因为此时队列已满,但是线程 `t1` 还未插入 `h` 元素,因此线程被阻塞着,直至下次队列有空余空间才会被唤醒。 4. 至此,一个自定义阻塞队列就已经实现了。 5. 细心的朋友会发现,我打印的 log 里用到了一个 `StrUtil.format()` 方法,这个和 `slf4j` 的 log 用法一致,可以使用占位符。这个是用到了一个国产良心工具类,[hutool](http://www.hutool.cn/),国产开源,需要大家的支持,觉得好用的话,期望可以去 [码云](https://github.com/looly/hutool) 或者 [github](http://git.oschina.net/loolly/hutool) 上给个 Star 吧! diff --git a/src/data/blog/2017-08-31.mysql-start-error.md b/src/data/blog/2017-08-31.mysql-start-error.md index 4db5a372..df117c47 100644 --- a/src/data/blog/2017-08-31.mysql-start-error.md +++ b/src/data/blog/2017-08-31.mysql-start-error.md @@ -31,7 +31,7 @@ description: "Windows 服务器上 MySQL 启动失败,报「系统错误 1067 但是我的 `my.ini` 原本就已经加入了这两个配置,仍然启动错误。 排查原因,发现是 mysql 的 `3306` 端口被IEXPLORE.exe占用了 -![](https://static.xkcoding.com/2017-08-31-15041452096442.jpg) +![](https://cdn.xkcoding.com/2017-08-31-15041452096442.jpg) ```bash # windows 下查看端口占用 diff --git a/src/data/blog/2017-11-08.dubbo-admin-install.md b/src/data/blog/2017-11-08.dubbo-admin-install.md index 0a077c72..f2675d53 100644 --- a/src/data/blog/2017-11-08.dubbo-admin-install.md +++ b/src/data/blog/2017-11-08.dubbo-admin-install.md @@ -113,12 +113,12 @@ description: "Dubbo 服务部署后如何监控和治理?dubbo-admin 是官方 7. 配置、调优结束,我们就可以运行 `tomcat/bin/start.sh` 或者 `tomcat/bin/start.bat`,启动 `Tomcat` 查看效果了,因为现在只是搭建了 `dubbo-admin` 还没有 `dubbo` 服务,因此我们现在只浏览首页和一些系统页面。 - 浏览器输入 `http://localhost:7777` 弹出验证信息。 - ![登录信息](https://static.xkcoding.com/2017-11-08-15101329196775.jpg) + ![登录信息](https://cdn.xkcoding.com/2017-11-08-15101329196775.jpg) - `dubbo-admin` 首页 - ![首页](https://static.xkcoding.com/2017-11-08-15101329460942.jpg) + ![首页](https://cdn.xkcoding.com/2017-11-08-15101329460942.jpg) - 系统状态 - ![系统状态](https://static.xkcoding.com/2017-11-08-15101329647345.jpg) + ![系统状态](https://cdn.xkcoding.com/2017-11-08-15101329647345.jpg) - 系统日志 - ![系统日志](https://static.xkcoding.com/2017-11-08-15101329831583.jpg) + ![系统日志](https://cdn.xkcoding.com/2017-11-08-15101329831583.jpg) - 系统环境 - ![系统环境](https://static.xkcoding.com/2017-11-08-15101329768536.jpg) + ![系统环境](https://cdn.xkcoding.com/2017-11-08-15101329768536.jpg) diff --git a/src/data/blog/2017-11-14.mysql-chinese-character-encoding.md b/src/data/blog/2017-11-14.mysql-chinese-character-encoding.md index 1464fc9f..25969647 100644 --- a/src/data/blog/2017-11-14.mysql-chinese-character-encoding.md +++ b/src/data/blog/2017-11-14.mysql-chinese-character-encoding.md @@ -144,7 +144,7 @@ export $LANG; 笔者这次遇到的问题就属于最后一种,前面的数据库配置,查看的编码都是正确的,并且使用程序查询出来的也没有乱码,偏偏 Navicat 显示数据表时中文便乱码了,那么请新建mysql服务器连接,Encoding那一栏不要选择默认的 UTF-8,选择Auto即可正常显示中文。 -![](https://static.xkcoding.com/2017-11-14-15106251909957.jpg) +![](https://cdn.xkcoding.com/2017-11-14-15106251909957.jpg) --- diff --git a/src/data/blog/2018-01-03.devops-gitlab.md b/src/data/blog/2018-01-03.devops-gitlab.md index f11b3476..c4550fe2 100644 --- a/src/data/blog/2018-01-03.devops-gitlab.md +++ b/src/data/blog/2018-01-03.devops-gitlab.md @@ -107,9 +107,9 @@ user['git_user_email'] = "237497819@qq.com" 5. 重新启动查看效果:`gitlab-ctl start` - ![个人首页](https://static.xkcoding.com/2018-01-03-15149489321563.jpg) + ![个人首页](https://cdn.xkcoding.com/2018-01-03-15149489321563.jpg) - ![管理员区域](https://static.xkcoding.com/2018-01-03-15149489565891.jpg) + ![管理员区域](https://cdn.xkcoding.com/2018-01-03-15149489565891.jpg) ## 3. 日常 GitLab 管理 @@ -165,12 +165,12 @@ gitlab-ctl start|stop|status|restart 4. User Settings --> Notifications 里 email保证正确,选择 level 为第一个,并勾选接收通知如下图 - ![6801515572021_.pic_hd](https://static.xkcoding.com/2018-01-10-6801515572021_.pic_hd.jpg) + ![6801515572021_.pic_hd](https://cdn.xkcoding.com/2018-01-10-6801515572021_.pic_hd.jpg) 5. 创建一个项目 6. Settings --> Integrations --> email on push,具体如下图 - ![6831515572084_.pic_hd](https://static.xkcoding.com/2018-01-10-6831515572084_.pic_hd.jpg) + ![6831515572084_.pic_hd](https://cdn.xkcoding.com/2018-01-10-6831515572084_.pic_hd.jpg) 7. ❤️enjoy it ~~ diff --git a/src/data/blog/2018-01-12.elasticsearch-note.md b/src/data/blog/2018-01-12.elasticsearch-note.md index 4b539052..1cdebe66 100644 --- a/src/data/blog/2018-01-12.elasticsearch-note.md +++ b/src/data/blog/2018-01-12.elasticsearch-note.md @@ -54,7 +54,7 @@ description: "从零到一的 ElasticSearch 完整学习笔记!覆盖单机安 6. 测试,打开 PostMan 测试 http://localhost:9200 ,查看返回的 JSON 信息 - ![PostMan测试单机部署结果](https://static.xkcoding.com/2018-01-12-15157443065618.jpg) + ![PostMan测试单机部署结果](https://cdn.xkcoding.com/2018-01-12-15157443065618.jpg) ### 1.2. 分布式安装 @@ -117,7 +117,7 @@ description: "从零到一的 ElasticSearch 完整学习笔记!覆盖单机安 5. 启动 head 插件,打开浏览器,访问 http://localhost:9100,查看分布式节点的运行状态 - ![分布式 ES 的集群状态](https://static.xkcoding.com/2018-01-12-15157518741687.jpg) + ![分布式 ES 的集群状态](https://cdn.xkcoding.com/2018-01-12-15157518741687.jpg) ### 1.3. 插件安装 @@ -183,7 +183,7 @@ description: "从零到一的 ElasticSearch 完整学习笔记!覆盖单机安 6. 打开浏览器,访问 http://localhost:9100/ ,查看 head 插件运行效果 - ![运行 head 插件,查看单机 ES 状态](https://static.xkcoding.com/2018-01-12-15157472517495.jpg) + ![运行 head 插件,查看单机 ES 状态](https://cdn.xkcoding.com/2018-01-12-15157472517495.jpg) ##### 集成安装 @@ -398,7 +398,7 @@ API 请求方式:GET、POST、PUT、DELETE 如图所示: -![创建索引](https://static.xkcoding.com/2018-01-17-15161702463041.jpg) +![创建索引](https://cdn.xkcoding.com/2018-01-17-15161702463041.jpg) #### 删除 @@ -452,7 +452,7 @@ API 请求方式:GET、POST、PUT、DELETE 如图所示: -![指定 id 插入](https://static.xkcoding.com/2018-01-17-15161719204773.jpg) +![指定 id 插入](https://cdn.xkcoding.com/2018-01-17-15161719204773.jpg) #### 不指定 ID 插入 @@ -490,11 +490,11 @@ API 请求方式:GET、POST、PUT、DELETE 如图所示: -![不指定 id 插入](https://static.xkcoding.com/2018-01-17-15161725779321.jpg) +![不指定 id 插入](https://cdn.xkcoding.com/2018-01-17-15161725779321.jpg) 然后前往 head 插件查看是否插入数据 -![head 插件中查看添加的文档记录](https://static.xkcoding.com/2018-01-17-15161727420397.jpg) +![head 插件中查看添加的文档记录](https://cdn.xkcoding.com/2018-01-17-15161727420397.jpg) ### 4.4. 修改 @@ -537,7 +537,7 @@ API 请求方式:GET、POST、PUT、DELETE 如图所示: -![修改文档](https://static.xkcoding.com/2018-01-17-15161741012942.jpg) +![修改文档](https://cdn.xkcoding.com/2018-01-17-15161741012942.jpg) ### 4.5. 删除 @@ -566,7 +566,7 @@ API 请求方式:GET、POST、PUT、DELETE ES 正如其名,本就是为了 Search 而生,所以查询是 ES 最牛逼的地方。使用上面提到的插入语法,预先插入一堆原始数据,用 head 查看。 -![预先插入的原始数据](https://static.xkcoding.com/2018-01-17-15161767712389.jpg) +![预先插入的原始数据](https://cdn.xkcoding.com/2018-01-17-15161767712389.jpg) #### 简单查询 diff --git a/src/data/blog/2018-11-07.spring-boot-start.md b/src/data/blog/2018-11-07.spring-boot-start.md index 7817b6ec..9ed39e6f 100644 --- a/src/data/blog/2018-11-07.spring-boot-start.md +++ b/src/data/blog/2018-11-07.spring-boot-start.md @@ -12,7 +12,7 @@ description: "还在被 Spring 的 XML 配置和依赖版本冲突折磨?Sprin Spring Boot 是Spring项目中的一个子工程,与我们所熟知的Spring-framework 同属于spring的产品: - + 官网这个logo下有这么一段介绍: @@ -63,19 +63,19 @@ Spring Boot 主要目标是: 1. 在IDEA中选择 `Spring Initializr` - + 2. 配置项目基础信息 - + 3. 选择依赖,也可以后续在 pom.xml 里手动添加 - + 4. 配置项目路径 - + 5. FINISH! @@ -83,7 +83,7 @@ Spring Boot 主要目标是: Spring Boot项目通过main函数即可启动,刚刚创建工程的时候已经自动创建了一个启动类: - + 然后查看启动类: @@ -103,7 +103,7 @@ public class DemoApplication { 我们编写一个Controller: - + ```java @RestController @@ -120,13 +120,13 @@ public class HelloController { 接下来,我们运行main函数,查看控制台: - + - ①是Spring MVC的映射路径 - ②是Tomcat监听的端口是8080 打开页面访问:http://localhost:8080/hello - + 测试成功了! diff --git a/src/data/blog/2019-01-01.bye2018-hello2019.md b/src/data/blog/2019-01-01.bye2018-hello2019.md index 1119e168..f58653b7 100644 --- a/src/data/blog/2019-01-01.bye2018-hello2019.md +++ b/src/data/blog/2019-01-01.bye2018-hello2019.md @@ -40,7 +40,7 @@ swarm,k8s,rancher自己瞎玩的也还算顺溜。 ### 1.5. 开源方面 -![2018年github贡献率](https://static.xkcoding.com/blog/bye2018-hello2019/image-2018-github-contributions.png) +![2018年github贡献率](https://cdn.xkcoding.com/blog/bye2018-hello2019/image-2018-github-contributions.png) 今年最大的坚持应该就是spring-boot-demo,地址: ,稍微介绍下吧,基于SpringBoot版本2.1.0.RELEASE版本,里面总共包含47个demo,当然都不仅仅是简单的demo而已,从基础的功能集成,到定时任务的管理,再到大家比较关注的前后端分离的动态权限管理、工作流等等都有涉及,希望可以做成入门SpringBoot最快,集成最简单的demo。 diff --git a/src/data/blog/2019-01-14.solve-mac-install-minikube-problem.md b/src/data/blog/2019-01-14.solve-mac-install-minikube-problem.md index fa20bed0..df899972 100644 --- a/src/data/blog/2019-01-14.solve-mac-install-minikube-problem.md +++ b/src/data/blog/2019-01-14.solve-mac-install-minikube-problem.md @@ -140,4 +140,4 @@ kubernetes (本文后续简称 k8s ),是一个开源的,用于管理云平 2. `minikube dashboard` 之后,启动 dashboad 截图: - ![dashboard](https://static.xkcoding.com/blog/solve-mac-install-minikube-problem/image-minikube-dashboard.png) + ![dashboard](https://cdn.xkcoding.com/blog/solve-mac-install-minikube-problem/image-minikube-dashboard.png) diff --git a/src/data/blog/2019-01-17.solve-nginx-413-problem.md b/src/data/blog/2019-01-17.solve-nginx-413-problem.md index 2578d84d..b43a01af 100644 --- a/src/data/blog/2019-01-17.solve-nginx-413-problem.md +++ b/src/data/blog/2019-01-17.solve-nginx-413-problem.md @@ -12,7 +12,7 @@ description: "Spring Boot 文件上传遇到 Nginx 413 Request Entity Too Large 使用 Spring Boot 文件上传的时候,前端使用 nginx 代理后端请求,出现如下 **413 Request Entity Too Large** 这个错误。 -![nginx-413](https://static.xkcoding.com/blog/solve-nginx-413-problem/image-nginx.png) +![nginx-413](https://cdn.xkcoding.com/blog/solve-nginx-413-problem/image-nginx.png) diff --git a/src/data/blog/2019-01-30.spring-boot-request-use-enums-params.md b/src/data/blog/2019-01-30.spring-boot-request-use-enums-params.md index 31900ae6..ad23da2d 100644 --- a/src/data/blog/2019-01-30.spring-boot-request-use-enums-params.md +++ b/src/data/blog/2019-01-30.spring-boot-request-use-enums-params.md @@ -439,17 +439,17 @@ public class WebMvcConfig implements WebMvcConfigurer { ### 4.1. Get 请求 -![Get请求参数1](https://static.xkcoding.com/blog/spring-boot-request-use-enums-params/image-get_string_1.png) +![Get请求参数1](https://cdn.xkcoding.com/blog/spring-boot-request-use-enums-params/image-get_string_1.png) -![Get请求参数2](https://static.xkcoding.com/blog/spring-boot-request-use-enums-params/image-get_string_2.png) +![Get请求参数2](https://cdn.xkcoding.com/blog/spring-boot-request-use-enums-params/image-get_string_2.png) ### 4.2. Post 请求 -![Post请求参数1](https://static.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_integer.png) +![Post请求参数1](https://cdn.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_integer.png) -![Post请求参数2](https://static.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_string_1.png) +![Post请求参数2](https://cdn.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_string_1.png) -![Post请求参数3](https://static.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_string_2.png) +![Post请求参数3](https://cdn.xkcoding.com/blog/spring-boot-request-use-enums-params/image-post_string_2.png) ## 5. 代码地址 diff --git a/src/data/blog/2019-02-04.use-maven-manage-spring-boot-profile.md b/src/data/blog/2019-02-04.use-maven-manage-spring-boot-profile.md index 36ae2a46..d50704a4 100644 --- a/src/data/blog/2019-02-04.use-maven-manage-spring-boot-profile.md +++ b/src/data/blog/2019-02-04.use-maven-manage-spring-boot-profile.md @@ -138,7 +138,7 @@ mvn clean package -Dmaven.test.skip=true -P ${profileActive} 如果我们使用 IDEA为开发工具的话,就会方便很多了。上一张图,不过多解释~ - + ## 3. 参考 diff --git a/src/data/blog/2019-02-13.design-pattern-simple-factory.md b/src/data/blog/2019-02-13.design-pattern-simple-factory.md index 65b1498f..72d1faad 100644 --- a/src/data/blog/2019-02-13.design-pattern-simple-factory.md +++ b/src/data/blog/2019-02-13.design-pattern-simple-factory.md @@ -28,11 +28,11 @@ description: "new LemonCandy() 散落在代码各处?简单工厂模式帮你 ### 3.1. 未使用简单工厂模式的UML图 - + ### 3.2. 使用简单工厂模式之后的UML图 - + ## 4. 代码实现 diff --git a/src/data/blog/2019-02-15.design-pattern-factory-method.md b/src/data/blog/2019-02-15.design-pattern-factory-method.md index f7fec76e..026b2fa7 100644 --- a/src/data/blog/2019-02-15.design-pattern-factory-method.md +++ b/src/data/blog/2019-02-15.design-pattern-factory-method.md @@ -27,7 +27,7 @@ description: "简单工厂的 if-else 越写越长?工厂方法模式来解耦 ## 3. UML图例 - + ## 4. 代码实现 diff --git a/src/data/blog/2019-03-12.scaffold-doc.md b/src/data/blog/2019-03-12.scaffold-doc.md index 87aaf43a..98c7c67e 100644 --- a/src/data/blog/2019-03-12.scaffold-doc.md +++ b/src/data/blog/2019-03-12.scaffold-doc.md @@ -18,7 +18,7 @@ scaffold: 脚手架的意思,是一款封装了一些通用组件的,让Spri ## 1. Scaffold组件图谱 - + ## 2. 项目信息 @@ -146,9 +146,9 @@ public class DemoApplication { 5. idea开发场景下设置参考下图 - + - + ##### ⑤ 自定义配置项 @@ -432,7 +432,7 @@ swagger: 访问 `http://${host}:${port}/${contextPath}/doc.html`,比如:http://localhost:8080/demo/doc.html - + > 注意:如果当前 `scaffold-launcher` 配置的 `spring.profiles.active` 环境是 `dev、test` 则可以访问接口文档,如果是 `prod` 则访问不到。 @@ -925,7 +925,7 @@ public class NotificationTest extends ScaffoldBaseTest {
微信打赏
@@ -935,7 +935,7 @@ public class NotificationTest extends ScaffoldBaseTest {
支付宝打赏
@@ -952,19 +952,19 @@ public class NotificationTest extends ScaffoldBaseTest { **钉钉消息** - + **简单邮件** - + **复杂邮件** - + **短信** - + ##### ⑥ 文档参考 @@ -1017,7 +1017,7 @@ spring: ##### ④ 启动项目,访问 http://localhost:8080/demo/codegen.html - + > 注意:如果当前 `scaffold-launcher` 配置的 `spring.profiles.active` 环境是 `dev、test` 则可以访问接口文档,如果是 `prod` 则访问不到。 diff --git a/src/data/blog/2019-04-24.use-vagrant-build-linux-virtual-machine.md b/src/data/blog/2019-04-24.use-vagrant-build-linux-virtual-machine.md index 8146fc9c..f6ed529c 100644 --- a/src/data/blog/2019-04-24.use-vagrant-build-linux-virtual-machine.md +++ b/src/data/blog/2019-04-24.use-vagrant-build-linux-virtual-machine.md @@ -108,7 +108,7 @@ box 就是我们系统的初始化镜像,我们可以在 http://www.vagrantbox > 建议大家去给这个仓库点个 Star,方便好用的东西永远值得表扬! 附上仓库链接:https://github.com/garethr/vagrantboxes-heroku 。 -vagrantbox-es +vagrantbox-es 理论上这里能搜到的 box,都可以在下面的初始化中直接使用 `init` 命令直接初始化。(但是国内的网络会导致经常失败。) @@ -211,7 +211,7 @@ Installed the plugin 'vagrant-proxyconf (2.0.1)'! 打开 VirtualBox,找到左上角 『工具』右侧的小图标,点击该图标,选择『网络』,可以看到如下界面: -VirtualBox配置网卡信息 +VirtualBox配置网卡信息 可以看到网卡列表里存在网卡信息,如果不存在,可以自己手动创建。这里网卡信息里的必须选择『手动配置网卡』。我们可以从虚拟机中访问宿主机的固定IP就是这里的IPv4地址。 diff --git a/src/data/blog/2019-05-22.spring-boot-login-with-oauth.md b/src/data/blog/2019-05-22.spring-boot-login-with-oauth.md index d8008b4d..3b960996 100644 --- a/src/data/blog/2019-05-22.spring-boot-login-with-oauth.md +++ b/src/data/blog/2019-05-22.spring-boot-login-with-oauth.md @@ -127,7 +127,7 @@ https://github.com/xkcoding/act-justauth-demo 前往阿里云DNS解析,将域名解析到我们的公网服务器上,比如我的就是将 `oauth.xkcoding.com -> 120.92.169.103` -![image-20190615165843639](https://static.xkcoding.com/blog/2019-07-06-095756.png) +![image-20190615165843639](https://cdn.xkcoding.com/blog/2019-07-06-095756.png) ### 1.4. nginx代理 @@ -190,7 +190,7 @@ $ nginx -s reload 3. 应用管理 -> 添加网站应用,等待审核通过即可 - ![image-20190617144655429](https://static.xkcoding.com/blog/2019-07-06-095903.png) + ![image-20190617144655429](https://cdn.xkcoding.com/blog/2019-07-06-095903.png) #### 1.5.2. GitHub平台申请 @@ -198,7 +198,7 @@ $ nginx -s reload 2. 点击 `New OAuth App` 按钮创建应用 - ![image-20190617145839851](https://static.xkcoding.com/blog/2019-07-06-095917.png) + ![image-20190617145839851](https://cdn.xkcoding.com/blog/2019-07-06-095917.png) #### 1.5.3 微信开放平台申请 @@ -213,7 +213,7 @@ $ nginx -s reload 因此这里我就贴出一张授权回调的地址作参考。 -![image-20190617153552218](https://static.xkcoding.com/blog/2019-07-06-095936.png) +![image-20190617153552218](https://cdn.xkcoding.com/blog/2019-07-06-095936.png) #### 1.5.4. 谷歌开放平台申请 @@ -221,9 +221,9 @@ $ nginx -s reload 2. 前往 https://console.developers.google.com/apis/credentials ,在第一步创建的项目下,添加应用 - ![image-20190617151119584](https://static.xkcoding.com/blog/2019-07-06-100015.png) + ![image-20190617151119584](https://cdn.xkcoding.com/blog/2019-07-06-100015.png) - ![image-20190617150903039](https://static.xkcoding.com/blog/2019-07-06-100027.png) + ![image-20190617150903039](https://cdn.xkcoding.com/blog/2019-07-06-100027.png) #### 1.5.5. 微软开放平台申请 @@ -231,15 +231,15 @@ $ nginx -s reload 2. 在注册应用的时候就需要填写回调地址,当然后期也可以重新修改 - ![image-20190617152529449](https://static.xkcoding.com/blog/2019-07-06-100042.png) + ![image-20190617152529449](https://cdn.xkcoding.com/blog/2019-07-06-100042.png) 3. client id 在这里 - ![image-20190617152805581](https://static.xkcoding.com/blog/2019-07-06-100056.png) + ![image-20190617152805581](https://cdn.xkcoding.com/blog/2019-07-06-100056.png) 4. client secret 需要自己在这里生成 - ![image-20190617152711938](https://static.xkcoding.com/blog/2019-07-06-100106.png) + ![image-20190617152711938](https://cdn.xkcoding.com/blog/2019-07-06-100106.png) #### 1.5.6. 小米开放平台申请 @@ -249,11 +249,11 @@ $ nginx -s reload 3. 填写基本信息之后,进入应用信息页面填写 `回调地址` - ![image-20190617151502414](https://static.xkcoding.com/blog/2019-07-06-100126.png) + ![image-20190617151502414](https://cdn.xkcoding.com/blog/2019-07-06-100126.png) 4. 应用审核通过之后,可以在应用信息页面的 `应用详情` 查看到 AppKey 和 AppSecret,吐槽下,小米应用的审核速度特别慢,需要耐心等待。。。。 - ![image-20190617151624603](https://static.xkcoding.com/blog/2019-07-06-100139.png) + ![image-20190617151624603](https://cdn.xkcoding.com/blog/2019-07-06-100139.png) #### 1.5.7. 企业微信平台申请 @@ -509,7 +509,7 @@ public class OauthController { > `Google 登录,有可能因为祖国的强大导致测试失败,自行解决~` :kissing_smiling_eyes: -![image-20190809161032422](https://static.xkcoding.com/blog/2019-08-09-081033.png) +![image-20190809161032422](https://cdn.xkcoding.com/blog/2019-08-09-081033.png) --- diff --git a/src/data/blog/2019-08-06.use-justauth-integration-wechat-enterprise.md b/src/data/blog/2019-08-06.use-justauth-integration-wechat-enterprise.md index 0ceb30f5..820c69fc 100644 --- a/src/data/blog/2019-08-06.use-justauth-integration-wechat-enterprise.md +++ b/src/data/blog/2019-08-06.use-justauth-integration-wechat-enterprise.md @@ -24,17 +24,17 @@ JustAuth 又㕛叒叕添加新平台啦~ 如果没有企业可以点击 `企业注册` -![image-20190806155101145](https://static.xkcoding.com/justauth/2019-08-06-075101.png) +![image-20190806155101145](https://cdn.xkcoding.com/justauth/2019-08-06-075101.png) ## 2. 创建企业微信应用 导航栏 -- 应用管理 -- 自建 -- 创建应用 -![image-20190806155308745](https://static.xkcoding.com/justauth/2019-08-06-075309.png) +![image-20190806155308745](https://cdn.xkcoding.com/justauth/2019-08-06-075309.png) 选择合适的应用logo,为应用取个名,同时设置应用的可见范围,创建应用 -![image-20190806155353393](https://static.xkcoding.com/justauth/2019-08-06-075353.png) +![image-20190806155353393](https://cdn.xkcoding.com/justauth/2019-08-06-075353.png) ## 3. 应用设置及基本关键参数 @@ -42,17 +42,17 @@ JustAuth 又㕛叒叕添加新平台啦~ 创建完应用之后,拖到应用最下方,配置应用信息 -![image-20190806155804425](https://static.xkcoding.com/justauth/2019-08-06-075805.png) +![image-20190806155804425](https://cdn.xkcoding.com/justauth/2019-08-06-075805.png) 使用 JustAuth 基本都是网页应用,所以这一步我们直接选择 `Web网页` -- `设置授权回调域` -![image-20190806160104626](https://static.xkcoding.com/justauth/2019-08-06-080105.png) +![image-20190806160104626](https://cdn.xkcoding.com/justauth/2019-08-06-080105.png) > 注意:这里 `授权回调域` 不需要指定 `前缀,后缀` 等信息,示例如下图 > > 关于授权回调域存在疑问的童鞋请看这里:https://open.work.weixin.qq.com/api/doc#90000/90135/90988 -![image-20190806160302321](https://static.xkcoding.com/justauth/2019-08-06-080302.png) +![image-20190806160302321](https://cdn.xkcoding.com/justauth/2019-08-06-080302.png) ### 3.2. 基本关键参数获取 @@ -62,13 +62,13 @@ JustAuth 又㕛叒叕添加新平台啦~ `agent-id` 和 `client-secret`这两个信息均在 `应用管理` 里可以查看 -![image-20190806161343874](https://static.xkcoding.com/justauth/2019-08-06-081344.png) +![image-20190806161343874](https://cdn.xkcoding.com/justauth/2019-08-06-081344.png) #### 3.2.2. client-id 信息 `client-id` 信息在 `我的企业` 中可以找到,该信息即 `企业ID` -![image-20190806161551756](https://static.xkcoding.com/justauth/2019-08-06-081552.png) +![image-20190806161551756](https://cdn.xkcoding.com/justauth/2019-08-06-081552.png) #### 3.2.3. redirect-uri 信息 @@ -150,9 +150,9 @@ public class TestController { ## 5. 效果演示 -![image-20190806162938367](https://static.xkcoding.com/justauth/2019-08-06-082938.png) +![image-20190806162938367](https://cdn.xkcoding.com/justauth/2019-08-06-082938.png) -![image-20190806163011576](https://static.xkcoding.com/justauth/2019-08-06-083011.png) +![image-20190806163011576](https://cdn.xkcoding.com/justauth/2019-08-06-083011.png) 果然! 不得不说:[`whnb.wang`](https://whnb.wang/) diff --git a/src/data/blog/2019-08-10.design-pattern-abstract-factory.md b/src/data/blog/2019-08-10.design-pattern-abstract-factory.md index 58e95da3..530266b9 100644 --- a/src/data/blog/2019-08-10.design-pattern-abstract-factory.md +++ b/src/data/blog/2019-08-10.design-pattern-abstract-factory.md @@ -279,7 +279,7 @@ public class AppleTechCompany extends TechFactory { ## 3. UML图例 -![抽象工厂模式UML图](https://static.xkcoding.com/blog/2019-08-09-abstractfactory-uml.png) +![抽象工厂模式UML图](https://cdn.xkcoding.com/blog/2019-08-09-abstractfactory-uml.png) ## 4. 应用 diff --git a/src/data/blog/2019-08-14.design-pattern-prototype.md b/src/data/blog/2019-08-14.design-pattern-prototype.md index 5d1349e5..38bb69eb 100644 --- a/src/data/blog/2019-08-14.design-pattern-prototype.md +++ b/src/data/blog/2019-08-14.design-pattern-prototype.md @@ -128,7 +128,7 @@ money2 -> 引用对象地址:com.xkcoding.design.pattern.creational.prototype. #### 2.1.2. UML图例 -原型模式-浅克隆UML图例 +原型模式-浅克隆UML图例 ### 2.2. 深克隆 @@ -258,7 +258,7 @@ money2 -> 引用对象地址:com.xkcoding.design.pattern.creational.prototype. #### 2.2.2. UML图例 -原型模式-深克隆UML图例 +原型模式-深克隆UML图例 ## 3. 应用 diff --git a/src/data/blog/2019-08-20.design-pattern-proxy.md b/src/data/blog/2019-08-20.design-pattern-proxy.md index 34abad64..bf9d80ab 100644 --- a/src/data/blog/2019-08-20.design-pattern-proxy.md +++ b/src/data/blog/2019-08-20.design-pattern-proxy.md @@ -128,7 +128,7 @@ public class PatternTest { #### 2.1.2. UML 图例 -代理模式-静态代理 +代理模式-静态代理 ### 2.2. 动态代理 @@ -284,7 +284,7 @@ public class PatternTest { ##### 2.2.1.2. UML 图例 -代理模式-动态代理-JDK动态代理 +代理模式-动态代理-JDK动态代理 #### 2.2.2. CGLIB 动态代理 @@ -396,7 +396,7 @@ public class PatternTest { ##### 2.2.2.2. UML 图例 -代理模式-动态代理-CGLIB动态代理 +代理模式-动态代理-CGLIB动态代理 ## 3. 应用 @@ -437,7 +437,7 @@ public class PatternTest { 首先我们将断点打在 [这个位置](https://github.com/xkcoding/design-pattern/blob/107158c96ed2a23d8aabf21f84cf0386169942c9/src/main/java/com/xkcoding/design/pattern/structural/proxy/dynamicproxy/jdk/run/PatternTest.java#L20) 如下图所示,可以发现,此时这个 `musicTicket` 对象的引用有点奇怪,居然是 `$Proxy0@554`,证明该对象是通过代理生成的新的代理类创建的,而不是由原生的 `MusicTicket` 类创建的。 - + 为什么嘞?我们来瞄一波源码。 @@ -980,7 +980,7 @@ private byte[] generateClassFile() { 有了 JDK 动态代理的经验,我们也先来看看 CGLIB 代理的对象有什么区别,同样在 [这个位置](https://github.com/xkcoding/design-pattern/blob/107158c96ed2a23d8aabf21f84cf0386169942c9/src/main/java/com/xkcoding/design/pattern/structural/proxy/dynamicproxy/cglib/run/PatternTest.java#L19) 打个断点,如下图所示。果不其然,此时的 `trainTicket` 的引用地址依然奇怪,调试发现,是这个东西 `TrainTicket$$EnhancerByCGLIB$$9d0422e9@681`。 -![image-20190824114026176](https://static.xkcoding.com/blog/2019-08-24-034026.png) +![image-20190824114026176](https://cdn.xkcoding.com/blog/2019-08-24-034026.png) 老规矩,我们来看下源码吧~ diff --git a/src/data/blog/2019-08-27.design-pattern-delegate.md b/src/data/blog/2019-08-27.design-pattern-delegate.md index 58c1cf77..622d0810 100644 --- a/src/data/blog/2019-08-27.design-pattern-delegate.md +++ b/src/data/blog/2019-08-27.design-pattern-delegate.md @@ -181,7 +181,7 @@ public class PatternTest { ## 3. UML 图例 - + ## 4. 应用 diff --git a/src/data/blog/2019-08-29.early-experience-about-github-actions.md b/src/data/blog/2019-08-29.early-experience-about-github-actions.md index c46c5f45..bb6b9427 100644 --- a/src/data/blog/2019-08-29.early-experience-about-github-actions.md +++ b/src/data/blog/2019-08-29.early-experience-about-github-actions.md @@ -41,29 +41,29 @@ description: "还在用 Jenkins、Travis-CI?GitHub 自家的 CI/CD 来了!Gi 点击链接 https://github.com/features/actions/signup 去申请 -![GitHub Actions Signup](https://static.xkcoding.com/blog/2019-08-29-GitHub-Actions-1.png) +![GitHub Actions Signup](https://cdn.xkcoding.com/blog/2019-08-29-GitHub-Actions-1.png) ### 2.2. 准备好一个仓库地址 当你申请通过之后,打开你的任意一个 GitHub 仓库,上方会出现 `Actions` 的选项卡,点击即可进入。 -![GitHub Repo](https://static.xkcoding.com/blog/2019-08-29-GitHub-Actions-2.png) +![GitHub Repo](https://cdn.xkcoding.com/blog/2019-08-29-GitHub-Actions-2.png) ### 2.3. 选择一个默认的模板 因为这里我的是 Spring Boot 项目,基于 Maven 来构建,所以我选择 Maven 的模板。选择之后,进入下面的页面。 -![GitHub Actions Maven Template](https://static.xkcoding.com/blog/2019-08-29-GitHub-Actions-3.png) +![GitHub Actions Maven Template](https://cdn.xkcoding.com/blog/2019-08-29-GitHub-Actions-3.png) ### 2.4. 触发 当我们的代码 `push` 的时候就会自动触发,当然也有别的触发策略,这个后面再讲。 -![GitHub Action Status](https://static.xkcoding.com/blog/2019-08-29-GitHub-Actions-4.png) +![GitHub Action Status](https://cdn.xkcoding.com/blog/2019-08-29-GitHub-Actions-4.png) 点击可以查看具体的构建情况,黑框里的每一项都可以点击查看详情。 -![GitHub Actions Details](https://static.xkcoding.com/blog/2019-08-29-GitHub-Actions-5.png) +![GitHub Actions Details](https://cdn.xkcoding.com/blog/2019-08-29-GitHub-Actions-5.png) ## 3. 配置语法 diff --git a/src/data/blog/2019-09-03.design-pattern-strategy.md b/src/data/blog/2019-09-03.design-pattern-strategy.md index 17018e5a..aa2c679e 100644 --- a/src/data/blog/2019-09-03.design-pattern-strategy.md +++ b/src/data/blog/2019-09-03.design-pattern-strategy.md @@ -325,7 +325,7 @@ public class PatternTest { ## 3. UML 图例 -![设计模式-行为型设计模式-策略模式](https://static.xkcoding.com/blog/2019-08-28-strategy-uml.png) +![设计模式-行为型设计模式-策略模式](https://cdn.xkcoding.com/blog/2019-08-28-strategy-uml.png) ## 4. 应用 diff --git a/src/data/blog/2019-09-13.design-pattern-adapter.md b/src/data/blog/2019-09-13.design-pattern-adapter.md index 0163762c..3fb61144 100644 --- a/src/data/blog/2019-09-13.design-pattern-adapter.md +++ b/src/data/blog/2019-09-13.design-pattern-adapter.md @@ -134,7 +134,7 @@ public class PatternTest { ## 3. UML 图例 -design-pattern-adapter-uml +design-pattern-adapter-uml ## 4. 应用 diff --git a/src/data/blog/2019-10-16.design-pattern-template.md b/src/data/blog/2019-10-16.design-pattern-template.md index dc1ef231..04443cb2 100644 --- a/src/data/blog/2019-10-16.design-pattern-template.md +++ b/src/data/blog/2019-10-16.design-pattern-template.md @@ -207,7 +207,7 @@ public class PatternTest { ## 3. UML 图例 -design-pattern-template-uml +design-pattern-template-uml ## 4. 应用 diff --git a/src/data/blog/2019-10-25.design-pattern-observer.md b/src/data/blog/2019-10-25.design-pattern-observer.md index 78dfbfcd..95eb74b3 100644 --- a/src/data/blog/2019-10-25.design-pattern-observer.md +++ b/src/data/blog/2019-10-25.design-pattern-observer.md @@ -147,7 +147,7 @@ xkcoding,你收到一条来自 user-1 的私信! ## 3. UML 图例 -design-pattern-observer-uml +design-pattern-observer-uml ## 4. 应用 diff --git a/src/data/blog/2019-11-11.design-pattern-decorator.md b/src/data/blog/2019-11-11.design-pattern-decorator.md index 4936c854..ef9fec30 100644 --- a/src/data/blog/2019-11-11.design-pattern-decorator.md +++ b/src/data/blog/2019-11-11.design-pattern-decorator.md @@ -335,7 +335,7 @@ public class PatternTest { ## 3. UML 图例 -design-pattern-observer-uml +design-pattern-observer-uml ## 4. 应用 diff --git a/src/data/blog/2020-01-01.bye2019-hello2020.md b/src/data/blog/2020-01-01.bye2019-hello2020.md index b6e6e025..4966785b 100644 --- a/src/data/blog/2020-01-01.bye2019-hello2020.md +++ b/src/data/blog/2020-01-01.bye2019-hello2020.md @@ -67,7 +67,7 @@ description: "十年匆匆而过,从中考吊车尾到大学专业前三,从 贴一张打卡图: - + 这里要说明一下 @@ -84,7 +84,7 @@ description: "十年匆匆而过,从中考吊车尾到大学专业前三,从 这个项目说实话,今年是它最辉煌的岁月,获得 GitHub 热门 Java 榜单 8 月份第一名、9 月份第八名、12 月份第三名的成绩。唯一遗憾的是,今年仍然没有给它完结掉。但好在,今年不是我一个人在战斗了,有不少热心的小伙伴提了很多有用的意见、也共享了一些 demo 的代码,真心感谢你们!下面晒一波战绩! - + 2. [JustAuth](https://github.com/justauth/JustAuth) @@ -139,7 +139,7 @@ description: "十年匆匆而过,从中考吊车尾到大学专业前三,从 6.22喜提阿特兹一枚~ - + ## 看今朝 @@ -176,4 +176,4 @@ description: "十年匆匆而过,从中考吊车尾到大学专业前三,从 附上帅照一张: - + diff --git a/src/data/blog/2020-04-20.how-to-design-your-own-rpc-framework-1.md b/src/data/blog/2020-04-20.how-to-design-your-own-rpc-framework-1.md index a62749dc..c2c6743e 100644 --- a/src/data/blog/2020-04-20.how-to-design-your-own-rpc-framework-1.md +++ b/src/data/blog/2020-04-20.how-to-design-your-own-rpc-framework-1.md @@ -21,11 +21,11 @@ RPC,全称:Remote Procedure Call,翻译过来就是:远程过程调用 首先我们看看本地调用的流程,本地调用,我们相当于是直接通过 new 的方式去创建一个对象,然后调用对象的方法去获取数据。 -![本地调用流程](https://static.xkcoding.com/blog/2020-05-06-053453.png) +![本地调用流程](https://cdn.xkcoding.com/blog/2020-05-06-053453.png) 如果是分布式系统,服务调用方和服务提供方,分布在不同的服务上,此时调用关系就变成了这样子,我们将调用方和提供方分别用 client 和 server 代替。 -![分布式调用](https://static.xkcoding.com/blog/2020-05-06-054506.png) +![分布式调用](https://cdn.xkcoding.com/blog/2020-05-06-054506.png) 既然是远程调用,必然涉及到 IO 通信、传输内容序列化 / 反序列化 等技术。 diff --git a/src/data/blog/2020-05-12.how-to-design-your-own-rpc-framework-2.md b/src/data/blog/2020-05-12.how-to-design-your-own-rpc-framework-2.md index 10c37a07..f56671d3 100644 --- a/src/data/blog/2020-05-12.how-to-design-your-own-rpc-framework-2.md +++ b/src/data/blog/2020-05-12.how-to-design-your-own-rpc-framework-2.md @@ -33,7 +33,7 @@ AIO:Asynchronous NonBlocking IO,异步非阻塞 IO。AIO 是基于 Proactor [Netty](https://netty.io/) 是一款基于 NIO 开发的高性能网络通信框架,同时支持自定义通信协议。下图是 Netty 官网的一张 Netty 组件图。 -![components](https://static.xkcoding.com/blog/2020-05-12-042612.png) +![components](https://cdn.xkcoding.com/blog/2020-05-12-042612.png) 本文将使用 Netty 去重构之前的 RPC 案例。 diff --git a/src/data/blog/2020-06-01.how-does-my-blog-integrate-cicd.md b/src/data/blog/2020-06-01.how-does-my-blog-integrate-cicd.md index 6a578ac8..fe9acc32 100644 --- a/src/data/blog/2020-06-01.how-does-my-blog-integrate-cicd.md +++ b/src/data/blog/2020-06-01.how-does-my-blog-integrate-cicd.md @@ -271,9 +271,9 @@ notifications: ##### ③ 相关截图 -![Jenkins-流水线](https://static.xkcoding.com/blog/2020-06-02-103628.png) +![Jenkins-流水线](https://cdn.xkcoding.com/blog/2020-06-02-103628.png) -![travis-ci流水线](https://static.xkcoding.com/blog/2020-06-02-104522.png) +![travis-ci流水线](https://cdn.xkcoding.com/blog/2020-06-02-104522.png) ### 3.3. 折腾 v2.0 @@ -459,11 +459,11 @@ networks: ##### ④ 相关截图 -![GitLab Pipeline 总览](https://static.xkcoding.com/blog/2020-06-02-114551.png) +![GitLab Pipeline 总览](https://cdn.xkcoding.com/blog/2020-06-02-114551.png) -![Pipeline 详情](https://static.xkcoding.com/blog/2020-06-02-114702.png) +![Pipeline 详情](https://cdn.xkcoding.com/blog/2020-06-02-114702.png) -![各节点容器运行情况](https://static.xkcoding.com/blog/2020-06-02-114500.png) +![各节点容器运行情况](https://cdn.xkcoding.com/blog/2020-06-02-114500.png) ## 4. 后记 diff --git a/src/data/blog/2020-11-02.quick-run-you-own-yiyan.md b/src/data/blog/2020-11-02.quick-run-you-own-yiyan.md index ee40fb29..b756376c 100644 --- a/src/data/blog/2020-11-02.quick-run-you-own-yiyan.md +++ b/src/data/blog/2020-11-02.quick-run-you-own-yiyan.md @@ -247,7 +247,7 @@ services: 再次执行 `docker-compose up` ,待运行成功之后,打开浏览器,输入 `localhost` 即可查看到页面,同时因为去掉了8000 端口的暴露,之前的`http://localhost:8000`的接口地址就变更为了 `http://localhost/api`了,是不是很方便呢~ -![image-20201103004506705](https://static.xkcoding.com/blog/2020-11-02-164507.png) +![image-20201103004506705](https://cdn.xkcoding.com/blog/2020-11-02-164507.png) ## 代码示例 diff --git a/src/data/blog/2021-01-18.Q4-2020-and-year-summary.md b/src/data/blog/2021-01-18.Q4-2020-and-year-summary.md index 63ce0e49..9695b02b 100644 --- a/src/data/blog/2021-01-18.Q4-2020-and-year-summary.md +++ b/src/data/blog/2021-01-18.Q4-2020-and-year-summary.md @@ -45,7 +45,7 @@ Q4 是 2020 年的尾巴,产品经理要做年度汇报,收尾工作要做 > 1. 我的阳历生日是 `11.22` > 2. 我希望 `1 + 1 = 2 + 2` 这个等式成立,就是希望我俩后面可以生俩宝宝的意思 -结婚证 +结婚证 其他的一些纪念照就不晒啦,哈哈哈~ diff --git a/src/data/blog/2021-03-10.fix-error-about-python-package-install-on-big-sur.md b/src/data/blog/2021-03-10.fix-error-about-python-package-install-on-big-sur.md index 27bbca96..3d19bf8d 100644 --- a/src/data/blog/2021-03-10.fix-error-about-python-package-install-on-big-sur.md +++ b/src/data/blog/2021-03-10.fix-error-about-python-package-install-on-big-sur.md @@ -176,7 +176,7 @@ auth success 因为这会直接影响到后面量化的指标!! -![不玩了,把钱还我](https://static.xkcoding.com/emoji/20210310.png) +![不玩了,把钱还我](https://cdn.xkcoding.com/emoji/20210310.png) ## 参考 diff --git a/src/data/blog/2021-03-15.run-local-k8s-cluster-quickly.md b/src/data/blog/2021-03-15.run-local-k8s-cluster-quickly.md index f5adfaca..e7d07e05 100644 --- a/src/data/blog/2021-03-15.run-local-k8s-cluster-quickly.md +++ b/src/data/blog/2021-03-15.run-local-k8s-cluster-quickly.md @@ -11,7 +11,7 @@ tags: description: "学 K8S 不想只看文档?想在本地搭集群练手却被网络和资源劝退?k3d 了解一下——基于 Docker 运行 k3s,一行命令创建「1主2从」集群,启动快、资源省、还能模拟多节点调度。本文对比 minikube/microk8s/kind/k3d 四种方案,手把手教你用 k3d 搭建本地 K8S 环境,从创建集群到部署 nginx 验证 Ingress,10 分钟搞定。附 kubecm 多集群切换技巧。" --- -![文章配图](https://static.xkcoding.com/blog/run-local-k8s-cluster-quickly/%E5%A6%82%E4%BD%95%E5%9C%A8%E6%9C%AC%E5%9C%B0%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8%E4%B8%80%E4%B8%AA%20K8S%20%E9%9B%86%E7%BE%A4.jpg) +![文章配图](https://cdn.xkcoding.com/blog/run-local-k8s-cluster-quickly/%E5%A6%82%E4%BD%95%E5%9C%A8%E6%9C%AC%E5%9C%B0%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8%E4%B8%80%E4%B8%AA%20K8S%20%E9%9B%86%E7%BE%A4.jpg) ## 背景 @@ -141,7 +141,7 @@ cef7277e43b9 rancher/k3s:latest "/bin/k3s agent" 4 hours ago 现在我们集群和主机的网络通信是这样子的: -![k3d-network](https://static.xkcoding.com/%20blog/run-local-k8s-cluster-quickly/k3d-network.png) +![k3d-network](https://cdn.xkcoding.com/blog/run-local-k8s-cluster-quickly/k3d-network.png) ## 4. 测试 diff --git a/src/data/blog/2021-04-14.run-local-zookeeper-cluster-with-docker.md b/src/data/blog/2021-04-14.run-local-zookeeper-cluster-with-docker.md index 54a38cba..4e781ff8 100644 --- a/src/data/blog/2021-04-14.run-local-zookeeper-cluster-with-docker.md +++ b/src/data/blog/2021-04-14.run-local-zookeeper-cluster-with-docker.md @@ -11,7 +11,7 @@ tags: description: "学大数据绑不开 ZooKeeper,但安装配置让人头疼?Docker Compose 一键搞定!本文提供单机版和集群版两套方案:单机一行命令 `docker run`;集群三节点配置 ZOO_MY_ID + ZOO_SERVERS,利用 Compose 网络互通特性组成集群。还解决了 Curator 连接集群报 NPE 的坑——需要配置 hosts 通过 hostname 连接。" --- -![文章配图](https://static.xkcoding.com/blog/run-local-zookeeper-cluster-with-docker/%E4%BD%BF%E7%94%A8%20docker%20%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8%E6%9C%AC%E5%9C%B0%20zookeeper%20%E9%9B%86%E7%BE%A4.jpg) +![文章配图](https://cdn.xkcoding.com/blog/run-local-zookeeper-cluster-with-docker/%E4%BD%BF%E7%94%A8%20docker%20%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8%E6%9C%AC%E5%9C%B0%20zookeeper%20%E9%9B%86%E7%BE%A4.jpg) ## 前言 diff --git a/src/data/blog/2021-04-22.when-lombok-met-mapstruct.md b/src/data/blog/2021-04-22.when-lombok-met-mapstruct.md index 891b4ce4..9bb215ed 100644 --- a/src/data/blog/2021-04-22.when-lombok-met-mapstruct.md +++ b/src/data/blog/2021-04-22.when-lombok-met-mapstruct.md @@ -249,7 +249,7 @@ public class CarDTO { 再来执行一遍测试类,编译时就会出现类似这种「`java: 找不到符号 符号: 方法 getXXX()`」错误提示,具体如下图: -![异常](https://static.xkcoding.com/blog/when-lombok-met-mapstruct/%E5%BC%82%E5%B8%B8%E4%BF%A1%E6%81%AF.png) +![异常](https://cdn.xkcoding.com/blog/when-lombok-met-mapstruct/%E5%BC%82%E5%B8%B8%E4%BF%A1%E6%81%AF.png) 当出现这种错误,分明就是 Lombok 没有生效嘛,那么该怎么解决这个问题呢? diff --git a/src/data/blog/2021-05-01.happy-dev-run-local-dns-server.md b/src/data/blog/2021-05-01.happy-dev-run-local-dns-server.md index aeb49b2d..6c3ac20b 100644 --- a/src/data/blog/2021-05-01.happy-dev-run-local-dns-server.md +++ b/src/data/blog/2021-05-01.happy-dev-run-local-dns-server.md @@ -24,7 +24,7 @@ description: "本地开发服务太多,ip:port 记不住?用域名替代! 这里推荐一款工具,[SwitchHosts](https://swh.app/zh/)(_记忆中,之前的版本叫做 `SwitchHosts!`_),页面简洁,操作一目了然。 配置 Hosts 文件,只需要将域名的地址跟 IP 做映射即可。 -![SwitchHosts](https://static.aliyun.xkcoding.com/2021/09/03/16305752555388.jpg?x-oss-process=style/tag_compress) +![SwitchHosts](https://cdn.xkcoding.com/2021/09/03/16305752555388.jpg) 测试 @@ -83,7 +83,7 @@ services: 运行 `docker-compose up` 启动服务,访问管理界面 http://localhost:8080 -![dnsmasq管理界面](https://static.aliyun.xkcoding.com/2021/09/03/16305767071171.jpg?x-oss-process=style/tag_compress) +![dnsmasq管理界面](https://cdn.xkcoding.com/2021/09/03/16305767071171.jpg) 测试(记得把方案一的 Hosts 记录重置一下😉) @@ -93,7 +93,7 @@ ping: cannot resolve a.dev.io: Unknown host ``` 哦豁,失败了,需要修改本机 DNS 配置,如图所示: -![DNS配置](https://static.aliyun.xkcoding.com/2021/09/03/16305773370388.jpg?x-oss-process=style/tag_compress) +![DNS配置](https://cdn.xkcoding.com/2021/09/03/16305773370388.jpg) 再次测试 diff --git a/src/data/blog/2022-01-04.fix-error-about-jd-gui-on-big-sur.md b/src/data/blog/2022-01-04.fix-error-about-jd-gui-on-big-sur.md index ed2b4af2..3dc6de33 100644 --- a/src/data/blog/2022-01-04.fix-error-about-jd-gui-on-big-sur.md +++ b/src/data/blog/2022-01-04.fix-error-about-jd-gui-on-big-sur.md @@ -40,9 +40,9 @@ Make sure you install the required Java version. 1. 先将下载下来的 `jd-gui-osx-1.6.6.tar` 解压,然后将 `JD-GUI.app` 文件拷贝到 `Applications` 目录下 2. 右击 `JD-GUI.app` 点击`显示包内容` - ![显示包内容](https://static.aliyun.xkcoding.com/2022/01/04/cleanshot-20220104-at-1719142x.png?x-oss-process=style/tag_compress) + ![显示包内容](https://cdn.xkcoding.com/2022/01/04/cleanshot-20220104-at-1719142x.png) 3. 将 `Contents/MacOS/universalJavaApplicationStub.sh` 文件的内容替换为 https://github.com/tofi86/universalJavaApplicationStub/blob/v3.2.0/src/universalJavaApplicationStub 中的内容,保存。 - ![universalJavaApplicationStub.sh](https://static.aliyun.xkcoding.com/2022/01/04/cleanshot-20220104-at-1722142x.png?x-oss-process=style/tag_compress) + ![universalJavaApplicationStub.sh](https://cdn.xkcoding.com/2022/01/04/cleanshot-20220104-at-1722142x.png) 注意:确保本地已存在 Java 环境 diff --git a/src/data/blog/_archive/2016-03-25.android-open-project.md b/src/data/blog/_archive/2016-03-25.android-open-project.md index e851e23f..e7c279a2 100644 --- a/src/data/blog/_archive/2016-03-25.android-open-project.md +++ b/src/data/blog/_archive/2016-03-25.android-open-project.md @@ -28,7 +28,7 @@ description: "目前包括: Android 开源项目第一篇——个性化控件 ### 提示 以下链接网址若存在不能访问,请自行尝试翻墙,推荐使用GreenVPN -[![好用的GreenVPN](/resources/greenvpn.gif)](http://gjsq.me/15038648) +[![好用的GreenVPN](https://cdn.xkcoding.com/blog/resources/greenvpn.gif)](http://gjsq.me/15038648) ## 第一部分 个性化控件(View) diff --git a/src/data/blog/_archive/2016-03-26.first-use-volley-demo.md b/src/data/blog/_archive/2016-03-26.first-use-volley-demo.md index a75f9012..3dc477b8 100644 --- a/src/data/blog/_archive/2016-03-26.first-use-volley-demo.md +++ b/src/data/blog/_archive/2016-03-26.first-use-volley-demo.md @@ -208,4 +208,4 @@ public class MainActivity extends Activity { ``` 4.Ctrl+F11 成功在LogCat中打印出号码信息 -![LogCat信息](/resources/volley-demo-20160326012642.png) +![LogCat信息](https://cdn.xkcoding.com/blog/resources/volley-demo-20160326012642.png) diff --git a/src/data/blog/_archive/2016-03-27.eclipse-tips-variable.md b/src/data/blog/_archive/2016-03-27.eclipse-tips-variable.md index e7c67068..4ac3f808 100644 --- a/src/data/blog/_archive/2016-03-27.eclipse-tips-variable.md +++ b/src/data/blog/_archive/2016-03-27.eclipse-tips-variable.md @@ -15,4 +15,4 @@ description: "前言在开发过程中,开发规范很重要,这样有利于后 比如我们在一个方法中写一个变量,后来发现在其他地方也要用到此变量,这时我们就需要把这变量做成成员变量,我们只要按下ctrl+1,就会提示是否需要变成成员变量,如果要它自动在变量前加m,其实这也是可以配置的,如图: -![eclipse中设置变量名前加m](/resources/eclipse-tips-20151107221921217.png) +![eclipse中设置变量名前加m](https://cdn.xkcoding.com/blog/resources/eclipse-tips-20151107221921217.png) diff --git a/src/data/blog/_archive/2016-04-03.android-svn-attention.md b/src/data/blog/_archive/2016-04-03.android-svn-attention.md index 62fc15f2..43e56ba3 100644 --- a/src/data/blog/_archive/2016-04-03.android-svn-attention.md +++ b/src/data/blog/_archive/2016-04-03.android-svn-attention.md @@ -30,4 +30,4 @@ description: "前言SVN是Subversion的简称,是一个开放源代码的版 屏蔽上传的方式:window-->preferences-->team-->Ignored Resources-->Add Pattern最后添加完成点击Apply 具体需要添加的部分见下图 -![eclipse中配置Android不需要版本控制的部分](/resources/android-svn-attention-20160403130946.png) +![eclipse中配置Android不需要版本控制的部分](https://cdn.xkcoding.com/blog/resources/android-svn-attention-20160403130946.png) diff --git a/src/data/blog/_archive/2016-04-05.coding-standards.md b/src/data/blog/_archive/2016-04-05.coding-standards.md index a5eeb1f7..e72c112b 100644 --- a/src/data/blog/_archive/2016-04-05.coding-standards.md +++ b/src/data/blog/_archive/2016-04-05.coding-standards.md @@ -27,16 +27,16 @@ Eclipse中设置代码样式在 `Window -->Preferences` 中设置 ### 2.1. 设置工作空间编码为UTF-8 -![设置工作空间编码为UTF-8](/resources/coding-standards-1.png) +![设置工作空间编码为UTF-8](https://cdn.xkcoding.com/blog/resources/coding-standards-1.png) ### 2.2. 设置文件编码为UTF-8 -![设置JAVA文件编码为UTF-8](/resources/coding-standards-2.1.png) -![设置XML文件编码为UTF-8](/resources/coding-standards-2.2.png) +![设置JAVA文件编码为UTF-8](https://cdn.xkcoding.com/blog/resources/coding-standards-2.1.png) +![设置XML文件编码为UTF-8](https://cdn.xkcoding.com/blog/resources/coding-standards-2.2.png) ### 2.3. 方法变量名转化成成员变量名称时,变量名称前加m以示区分 -![方法变量名转化成成员变量名称时,变量名称前加m以示区分](/resources/coding-standards-3.png) +![方法变量名转化成成员变量名称时,变量名称前加m以示区分](https://cdn.xkcoding.com/blog/resources/coding-standards-3.png) ### 2.4. 配置文档注释 diff --git a/src/data/blog/_archive/2017-07-18.win10-download.md b/src/data/blog/_archive/2017-07-18.win10-download.md index 21c33b47..ea4685cf 100644 --- a/src/data/blog/_archive/2017-07-18.win10-download.md +++ b/src/data/blog/_archive/2017-07-18.win10-download.md @@ -18,13 +18,13 @@ windows 是目前我们用的最多一个系统,win10 现在也优化的越来 1. [微软各类软件下载地址](http://msdn.itellyou.cn/)(国人自己搞得,非微软官方) 2. 选择 win10 更新版本 - ![选择 win10 更新版本](https://static.xkcoding.com/2017-07-18-15003417580098.jpg) + ![选择 win10 更新版本](https://cdn.xkcoding.com/2017-07-18-15003417580098.jpg) 3. 选择 win10 的具体版本 - ![选择 win10 的具体版本](https://static.xkcoding.com/2017-07-18-15003421081219.jpg) + ![选择 win10 的具体版本](https://cdn.xkcoding.com/2017-07-18-15003421081219.jpg) 4. 复制版本下载地址,使用迅雷下载 - ![复制版本下载地址,使用迅雷下载](https://static.xkcoding.com/2017-07-18-15003421847355.jpg) + ![复制版本下载地址,使用迅雷下载](https://cdn.xkcoding.com/2017-07-18-15003421847355.jpg) 我已经把64位多版本的链接单独抠出来了,需要的直接拿去用~ ``` @@ -36,12 +36,12 @@ windows 是目前我们用的最多一个系统,win10 现在也优化的越来 这个激活工具很强大,可以激活 `windows 系统` 和 `office 软件`,并且提供 `office 2016`和 `office 2013`两个版本的自动安装功能。 6. 激活工具的使用,下载激活工具,解压压缩包,得到一个名为 `KMSTools.exe` 的文件,然后右键,`以管理员身份运行`。(运行之前,注意`关闭所有杀毒软件`!) - ![运行 KMSTools.exe ](https://static.xkcoding.com/2017-07-18-15003439698423.jpg) + ![运行 KMSTools.exe ](https://cdn.xkcoding.com/2017-07-18-15003439698423.jpg) 7. 打开之后,选择第二个 `KMSAuto Lite` - ![选择 KMSAuto Lite ](https://static.xkcoding.com/2017-07-18-15003441158580.jpg) + ![选择 KMSAuto Lite ](https://cdn.xkcoding.com/2017-07-18-15003441158580.jpg) 8. 弹出另一个界面,点击 `Activate Windows` 选择,然后当下面的蓝框框输出 `ACTIVATION SUCCESSFUL` 的时候,就代表激活成功了。如果卡住了,可以多试几次。 - ![点击激活按钮,等待激活成功](https://static.xkcoding.com/2017-07-18-15003443441517.jpg) + ![点击激活按钮,等待激活成功](https://cdn.xkcoding.com/2017-07-18-15003443441517.jpg) 9. have fun! diff --git a/src/data/blog/_archive/2020-09-13.what-i-like-to-do-0.md b/src/data/blog/_archive/2020-09-13.what-i-like-to-do-0.md index 94105322..9e6ad613 100644 --- a/src/data/blog/_archive/2020-09-13.what-i-like-to-do-0.md +++ b/src/data/blog/_archive/2020-09-13.what-i-like-to-do-0.md @@ -16,6 +16,6 @@ description: "周末的不务正业~ 花了一个半小时折的燕子" -燕子 +燕子 -燕子 +燕子