使用 GitLab-CI 和 GitLab Runner 打包 iOS 应用

折腾由来

来源于一篇国外的博客 iOS Continuous Integration with GitLab CI, Fastlane & OTA Installation,看到其使用 GitLab-CI 和 GitLab Runner 来打包 iOS 应用做持续集成,看着还不错,自己之前也做了一些持续集成的例子;

1. 安装 runner

  • 手动安装(推荐)
    1. 下载 binary sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
    2. 设置权限 sudo chmod +x /usr/local/bin/gitlab-runner
  • brew 安装(发现会有权限问题)
    1. 安装 brew install gitlab-runner
    2. 启动 brew services start gitlab-runner

2. 注册 Runners

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 注册 runner
sudo gitlab-runner register
2. 填写 gitlab-ci 地址,这边以官方的为主
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
https://gitlab.com
3. 输入获取的 token ,下面会介绍如何获取
Please enter the gitlab-ci token for this runner
xxx
4. 输入描述,后面可以修改
Please enter the gitlab-ci description for this runner
mac description
5. 输入机器的 tag,后面可以修改
Please enter the gitlab-ci tags for this runner (comma separated):
mymac
6. 输入执行类型,这边填入 shell
Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker
shell

获取 token

3. 配置 .gitlab-ci.yml 文件

在 git 的根目录创建 .gitlab-ci.yml 文件,内容示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

before_script:
- echo "----------global---before_script--------------"

stages:
- build
- publish
# 构建 pipeline

build_job:
stage: build
tags:
- testmac
before_script:
- echo "------build-------before_script--------------"
script:
- sh ios.sh
# 打包 App 脚本
artifacts:
expire_in: 90 days
paths:
- build
# 构建产物打包保存服务器
only:
- master

pages:
stage: publish
script:
- ls build
index.html
artifacts:
expire_in: 90 days
paths:
- build
# 把 build 文件设置成 pages 静态托管
only:
- master

一些 gitlab-ci 配置文档, GitLab CI/CD Pipeline Configuration Reference

4. 自动构建

当代码推上去后, gitlab-ci 会触发构建,向 GitLab Runner(自己设置的机器) 发布任务,并执行构建

  • push 取消自动构建
1
2
git push --push-option=ci.skip    # using git 2.10+
git push -o ci.skip # using git 2.18+

构建面板

5. 定时自动构建

根据自己需要自己添加定时构建

使用 Storybook 开发管理组件(ReactNative 版本)

0. 介绍

Storybook 是 UI 组件的开发环境。它允许您浏览组件库,查看每个组件的不同状态,以及交互式开发和测试组件。

根据官网的介绍可以支持 React,React Native,Vue,Angular,Ember,HTML,Svelte,Mithril,Riot.

做了不少 ReactNative 的项目,每个项目的组件不少,开发到最后都不知道有多少个,如何使用,所以才会想着用啥管理起来,我这次讲的就是关于 React Native 的一些简单使用方法;

1. 安装(ReactNative 版本)

  • Automatic setup 自动安装,通过官方的 @storybook/cli 命令行创建

    npx -p @storybook/cli sb init --type react_native

    • 是否安装 web server 选取



安装完成,一些依赖,以及文件目录

