## 1. Project Overview & Quickstart (dushixiang/next-terminal)

# Next Terminal

[](https://github.com/next-terminal/next-terminal/stargazers)
[](./LICENSE)
[](https://www.next-terminal.com)
[](https://docs.next-terminal.com)
[](https://demo.next-terminal.com)

English | [简体中文](./README-en.md)

Note: Starting with v2.0.0, the backend code for this project is no longer open source.

----

## Introduction

Next Terminal is a simple, secure, and user-friendly interactive auditing system that supports multiple remote access protocols including RDP, SSH, VNC, Telnet, and HTTP. It is designed for enterprise IT environments and helps facilitate session recording, audit tracking, and compliance reporting.

### Quick Start

Refer to the installation guide here:
👉 [Installation Documentation](https://docs.next-terminal.com)


## Screenshots




### License & Terms

Before downloading, using, or distributing Next Terminal, please read and agree to the [LICENSE](./LICENSE).
This project is provided "as is" without any warranties or guarantees. Use at your own risk.

⚠️ It is recommended to consult your **IT administrator** before deploying Next Terminal within a corporate network.

### Security Issues

If you discover any security vulnerabilities, please contact the maintainer:

📧 [support@next-terminal.com](mailto:support@next-terminal.com)

## Contact

- **Website**: https://www.next-terminal.com
- **Docs**: https://docs.next-terminal.com
- **Support email (official)**: [support@next-terminal.com](mailto:support@next-terminal.com) — after-sales within 1 year of purchase, workdays 10:00–18:00; replies on the next workday outside these hours
- **Issues / Bugs / Feature requests**: [GitHub Issues](https://github.com/next-terminal/next-terminal/issues) (public and searchable — your question helps others with the same issue)
- **Community**: Telegram https://t.me/next_terminal · WeChat group — see https://www.next-terminal.com

### Sponsors

CDN acceleration and security protection for this project is sponsored by [Tencent EdgeOne](https://edgeone.ai/zh?from=github).


Acknowledgements

Thanks to the following sponsors for their support:

- [Laika Cloud](https://www.lcayun.com/actcloud.html?from=next-terminal)
- [Digitalvirt](https://digitalvirt.com/aff.php?aff=1326) — Elastic compute service
- [Gomami](https://gomami.io/aff.php?aff=298) — The Fastest China Route



### Recommended Projects

* [go-ldap-admin](https://github.com/eryajf/go-ldap-admin): A web-based OpenLDAP management tool built with Go and Vue

## 2. Official Technical Reference & Guides (dushixiang/dushixiang.github.io)

## File: content/posts/air780e-giffgaff.md

---
title: 30 元成本搞定 GiffGaff 短信转发与自动保活
date: 2025-12-13T21:33:35+08:00
draft: false
build:
  list: never
---

先说明一点：**这个项目并不是重复造轮子。**
如果你搜索过「**air780e 短信转发**」，大概能看到目前主要有两种方案：

**方案一：使用 air780e SIM卡自带流量进行转发**
实现简单，但问题也很明显——像 GiffGaff 这种卡流量价格非常离谱，长期使用成本太高，直接 Pass。

**方案二：air780e + ESP32，通过 WiFi 转发**
这种方式在技术上是可行的，但需要自己焊接 ESP32。
奈何我属于那种**一拿电烙铁手就开始手抖**的人，焊精密元件基本等于劝退 😅。

---

基于以上原因，我最终选择了一条**完全不需要焊接**的路线：

## 串口控制 air780e + Golang 后端

通过串口直接控制 air780e，把短信的收发能力暴露出来，后端使用 **Golang** 来实现统一的服务层。

目前这个方案的特点是：

* **即插即用，无需焊接**
* **硬件成本极低，理论上 air780e xx系列的硬件都支持**
  * 移动 / 联通版本：约 **30 元**
  * 全网通版本：约 **50 元**
* **部署简单**

  * 只需要一台能插 USB 的小型设备
  * 比如 N1 盒子、软路由、迷你主机等
* **功能已经可用**

  * 短信接收
  * 短信发送
  * 消息推送
  * 自动发送 / 自动回复

插上 air780e，运行我写的程序，完事。

项目地址：https://github.com/dushixiang/uart_sms_forwarder

截图如下：





## 其他补充说明

合宙 air780 系列型号极多，刚开始搞的时候我也是一脸懵逼，后面看到一张对比图才了解到它们之间的区别。

https://docs.openluat.com/air780epm/

---

## File: content/posts/base58-implementation-in-go.md

---
title: Base58 编码详解与 Go 实现
date: 2025-08-10T23:36:35+08:00
draft: false
---
在处理二进制数据时，我们经常需要将其转换成可读的字符串，比如生成地址、密钥、邀请码等。
常见方案有 **Base64**、**Hex** 等，但在某些场景下，它们并不是最佳选择。
这时，**Base58** 就登场了。

## 什么是 Base58？

Base58 是一种基于 58 个字符的编码方式，它的设计目标是：

* **避免易混淆字符**（`0` 和 `O`，`l` 和 `I`）
* **不包含特殊符号**（`+` `/` 等），适合直接用于 URL、文件名
* **更适合人工抄写和阅读**，降低输入错误率

它的字符集是：


123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz


> 可以看到，`0`、`O`、`l`、`I` 等容易混淆的字符被剔除，没有 `+`、`/` 这类特殊符号。

---

## 为什么用 Base58？

假设你有一个用户需要手动输入邀请码、钱包地址或交易 ID：

* 如果用 **Base64**，会出现 `+` `/` 等特殊字符，在 URL 中需要转义；
* 如果用 **Hex**，数据会变得很长（每个字节要用两个字符表示）；
* 如果用 **Base58**，长度更短，可读性更高，错误率更低。

这也是为什么比特币、IPFS 等项目都选择了 Base58（或其变种 Base58Check）。

---

## Base58 在实际项目中的应用

* **比特币地址**：Base58Check 编码，防止字符混淆并带有校验码
* **IPFS CID**：文件内容 ID 使用 Base58 编码，更短且可直接用在命令行
* **邀请码 / 短链**：直接放在 URL 中，不需要额外转义

---

## Go 语言 Base58 编码示例

这里用我开源的 Golang Base58 库来演示：

```go
package main

import (
	"fmt"
	"log"

	"github.com/go-orz/base58"
)

func main() {
	data := []byte("Hello, Base58!")

	// 编码
	encoded := base58.EncodeToString(data)
	fmt.Println("Encoded:", encoded)

	// 解码
	decoded, err := base58.DecodeString(encoded)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Decoded:", string(decoded))
}
```

运行结果：

```shell
Encoded: TcgsE5e9XJSrakNTEQQ
Decoded: Hello, Base58!
```

---

## Base58 与 Base64 对比

| 特性     | Base64      | Base58       |
| ------ | ----------- | ------------ |
| 字符集    | 64 个（含 + /） | 58 个（无易混淆字符） |
| URL 友好 | ❌（需转义）      | ✅            |
| 人工抄写友好 | ❌           | ✅            |
| 编码长度   | 短           | 稍长           |
| 编解码速度  | 快           | 略慢           |

如果你的数据主要是**人类可读/可输入**，Base58 更合适；
如果主要是**机器传输**，Base64 更高效。

---

## 项目地址

我开源了一个 Golang 的 Base58 编码库，简单、轻量、无第三方依赖，
欢迎 Star & Fork：

👉 **[https://github.com/go-orz/base58](https://github.com/go-orz/base58)**

---

💡 **总结**：
Base58 解决了 Base64 在人工输入、URL 传递上的不便，特别适合区块链、邀请码、短链接等场景。
有了 Go 的实现，你可以在项目中轻松集成这种编码方式。

---

## File: content/posts/clamav-install.md

---
title: "Linux 杀毒软件 ClamAV 安装"
categories: [ "linux" ]
tags: [ "杀毒软件", "ClamAV" ]
draft: false
slug: "clamav-install"
date: "2022-03-07T21:52:00+08:00"
---

**Clam AntiVirus**（**ClamAV**）是免费而且开放源代码的杀毒软件，软件与病毒码的更新皆由社群免费发布。Github 地址：https://github.com/Cisco-Talos/clamav

### 安装 ClamAV

```bash
yum install -y clamav*
```

### 配置 ClamAV

```bash
cat > /etc/freshclam.conf <<EOF
# 数据库配置文件夹
DatabaseDirectory /var/lib/clamav
# 更新日志文件夹
UpdateLogFile /var/log/freshclam.log
# 日志大小
LogFileMaxSize 2M
# 日志记录时间
LogTime yes
# 所属用户
DatabaseOwner root
# 同步病毒库的地址
DatabaseMirror database.clamav.net
EOF
```

### 更新病毒库

大概需要几分钟时间

```bash
freshclam
```

### 进行病毒扫描测试

```bash
clamscan -ri  
```

常用配置说明

```
--recursive[=yes/no(*)]     -r      递归查找
--infected                  -I      只打印受影响的文件信息
--remove[=yes/no(*)]                删除受影响的文件。(不建议使用,根据扫描结果进行手动删除,避免误删。)
```

### 配置邮箱

如果不需要邮件通知的可以忽略此步骤。

#### 安装邮件服务

```bash
yum install -y mailx
```

#### 修改配置

修改大写字母为你的邮箱配置

```bash
cat > /etc/mail.rc <<EOF
set from=USERNAME@YOURDOMAIN.COM
set smtp=smtps://smtp.exmail.qq.com:465
set smtp-auth=login
set smtp-auth-user=USERNAME@YOURDOMAIN.COM
set smtp-auth-password=YOURPASSWORD
set ssl-verify=ignore
set nss-config-dir=/etc/pki/nssdb/
EOF
```

#### 发送邮件进行测试

```bash
echo "Your message" | mail -v -s "Message Subject" email@address
```

如果出现 “Error in certificate: Peer's certificate issuer is not recognized” 属于正常。

### 定期扫描

使用 `crontab -e` 增加两个定时任务

```bash
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -ri  | mail -s "病毒文件扫描报告" 'youremailaddress'
```

如果不需要发送邮件使用下面的配置

```bash
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -ri 
```

---

## File: content/posts/collect-vm-stats-by-libvirt-java.md

---
title: "使用libvirt-java采集KVM虚拟机状态信息"
categories: [ "虚拟化" ]
tags: [ "Java","libvirt" ]
draft: false
slug: "collect-vm-stats-by-libvirt-java"
date: "2021-05-19T20:18:20+08:00"
---

虚拟化开发相较于普通开发是一个冷门的方向，大多数是使用Python开发，其中使用Java来做虚拟化的少之又少，资料更是少的可怜，为了实现需求我也是踩了不少坑，今天就为大家分享一下如何使用 `libvirt-java` 来采集KVM虚拟机的资源使用信息。

### CPU使用率

`libvirt`并没有直接提供获取虚拟机CPU使用率的接口，需要我们自己来计算，网上分享的代码或者公式五花八门，大部分都是错误的，经过我的测试，找到了一个相对准确的计算公式。

```latex
cpu_usage = (cpu_time_now - cpu_time_t_second_ago) * 100 / (t * vCpus * 10^9)
```

Java代码如下

```java
// t秒前的CPU时间
long c1 = domain.getInfo().cpuTime;
Thread.sleep(1000);
// 当前CPU时间
long c2 = domain.getInfo().cpuTime;

// 虚拟CPU数量
int vCpus = domain.getMaxVcpus();
// t 为1秒
Double cpuUsage = 100 * (c2 - c1) / (1 * vCpus * Math.pow(10, 9));
log.debug("虚拟机[{}]CPU使用率为: {}", uuid, cpuUsage);
```

### 内存使用率

不要使用`domain.getInfo()`返回的 `memory`字段，虽然它注释写的是`the memory in KBytes used by the domain`，但它的意思真的不是虚拟机内部进程已使用的内存大小，而是从宿主机器的角度来看分配给这个虚拟机的内存它使用了多少，如果没有特殊配置，它会和`maxMem`字段的值是相同的。

正确做法是使用`domain.memoryStats(10)`来获取，那为什么参数要输入一个`10`呢？这是因为`10`代表的是要返回的信息数量，经过我手动执行`virsh dommemstat uuid` 测试发现有10个参数返回，所以需要填入`10`。另外命令返回的`unused` 字段值与数组中`tag=8`的数据一致，最终我们获取到了未使用的内存大小，计算内存使用率更是轻轻松松。

Java代码如下

```java
MemoryStatistic[] memoryStatistics = domain.memoryStats(10);
Optional<MemoryStatistic> first = Arrays.stream(memoryStatistics).filter(x -> x.getTag() == 8).findFirst();
if (first.isPresent()) {
  MemoryStatistic memoryStatistic = first.get();
  long unusedMemory = memoryStatistic.getValue();
  long maxMemory = domain.getMaxMemory();
  double memoryUsage = (maxMemory - unusedMemory) * 100.0 / maxMemory;
  log.debug("虚拟机[{}]内存使用率为: {}", uuid, memoryUsage);
}
```

### 网卡数据包信息

同样`libvirt`并没有提供获取虚拟机网卡的接口，因此需要获取虚拟机的xml文件来查询。

此处没有什么坑，解析`xml`是使用了`html`解析库`Jsoup`，`xml`算是`html`的亲戚吧，比`html`书写严格很多，解析数据更为方便。

获取到网卡名称之后再获取统计信息，可以获取的数据有：

| 字段       | 含义               |
| ---------- | ------------------ |
| rx_bytes   | 接收数据包大小     |
| rx_packets | 接收数据包数量     |
| rx_errs    | 接收错误数据包数量 |
| rx_drop    | 接收丢弃数据包数量 |
| tx_bytes   | 发送数据包大小     |
| tx_packets | 发送数据包数量     |
| tx_errs    | 发送错误数据包数量 |
| tx_drop    | 发送丢弃数据包数量 |

Java代码如下

```java
String xmlDesc = domain.getXMLDesc(0);
Document document = Jsoup.parse(xmlDesc);

// 网卡
Elements interfaces = document.getElementsByTag("devices").get(0).getElementsByTag("interface");
for (Element inter : interfaces) {
  String interName = inter.getElementsByTag("target").get(0).attr("dev");
  DomainInterfaceStats domainInterfaceStats = domain.interfaceStats(interName);
  log.debug("dev {} stats {}", interName, Json.toJsonString(domainInterfaceStats));
}
```

### 磁盘IO信息

和网卡同样，`libvirt`也没有提供获取虚拟机磁盘的接口，还是需要获取虚拟机的xml文件来查询，获取到磁盘名称之后再获取统计信息，可以获取的数据有：

| 字段     | 含义           |
| -------- | -------------- |
| rd_req   | 读取请求总数   |
| rd_bytes | 读取的数据大小 |
| wr_req   | 写入请求总数   |
| wr_bytes | 写入的数据大小 |
| errs     | 失败次数       |

Java代码如下

```java
String xmlDesc = domain.getXMLDesc(0);
Document document = Jsoup.parse(xmlDesc);
// 磁盘
Elements disks = document.getElementsByTag("devices").get(0).getElementsByTag("disk");
for (Element disk : disks) {
  String dev = disk.getElementsByTag("target").get(0).attr("dev");
  DomainBlockStats domainBlockStats = domain.blockStats(dev);
  log.debug("dev {} stats {}", dev, Json.toJsonString(domainBlockStats));
}
```

至此采集虚拟机状态信息算是告一段落，学的越多才发现不会的越多...

---

## File: content/posts/cyberpoc.md

+++
date = '2025-09-06T00:00:00+08:00'
draft = false
title = '我为什么开源了三年前写的漏洞验证平台？'
+++

大概三年前，我看到有人用代码片段分享漏洞原理，觉得很有意思，我也模仿设计了一些 Golang 的漏洞。

可惜想要验证，得自己搭建环境，门槛不低。

于是我又做了一个平台，把漏洞环境封装成 Docker 镜像。用户只需点一下，就能在几秒内启动环境，直接验证。

第一个版本很快上线，我陆续封装了十几个漏洞环境。但后来越做越复杂，加了虚拟机管理和分布式网络，结果部署维护麻烦到连我自己都不想搞了，再加上我了解到的 Golang 漏洞就那么多，平台没人用，项目也就搁置了。

最近我做了一个违背祖宗的决定：去掉多余的东西，只保留容器化的漏洞环境，并把平台和漏洞环境全部开源。

项目和漏洞环境都是用 Golang 写的，配置要求很低，开箱即用。
如果你觉得有用，欢迎点个 star：

- 漏洞环境：https://github.com/dushixiang/vulnerable-code
- 验证平台：https://github.com/dushixiang/cyberpoc
- 测试地址：https://cyberpoc.typesafe.cn

附上几张截图

---

## File: content/posts/golang-binary-linking-types.md

+++
date = '2025-01-07T00:00:00+08:00'
draft = false
title = 'Golang 二进制文件的静态和动态链接'
+++


## 概述

Golang 最大的优势之一就是支持跨平台编译，真正的实现了Java的梦想：一次编译，到处运行。

不过，在某些特定情况下，也会存在一些特殊之处。

## 静态链接与动态链接

**静态链接**是是在编译时将程序与所依赖的所有库（通常是 `.a` 或 `.lib` 文件）一起打包，生成一个独立的可执行文件。也就是说，所有需要的函数和库代码在编译过程中就已经嵌入到可执行文件中。

因为这样编译出来的二进制更便携，它不需要在其运行的主机系统上存在库。因此，你的二进制文件可以在任何系统上运行，无论哪个发行版/版本，并且不依赖于任何系统库。

**动态链接**是在程序运行时（即执行时）将程序与共享库（如 `.dll`、`.so` 或 `.dylib` 文件）链接。程序本身包含对这些库的引用，而不直接包含库的代码。程序启动时，操作系统会加载这些库文件并链接到程序。

这种方式的优点是比较灵活，如果依赖的库发生了更新，只要接口没变，程序不需要编译就可以使用最新版本的库，而且因为二进制包含对库的引用，不包含库代码，因此文件较小。
## 静态链接程序

我们先来编写一个简单的 “Hello World” 程序，，经过打包后，它将成为一个静态链接的二进制文件。

```go
package main  
  
func main() {  
    println("hello world!")  
}
```

接着编译这个 Go 文件：

```bash
go build -o main1 main.go
```

然后使用 `file` 命令来检查文件类型：

```bash
$ file main1 | tr , '\n' 
hello_linux: ELF 64-bit LSB executable
 x86-64
 version 1 (SYSV)
 statically linked
 Go BuildID=...
 with debug_info
 not stripped
```

从上述命令的输出结果可知，`main1` 文件属于 **64 位静态链接的 ELF 格式** 可执行文件，适用于 **x86-64** 架构，包含了 **调试信息**，并且没有进行 **剥离**（即保留了调试符号）。

另外，还有 Linux 程序 `ldd` 能够帮助我们判断二进制文件是静态链接还是动态链接，执行以下命令：

```bash
$ ldd main1
not a dynamic executable
```

## 动态链接程序

在 Golang 中，有一种名为 CGO 的机制，它可以用来调用 C 代码，就连 Go 的标准库也在使用这一机制，例如 `net` 包（[https://pkg.go.dev/net](https://pkg.go.dev/net)），其就借助了 C 库来处理 DNS。以下是相关示例代码：

```go
package main  
  
import (  
    "fmt"  
    "log"    
    "net"
)  
  
func main() {  
    ns, err := net.LookupNS("typesafe.cn")  
    if err != nil {  
       log.Fatal(err)  
    }  
    for _, n := range ns {  
       fmt.Printf("%+v\n", n)  
    }  
}
```

同样需要编译这个 Go 文件：

```bash
go build -o main2 main2.go
```


之后，我们再次利用 `file` 和 `ldd` 程序来检查第二个二进制文件，操作及结果如下：

```bash
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
 x86-64
 version 1 (SYSV)
 dynamically linked
 interpreter /lib64/ld-linux-x86-64.so.2
 Go BuildID=...
 with debug_info
 not stripped
```

```bash
$ ldd main2
        linux-vdso.so.1 (0x00007ffc09ec8000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3651936000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3651b21000)
```

由此可以看出，`main2` 是一个 **64 位动态链接的 ELF 可执行文件**，其运行依赖于 **glibc**（`libc.so.6`）以及 **动态链接器**（`ld-linux-x86-64.so.2`），并且包含 **调试信息**，未进行剥离操作。在运行时，会通过 **动态链接器** 来加载这些共享库。

## 将动态链接转为静态链接

有意思的是，Go 的 `net` 包提供了纯 Go 实现，这使得我们在编译时可以禁用 `cgo`。具体而言，可以通过指定构建标签或者设置 **CGO_ENABLED=0** 来彻底禁用 `cgo`。示例如下：

```bash
$ CGO_ENABLED=0 go build -o main2 main2.go
$ ldd main2
not a dynamic executable
```

从上述输出结果能够证实，通过这两种方式操作后，最终都能得到一个静态二进制文件。
## 交叉编译

如文章开头所提到的，交叉编译是 Golang 的一大显著优势，它能够让代码在各种不同的平台与架构上运行。以下是一些常见的交叉编译命令示例：

```bash
# 编译为 macOS 上的 amd64 架构
GOOS=darwin GOARCH=amd64 go build -o main2_darwin_amd64 main2.go

# 编译为 macOS 上的 arm64 架构
GOOS=darwin GOARCH=arm64 go build -o main2_darwin_arm64 main2.go

# 编译为 Linux 上的 amd64 架构
GOOS=linux GOARCH=amd64 go build -o main2_linux_amd64 main2.go

# 编译为 Linux 上的 arm64 架构
GOOS=linux GOARCH=arm64 go build -o main2_linux_arm64 main2.go

# 编译为 Linux 上的 386 架构 (32-bit)
GOOS=linux GOARCH=386 go build -o main2_linux_386 main2.g
...
```

需要注意的是，前提是没有使用 CGO，一旦使用了 CGO，那么运行平台基本就会和开发平台绑定起来了。

## 减少二进制大小

或许你已经留意到，在上述 `file` 命令输出结果中出现了 “with debug_info not stripped” 这样的提示，这意味着二进制文件包含了调试信息。通常情况下，我们并不需要这些调试信息，将其删除能够有效减小二进制文件的大小，示例操作及结果如下：

```bash
$ CGO_ENABLED=0 go build -o main2 main.go
$ ll -h main2 
-rwxr-xr-x 1 root root 2.9M  1月 5日 15:32 main2
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
 x86-64
 version 1 (SYSV)
 statically linked
 Go BuildID=...
 with debug_info
 not stripped
$ CGO_ENABLED=0 go build -ldflags "-s -w" -o main2 main.go
$ ll -h main2 
-rwxr-xr-x 1 root root 2.0M  1月 5日 15:33 main2
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
 x86-64
 version 1 (SYSV)
 statically linked
 Go BuildID=...
 stripped
```

## 结语

静态链接和动态链接各有千秋，根据自身的业务选择合适的方案即可。

---

## File: content/posts/golang-unmarshal-vuln.md

+++
date = '2025-06-23T22:00:00+08:00'
draft = true
title = 'Golang Json 反序列化漏洞'
+++

众所周知，Java 的 f***json 反序列化漏洞养活了国内大半的安全从业人员。

作为一个前 Java 现 Go选手，Go 中容易写出来的安全漏洞的代码我也搞过几十个案例，最近我又在网络上发现了一个 Go 中 json 反序列化的非预期行为可以绕过身份验证、规避授权控制的案例。

举个例子。

假设 用户 结构体如下

```go
type User struct {  
    Username string `json:"username,omitempty"`  
    Password string `json:"password,omitempty"`  
    IsAdmin  bool   `json:"-,omitempty"`  
}
```

我们希望在用户注册的接口，反序列化的时候忽略 `IsAdmin` 这个字段，将会默认设置为 false。

不懂 Go 语言的也没关系，让AI来解释一下。





让我们来写一段代码来模拟注册用户的逻辑。

```go
package main  
  
import (  
    "encoding/json"  
    "fmt"    "log"
)  
  
type User struct {  
    Username string `json:"username,omitempty"`  
    Password string `json:"password,omitempty"`  
    IsAdmin  bool   `json:"-,omitempty"`  
}  
  
func register() {  
    // 模拟用户提交的 JSON 字符串
    var input = `{  
    "username": "dushixiang",    
    "password": "password",    
    "-": true
}`  
    // 解析JSON字符串到User结构体  
    var user User  
    err := json.Unmarshal([]byte(input), &user)  
    if err != nil {  
       log.Fatal(err)  
    }  
    // 模拟保存用户到数据库  
    // ...  
  
    fmt.Printf("%+v", user)  
}  
  
func main() {  
    register()  
}
```

您猜怎么着？这段代码将会输出

```shell
{Username:dushixiang Password:password IsAdmin:true}
```

你问我这怎么了？

用户直接注册成你系统的管理员了，你还问我怎么了。

你问我这是什么问题导致的？

背后的原因让人暖心，AI 说对了一半。

~~`-` 和 `omitempty` 确实会冲突，但是结果是  `omitempty`  会让 `-` 作为  `IsAdmin`  字段的 json key 参与反序列化，最终导致了 `IsAdmin` 被用户提交的内容赋值了。~~

~~解决方案也很简单，把 `,omitempty` 移除就好了。~~

经过其他大佬的指点，实际上是 `json: "-"` 和 `json: "-,"` 的行为不一致，加上逗号就是把 `-` 当作 json key。

解决方案也很简单，把 `,` 移除就好了。

---

## File: content/posts/good-system-design-summary.md

---
title: 《关于优秀系统设计我所知道的一切》的总结
date: 2025-08-22T13:15:30+08:00
draft: false
---
今天看到 GitHub 高级工程师写的一篇文章《Everything I know about good system design》，写的很好。

链接地址：

https://www.seangoedecke.com/good-system-design/

总结一下。

> **真正优秀的系统设计是“不起眼的”（underwhelming）和简单的**。它追求的是长期稳定运行，而不是使用各种时髦、复杂的技术。一个看起来令人印象深刻的复杂系统，往往掩盖了糟糕的底层决策。

### 核心设计哲学

* **好的设计是无趣的**：当你感觉某个系统“比预想的简单”或“从来不用操心”时，这恰恰是好设计的标志。
* **从简单开始**：一个能正常工作的复杂系统，总是从一个能正常工作的简单系统演化而来的。直接从零开始构建复杂系统是个坏主意。

---

### 状态管理 (State)

* **状态是万恶之源**：软件设计中最难的部分就是管理状态（State）。无状态（Stateless）的服务更容易维护，因为出问题时可以直接重启恢复。
* **集中管理状态**：尽量减少有状态的组件。最佳实践是让一个专门的服务与数据库交互来管理状态，而其他服务通过 API 请求或事件与该服务通信，而不是让多个服务都去读写同一个数据库表。

---

### 核心组件与模式

* **数据库 (Database)**
    * 数据库访问通常是性能瓶颈。
    * **Schema 设计**要在灵活性和可读性之间找到平衡。
    * 为常用查询创建**索引 (Index)**，并将高基数（high-cardinality）的字段放在前面。
    * 尽可能让数据库完成工作（如使用 `JOIN`），而不是在应用内存中处理数据。
    * 将**读请求**分发到只读副本（read-replicas）以减轻主库压力。

* **处理慢操作：后台作业 (Background Jobs)**
    * 面向用户的操作必须快速响应（几百毫秒内）。
    * 耗时长的任务（如转换一个大文件）应该放入**后台作业**异步处理。这通常通过一个队列（Queue）和作业执行器（Job Runner）实现。

* **缓存 (Caching)**
    * 缓存是解决昂贵重复计算的经典方案，但它本质上也是一种**状态**，会引入复杂性（如缓存失效、数据不一致）。
    * 高级工程师倾向于尽可能少地使用缓存。在使用缓存前，应首先尝试从根本上优化性能（例如，为慢查询添加索引）。

* **事件驱动 (Events)**
    * 事件中心（如 Kafka）用于实现服务解耦。一个服务发布“某事发生”的事件，多个下游服务可以消费该事件并各自处理。
    * 适用于**发送方不关心消费者行为**的场景。不要过度使用，很多时候直接的 API 调用更简单、更易于追踪。

---

### 系统运维与容错

* **关注关键路径 (Hot Paths)**
    * 设计时应集中精力处理系统中**最重要**和**流量最大**的部分。这些路径的设计方案选择更少，且一旦出错影响巨大。

* **日志与监控 (Logging & Metrics)**
    * 在**异常路径**（unhappy path）中积极记录日志，以便于排查问题。
    * 除了平均响应时间，还应监控 **p95 和 p99** 分位值，因为这能反映你最大、最重要客户的真实体验。

* **容错设计 (Failing Gracefully)**
    * **重试 (Retries)**：不能盲目重试，可能会压垮下游服务。写操作需要使用“幂等键 (idempotency key)”来防止重复执行。
    * **故障开放/关闭 (Fail Open/Closed)**：必须明确组件故障时的行为。例如，**限流**系统应“故障开放”（允许请求通过），而**认证**系统则必须“故障关闭”（拒绝访问）。

### 结论

好的系统设计不是关于使用各种“聪明技巧”，而是关于**知道如何将那些无聊、经过充分测试的组件用在正确的地方**。就像好的管道工一样，如果你在工作中寻求刺激，最后很可能会搞得一团糟。

---

## File: content/posts/how-does-java-make-methods-execute-only-once.md

---
title: "在 Java 里如何让方法只执行一次？"
categories: [ "Java" ]
tags: ["Java","并发"]
draft: false
slug: "how-does-java-make-methods-execute-only-once"
date: "2023-04-02T13:18:00+08:00"
---

最近一年时间一直在写 Golang ，也算是对 Golang 有了初步的掌握，再次写 Java 的时候发现有点生疏了，写代码的时候也不自觉代入了写 Golang 的思维。

正如我想要在 Java 里面想让某一个方法只执行一次的时候，我第一时间想到了 Golang 里面的 `Once` 功能。

> `sync.Once` 是 Golang 的一个并发原语，它提供了一种安全地在多个 goroutine 中执行某个函数（或代码块）一次的机制。
> 
> `sync.Once` 类型有一个 `Do` 方法，该方法接收一个函数作为参数，并确保这个函数只会被执行一次，无论有多少个 goroutine 同时调用它。具体来说，第一个调用 Do 方法的 goroutine 会执行这个函数，而其他 goroutine 则会等待它完成，然后返回相同的结果。
>
> `sync.Once` 可以用于一些需要全局初始化的场景，比如初始化配置信息、数据库连接等。使用 sync.Once 可以确保这些初始化只会被执行一次，并且可以安全地被多个 goroutine 共享使用。
> 
> -- 来自 ChatGPT

其实和`单例模式`差不多，但我想要的是让方法只执行一次，我魔改了一下，直接上代码吧。

```java
package cn.typesafe.sync;

import lombok.SneakyThrows;
import java.util.concurrent.Callable;

public class Once<T> {
	private volatile T t = null;

	@SneakyThrows
	public T doOnce(Callable<T> action) {
		if (t == null) {
			synchronized (this) {
				if (t == null) {
					t = action.call();
				}
			}
		}
		return t;
	}
}
```

单元测试

```java
package cn.typesafe.sync;

import cn.typesafe.sync.Once;
import org.junit.jupiter.api.Test;
import org.wildfly.common.Assert;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class OnceTest {

	@Test
	public void onceTest() {
		Once once = new Once<>();
		AtomicInteger count = new AtomicInteger(0);

		Integer value1 = once.doOnce(count::incrementAndGet);

		Assert.assertTrue(value1.equals(1));

		Integer value2 = once.doOnce(count::incrementAndGet);

		Assert.assertTrue(value2.equals(1));
	}

	@Test
	public void onceConcurrentTest() throws InterruptedException {

		Once once = new Once<>();
		AtomicInteger count = new AtomicInteger(0);

		int threadCount = Runtime.getRuntime().availableProcessors() * 2;
		ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
		CountDownLatch countDownLatch = new CountDownLatch(threadCount);

		for (int i = 0; i < threadCount; i++) {
			executorService.execute(() -> {
				Integer value1 = once.doOnce(count::incrementAndGet);

				Assert.assertTrue(value1.equals(1));
				countDownLatch.countDown();
			});
		}


		countDownLatch.await();
	}
}
```