* 启动项目

    > ![](https://ws1.sinaimg.cn/large/8bbf0afbly1g5byy5yakej20q81g0wq9.jpg)
发现会有依赖缺失,缺失的依赖安装(估计是当前版本缺失吧)
`yarn add -D emotion-theming @emotion/core`
![](https://ws1.sinaimg.cn/large/8bbf0afbly1g5byy5kfm6g208e0gkgu5.gif)运行成功

* `yarn storybook` 启动 web server 管理
    > ![](https://ws1.sinaimg.cn/large/8bbf0afbly1g5byy5k5paj22081d018u.jpg)
  • Manual setup 手动安装,自己添加依赖,以及配置文件位置(稍微麻烦点)

    yarn add -D @storybook/react-native @storybook/addons @storybook/addon-links @storybook/addon-actions emotion-theming @emotion/core babel-loader @storybook/react-native-server

    文件目录的就可以参照自动创建的,或者跟着官方教程

做,这边就不在展开,基本后面展示和上面一样

2. 插件 addon

刚才跑起来的项目点击到 ADDONS 里面显示没有配置加载,这里就说下插件已经配置
首先是一张官方 addon 插件支持表,可以看出 React 支持的最多, rn 的支持偏少而且支持配置比较复杂,接下来继续看

2.1 @storybook/addon-actions | @storybook/addon-ondevice-actions

Storybook Addon Actions可用于显示Storybook中事件处理程序接收的数据。

2.2 @storybook/addon-notes | @storybook/addon-ondevice-notes

Storybook Addon Notes允许您在Storybook中为故事编写注释(文本或HTML)。

Storybook Links addon 可用于创建在故事书中的故事之间导航的链接。在 ReactNative 中链接跳转这个版本好像有点问题,待跟进看下

2.4 @storybook/addon-backgrounds | @storybook/addon-ondevice-backgrounds

Storybook Background Addon 可用于更改故事书中预览中的背景颜色。

2.5 @storybook/addon-knobs | @storybook/addon-ondevice-knobs

Storybook Addon Knobs 允许您使用Storybook UI动态编辑React道具。您还可以使用旋钮作为Storybook中故事的动态变量。

2.6 @storybook/addon-options

  • NOTE: Options Addon is deprecated as of Storybook 5.0,直接内嵌
    • Global options: addParameters({ options: { … }}) and no addon is needed.
    • Story options: storiesOf(…).add(‘name’, storyFn, { options: { … }})

3.总结

  • Storybook 可以通过给每个组件写 story ,然后显示一些使用案例,同时可以根据一些 addon 插件,做到直接调试 UI,同时这样也能很好的管理组件;

  • 可以通过官方给的 addon API 自己写一些好用的插件,可以参考 官方教程

相关链接

  • GitHub 示例代码地址
  • Storybook GitHub
  • Storybook 官网

Jenkins 基本食用方法

0. 介绍

Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。

Jenkins功能包括:

  1. 持续的软件版本发布/测试项目。
  2. 监控外部调用执行的工作。

1. 安装(Mac OS)

通过 brew 安装 Jenkins,其中可能还需要 Java 环境依赖,需要去官网下载 jdk,我这边用的是 jdk1.8

安装: brew install jenkins

  • 直接启动: jenkins 就可以启动,退出命令行就关闭服务

  • 通过 brew services

    启动: brew services start jenkins

    重启: brew services restart jenkins

    关闭: brew services stop jenkins

2. 初始配置

第一次安装后启动需要做初始配置

  • 解锁 Jenkins ,从命令行或者提示的路径找到密码

  • 自定义一些扩展插件,可选择安装推荐的或者自选,选择推荐安装,后续管理插件还能继续安装


  • 创建管理账户

  • 然后就进入 Jenkins 了

  • 插件的查找和安装


  • 简单的创建任务

创建任务,给任务起一个名

任务做配置,如 描述,Git,构建等;

任务构建,以及进度条

构建日志输出

3.一些好用的插件

  • 动态参数构建 Dynamic Extended Choice Parameter
  • Git 参数 Git Parameter

  • Build Name Setter
  • 设置构建后描述 Project Description Setter

  • 设置强制语言包 Locale

4. 一些问题解决

  • 有些时候 git 内容很大,pull 时会出现超时导致构建失败!

解决方式: Git 配置时添加一个 clone 的配置把超时改为 60 或者更多,把超时改大,一般就第一次的 pull 会请求比较久,后续都是增量的,会比较快了;

5. 等等

等待继续研究,估计是和 docker 相关的!

idea 使用一些配置

1. 创建文件自动添加注释

1
2
3
4
5
6
/**
* @Author: ferryvip
* @Description:
* @Date: Create in ${TIME} ${DATE}
*
*/

2.

App 打包构建系统

0. 由来

根据开发测试等实际应用需求设计了一套打包构建系统,系统包括以下主要功能:

  1. App 多环境打包;
  2. App 自动发布热更新;
  3. App 管理托管;

系统的基本组成:

  1. 通过 Jenkins 来做打包,热更新推包,自动测试服务;
  2. 应用打包完成上传到蒲公英,并把蒲公英上的下载地址等信息通过自建的 Node 服务做应用的托管,这样我们就可以在 Node 中对应用做相应的管理操作;
  3. 热更新推包后,我们记录相应的数据后,也记录到 Node 服务中,这样我们也可以在 Node 服务中对热更新做出查看或者撤回更新等操作;

打包对比

手动打包 VS 自动打包

手动打包 自动打包
耗时长,效率低; 自动化打包,解放双手;
重复性多,人工成本高; 配置好后,一键操作;
出错性概率大; 可配置一键远程打包;

1. 主要功能点





2. 主要实现点

利用 App Center 来打包你的应用

App Center 是巨硬(微软)家的一个应用开发管理平台服务,之前主要使用的是他的 CodePush 服务,后续平台升级了,把开发的崩溃收集,数据统计,推送服务,应用构建及测试,CodePush 等多个服务融合在一起,做出的 App Center 平台,基本功能也都是免费的,今天这篇文章主要是讲 iOS (原生和 ReactNative)应用通过 App Center 来打包;

0. 前期准备

  1. App Center 账号(可通过 GitHub 账号授权登录) https://appcenter.ms;
  2. GitHub 账号 https://github.com/;

1. 设置 Build 配置

1.1 仪表盘点击创建应用

1.2 填写应用信息

  1. 填写应用基础信息 –> 应用名称 –> Release type
  2. App Center 可以用来构建多种系统平台的应用,以及不同平台写的应用
  3. 这边我们 OS 选取 iOS , Platform 选 Objective-C / Swift

1.3 添加构建仓库

点击构建配置,连接仓库,选取 GitHub 仓库

通过关联的 GitHub 账号,选取相应的 iOS 仓库

1.4 分支构建配置

根据 GitHub 仓库获取仓库分支,点击分支对应的构建配置

1.5 构建配置

  1. App Center 会读取仓库的文件,让你选取你要构建的 Project/Workspace 和相应 Shared Scheme;
  2. Xcode 版本;
  3. Build scripts 自定义构建脚本,脚本是包含在 git 仓库文件里,具体配置见文档;(可选配置)
  4. 构建触发规则, push 触发构建/手动触发;
  5. 自动增加构建号,自动测试配置;(可选配置)
  6. 环境变量设置,见文档
  7. 签名证书设置,证书导出这边不做过多介绍,可参考链接;
  8. 真机测试,分发构建,构建状态图标;(可选配置)

1.6 分支构建历史

1.7 构建产物

构建完成后有 ipa 包等,可以通过 App Center 分发

但是个人发现访问和下载速度上不是很快,所以通过 Post-build 脚本自己上传到蒲公英托管平台;

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash

if [ "$AGENT_JOBSTATUS" == "Succeeded" ]; then

ukey=蒲公英ukey
apikey=蒲公英apikey
desc=蒲公英上传描述

curl -F "file=@$APPCENTER_OUTPUT_DIRECTORY/$APPCENTER_XCODE_SCHEME.ipa" \
-F "uKey=$ukey" -F "_api_key=$apikey" \
-F "updateDescription=$desc" https://qiniu-storage.pgyer.com/apiv1/app/upload

fi

2. 总结

App Center 里面提供了一系列的关于应用开发的 SDK 和服务,产品做得很全面,还提供了一些云的概念操作(线上打包);

有时间可以多研究 App Center 的这一套东西,如果一个公司规模够大,产品丰富,也可以自己搭建一个自己像这样的一套系统;

微信小程序开发 Jenkins 合并代码提交体验版本

1. 起因

最近公司项目在做小程序开发,有多个小伙伴一起开发,测试的小伙伴也要加入测试,为了做到敏捷开发,我这个懒人就在想有啥办法可以做到这点,最后发现微信的小程序工具提供命令行调用,查看其文档发现了新大陆,也就有了这篇文章;

2. 流程

  1. 开发人员开发后代码推到 git 仓库;
  2. 测试人员或开发触发 Jenkins 构建;
  3. 通过微信的工具上传微信小程序最新代码,并通过钉钉机器人通知打包成功;
  4. 测试就可以打开微信小程序的体验版开始体验最新的修改;

3. Jenkins 配置

1. 配置参数化构建, 主要是配置了 接口请求环境,和打包的 Git 分支

2. Git 配置

3. Build 执行 shell 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo ------------ 构建分支 --------------
echo $BRANCH
echo ------------ 打包环境 --------------
echo $ENV

# 导入 node 环境
export PATH=$PATH:/usr/local/bin

# 执行 接口请求环境的 替换操作
node env.js $ENV

# 上传最新修改到微信小程序
/Applications/wechatwebdevtools.app/Contents/MacOS/cli -u 1.0.0@$PWD --upload-desc 'Jenkins 小程序更新最新代码了,快去体验版查看'
# 上传成功后发送钉钉通知
node index.js $ENV $BRANCH

4. 微信开发工具配置

  1. 微信开发工具登录并打开设置里面安全的服务端口,还有就是微信版本管理把这个上传的微信版本选为体验版,这样下次再上传上去体验版就可以下载最新的上传修改了;

本人手上是有一台 Mac 电脑专门用来打包 App 的,所以只讲了 Mac 的,但道理是差不多一样的,可以自己折腾 Windows 的系统,要在这台打包电脑安装 Jenkins 还有 微信开发工具;(因为自己脚本写的太差了,所以有两个地方用到了 node 来实现,所以还安装了 node 环境);

钉钉设置机器人

之前我已经写了几个涉及钉钉机器人的配置,直接看之前的配置

配置完成后,获取里面的 token

5. 主要代码配置

1. 配置环境的文件 './config/index.js'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 全局配置文件
let HOST = '', USERHOST = '';

let ENV = 'DEV'; // 设置环境 测试 DEV, 正式环境 PROD

if (ENV === 'PROD') {
// 正式环境 host
HOST = 'https://api.prod.com';
USERHOST = 'https://user.prod.com';
} else {
// 测试环境 host
HOST = 'https://api.dev.com';
USERHOST = 'https://user.dev.com';
}

module.exports = {
ENV: ENV,
HOST: HOST,
USERHOST: USERHOST,
}

2. 替换接口环境的文件 './env.js'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const fs = require('fs');
const ENV = process.argv[2];
console.log(`/************* 替换环境为 ${ENV} 开始 *************/`);
// 环境文本
const prodTep = `
// 全局配置文件
let HOST = '', USERHOST = '';
let ENV = '${ENV}'; // 设置环境 测试 DEV, 正式环境 PROD
if (ENV === 'PROD') {
// 正式环境 host
HOST = 'https://api.prod.com';
USERHOST = 'https://user.prod.com';
} else {
// 测试环境 host
HOST = 'https://api.dev.com';
USERHOST = 'https://user.dev.com';
}
module.exports = {
ENV: ENV,
HOST: HOST,
USERHOST: USERHOST,
}
`;
process.chdir(`./config`); // cd $1
console.log(`/************* 前往 config 文件夹,并替换写入文件 *************/`);
console.log(prodTep);
fs.writeFileSync('index.js', prodTep);
console.log(`/************* 替换环境为 ${ENV} 成功 *************/`);
process.exit(0);

3. 发送钉钉消息的文件 './index.js'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const ENV = process.argv[2];
const branch = process.argv[3];

const ddtoken = 'you-dingding-token'; // 钉钉机器人 token
const qrurl = 'you-qrcode-url'; // 微信体验版的 二维码地址

let tokens = [
wscUrl,
];
const env = ENV === 'PROD' ? '正式环境' : '测试环境';
var exec = require('child_process').exec;
// git 最近 5 条修改的 commit 信息
const str = `git log -5 --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:"* %cd - %s "`;
function runExec(cmdStr) {
return new Promise((resolve, reject) => {
exec(cmdStr, function (err, stdout, stderr) {
if (err) {
console.log('runExec error:' + stderr);
reject();
} else {
// console.log(stdout);
resolve(stdout);
}
});
});
}
let gitlog = '';
runExec(str).then(res => {
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
gitlog = res;
sendDing(token, gitlog)
}
});
function sendDing(token, gitlog) {
var https = require('https');
var options = {
hostname: "oapi.dingtalk.com", // 呼叫的域名
port: 443, // 端口固定
path: `/robot/send?access_token=${token}`, // 请求的api名称
method: "POST", // get和post请求
json: true, // 此地方表示json
rejectUnauthorized: true, //请校验服务器证书,否则ssl没有意义。
headers: {
'Accept': 'application/json;version=2.0',
'Content-Type': 'application/json', //此地方和json很有关联,需要注意
}
};
var post_data = {
"msgtype": "markdown",
"markdown": {
"title": "小程序新包更新",
"text": `# 小程序新包发布 \n \n > 分支: ${branch} \n \n > 服务器环境: ${env} \n \n > 更新内容: \n \n ${gitlog} \n \n > 微信扫码体验小程序: ![](${qrurl})`
}
};
var json = JSON.stringify(post_data);
var req = https.request(options, function (res) {
res.setEncoding('utf8');
});
req.on('error', function (e) {
console.log('problem with request: ' + e.message);
});
req.write(json);
req.end();
}

6. 最终钉钉消息效果

地址

GitHub 仓库地址

个人博客

博客地址:
https://gblog.ferryvip.com/

项目说明

在用 ReactNative 开发 App 时,会用到 CodePush 做应用的热更新,但是在升级的时候没有好看的弹窗提示,自己就找了个时间写了一个简单集成 ReactNative 又有好看的更新弹窗;

准备工作

  1. 首先是安装 react-native-code-push, yarn add react-native-code-push;
  2. link 一下项目, react-native link react-native-code-push;
  3. 安装 react-native-code-push-dialog, yarn add react-native-code-push-dialog;

项目中配置

最简单的配置如下,尽量把 节点放在顶层的节点处;

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Button, Dimensions} from 'react-native';


import HotUpdate, { ImmediateCheckCodePush } from 'react-native-code-push-dialog';

export default class Index extends React.Component {
render() {
return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text> CodePushExample </Text>
<HotUpdate />
<Text onPress={()=>{
ImmediateCheckCodePush();
}}>点击立即检查</Text>
</View>;
}
}

更多用法

props PropTypes use description
deploymentKey PropTypes.string jaasdasdsa2w12wed2we3e23 code-push deploymentKey 非必须参数,没有会读取原生的;
isActiveCheck PropTypes.bool true or false code-push CheckFrequency 检查更新策略,只提供2种, true 每次返回前台就更新(高频率), false 只有 App 启动才检测更新, 默认 true;
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Button, Dimensions} from 'react-native';


import HotUpdate, { ImmediateCheckCodePush } from 'react-native-code-push-dialog';

export default class Index extends React.Component {
render() {
return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text> CodePushExample </Text>
<HotUpdate deploymentKey={'askjdkas32232dw32d'} isActiveCheck={false}/>
<Text onPress={()=>{
ImmediateCheckCodePush();
}}>点击立即检查</Text>
</View>;
}
}

推热更新代码的时候如果是有多条类型的,尽量在推更新的时候弄成每条后面回车,这样到时候展示的内容才能换行美观;

code-push release-react App android --des "1.按照区域展示轮播内容; 2.分析数据,优化体验;" -t "5.1.2"

显示效果

地址

GitHub 仓库地址

小型失物招领后端(Koa 版本)

项目说明

本身是搞 iOS 的后面入了 ReactNative 的坑,慢慢的就比较多的接触 JavaScript ,还有就是 JavaScript 慢慢的可以做的东西越来越多;
不仅可以做前端网页,还可以做移动端 App(ReactNative),还能做后端(Node.js),还有其他的;

这个失物招领是为了练手 Node.js,同时也为了配合这个写了个 App 前端(iOS 和 安卓),链接在后面也会放出了;

目前主要的 Feature:

  1. 用户登录注册;
  2. 基本的管理员权限管理;
  3. 失物招领信息发布,关注收藏,通知审核等;
  4. 管理员的基本功能的管理与审核;
  5. 极光推送对接,使内容实时推送给用户;

本地运行条件

  • MongoDB 数据库
  • Node.js 环境

基础配置

  1. 七牛配置
    1. 七牛配置是为了保存图片到七牛;
    2. 申请相应的开发者账号,填入到 config文件夹的config文件;
    3. 配置错误,或未配置调用到会导致程序崩溃;


  1. 极光配置
    1. 使消息即使让用户知道;
    2. 申请相应的开发者账号,填入到 config文件夹的config文件;
    3. 配置错误,或未配置调用到会导致程序崩溃;

造一些假数据的

项目目录下执行 node data.js 两次就行;

1
2
3
4
超级管理员
用户名: admin@163.com
密码: 123456
其他的用户: user[1 - 10]@163.com 密码: 123456

启动

  • 本地启动

    1. 首先启动本地 MongoDB 数据库,项目目录下执行 npm run mongo 数据库跑默认的段口;
    2. 项目目录下执行 npm start 启动;
    3. 就可以打开文档 http://localhost:5566/docs/
  • 服务器部署(主要使用 pm2 部署)

    1. 服务器安装 node pm2 MongoDB 环境等;
    2. 项目目录下执行
      1. 测试 pm2 start dev.json
      2. 正式 pm2 start dev.json
    3. 查看日志 pm2 logs

对应的客户端

  • 目前就只做了 App 端,按理说目前接口基本可以做个网页前端的,微信公众号以及小程序,在做些修改也是应该可以的;

下载体验

iOS 没有发布,下载安卓体验

安卓 fir.im 下载

或者直接扫码下载

扫码下载

仓库地址

Express 版本 GitHub 地址,具体配置看文档

Koa 版本 GitHub 地址,具体配置看文档

App 地址,具体配置看文档

个人博客

博客地址: https://cblog.ferryvip.com/

项目说明

在一些 Node 的项目中,一直使用 log4js 的做日志插件,然后发现里面有个 appenders 插件,可以发送消息到国外软件 slack 里,这样就可以便捷的做一些扩展,可是本人最近在用钉钉,而且那个接触也少,所以本着东西(接口文档的)都有,就找了时间自己撸了一个出来.

准备工作

找了一下两家的文档

log4js 文档

Log4js - Appenders

Slack Appender

钉钉自定义机器人文档

安装

npm install --save log4jsdd log4js

配置 log4js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

const log4js = require('log4js');
log4js.configure({
appenders: {
out: { type: 'stdout'},
dingding: {
type: 'log4jsdd',
hookUrl: '填写获取钉钉里面设置的 webhook 地址',
title: 'Node 消息'
}
},
categories: { default: { appenders: ['out', 'dingding'], level: 'debug' }}
});

let app = log4js.getLogger();
app.info('测试发送到钉钉');

钉钉机器人配置






地址

GitHub 仓库地址

npm 地址

个人博客

博客地址: https://cblog.ferryvip.com/