Golang
2023-05-29
Go 并没有支持集合类型,我们需要自己实现:
https://go.dev/play/p/uVDCiN4Cbpt
package main
import "fmt"
type Set map[string]bool
func (s Set) Add(item string) {
s[item] = true
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set)
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
注意:
- 使用 map 做底层存储,因此实现的 set 也是无序的
- map 不是线程安全的,如果有并发操作,需要加锁
- 如果真的要使用集合类型,应该再扩充一下交集,差集等方法
改进
参考 https://github.com/deckarep/golang-set 的设计:
https://go.dev/play/p/BKWT84lXfuz
package main
import "fmt"
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
func (s Set[T]) Remove(item T) {
delete(s, item)
}
func (s Set[T]) Contains(item T) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set[string])
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
优化点:
- 空结构体不占空间
- 泛型让代码复用性更好
文学 阅读
2023-05-27

计算机网络
2023-05-20
There’s more than one way to write an IP address 介绍了 IP 地址的另外几种不常用表示方法。
SSH
2023-05-19
Closing a stale SSH connection(关闭过时的 SSH 连接)中介绍了利用 SSH 转义序列来关闭失去响应的 SSH 连接。
也就是说在 SSH 终端输入 ~. 会直接中断 SSH 连接。
经过试验,确实有效(使用 SSH 代理建立的连接就没效)。
所有 SSH 转义序列:
[root@dell ~]# ~?
Supported escape sequences:
~. - terminate connection (and any multiplexed sessions)
~B - send a BREAK to the remote system
~C - open a command line
~R - request rekey
~V/v - decrease/increase verbosity (LogLevel)
~^Z - suspend ssh
~# - list forwarded connections
~& - background ssh (when waiting for connections to terminate)
~? - this message
~~ - send the escape character by typing it twice
(Note that escapes are only recognized immediately after newline.)
低代码 Frappe
2023-05-08

Frappe Framework 是一个基于 Python 的开源 Web 应用框架,定位于低代码业务平台,强调通过元数据驱动快速构建企业级系统。其核心抽象是 DocType(数据模型),开发者通过定义数据结构即可自动生成数据库表、表单界面、列表视图及基础 API,大幅降低 CRUD 类系统开发成本。框架内置权限控制、工作流引擎、报表系统和 REST 接口,支持多租户架构,适合 SaaS 化部署。
在 UI 层,Frappe 采用“后端驱动 + 自动生成”的模式,基于 DocType 自动生成标准后台界面(Form、List、Report 等),多数中后台场景无需手写前端代码。同时也提供分层扩展能力:通过 Client Script 实现表单交互与校验,通过 Custom Page 构建自定义页面,或在需要时完全接管前端,采用 Vue/React 等框架调用其 REST API,实现 Headless 架构。
后端方面,开发者可基于 Python 扩展业务逻辑(如 hooks、controller、scheduler),并通过模块化 App 机制进行功能解耦与复用。Frappe 提供完整的开发与运维工具链(如 Bench),支持插件化扩展,生态中典型应用包括 ERPNext 等。总体来看,Frappe 更接近一个“可编程业务平台”,适用于中后台系统、流程驱动型应用以及需要快速迭代的企业级场景。
授权协议:Frappe 框架是 MIT 协议,官方提供的应用都是 GPLv3 或者 AGPLv3 协议。
架构

依赖
- 后端
- Python 3.10+
- MariaDB / Postgres
- Redis 6
- Nginx
- cron
- 前端
- Node.js 16
- BootStrap
- jQuery
安装
sudo apt install -y git python-dev python-pip redis-server
sudo apt install -y mariadb-server mariadb-client
pip3 install frappe-bench
bench --version
➜ type bench
bench is /home/catroll/.python/bin/bench
➜ cat /home/catroll/.python/bin/bench
#!/home/catroll/.python/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from bench.cli import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())
➜ bench --help
WARN: Command not being executed in bench directory
Usage: [OPTIONS] COMMAND [ARGS]...
Options:
--version
--use-feature TEXT
-v, --verbose
--help Show this message and exit.
Commands:
app-cache View or remove items belonging to bench...
backup-all-sites Backup all sites in current bench
config Change bench configuration
disable-production Disables production environment for the bench.
download-translations Download latest translations
drop
exclude-app Exclude app from updating
find Finds benches recursively from location
get Clone an app from the internet or filesystem...
get-app Clone an app from the internet or filesystem...
include-app Include app for updating
init Initialize a new bench instance in the...
install Install system dependencies for setting up...
migrate-env Migrate Virtual Environment to desired Python...
new-app Create a new Frappe application under apps folder
pip For pip help use `bench pip help [COMMAND]` or...
remote-reset-url Reset app remote url to frappe official
remote-set-url Set app remote url
remote-urls Show apps remote url
remove Completely remove app from bench and re-build...
remove-app Completely remove app from bench and re-build...
renew-lets-encrypt Sets Up latest cron and Renew Let's Encrypt...
restart Restart supervisor processes or systemd units
retry-upgrade Retry a failed upgrade
rm Completely remove app from bench and re-build...
set-mariadb-host Set MariaDB host for bench
set-nginx-port Set NGINX port for site
set-redis-cache-host Set Redis cache host for bench
set-redis-queue-host Set Redis queue host for bench
set-redis-socketio-host Set Redis socketio host for bench
set-ssl-certificate Set SSL certificate path for site
set-ssl-key Set SSL certificate private key path for site
set-url-root Set URL root for site
setup Setup command group for enabling setting up a...
src Prints bench source folder path, which can be...
start Start Frappe development processes
switch-to-branch Switch all apps to specified branch, or...
switch-to-develop Switch frappe and erpnext to develop branch
update Performs an update operation on current bench.
validate-dependencies Validates that all requirements specified in...
bench init
➜ sudo mkdir -p /opt/frappe
➜ sudo chown catroll:catroll /opt/frappe
➜ cd /opt/frappe
bench init helloworld --skip-redis-config-generation --version version-15 --frappe-path https://gitee.com/mirrors/frappe
# 通过 Git 克隆一个 Frappe 项目到本地,
# git clone https://gitee.com/mirrors/frappe --branch version-16 --depth 1 --origin upstream
# 创建 Python 虚拟环境并安装 Python 依赖,
# uv pip install --quiet --upgrade -e helloworld/apps/frappe --python helloworld/env/bin/python
# 然后使用 yarn 安装 Node 依赖,
# yarn install --check-files
# 还有就是编译多语言文件(MO 文件)。
# --skip-redis-config-generation
# 默认 Redis Server 在本地安装,会执行本地命令获取 Redis 版本号等操作。
# 跳过 Redis 配置,后面在 common-site-config 中手动配置。
# --version version-15
# 默认使用主干开发分支,我这里制定一个稳定版本。
# 而且 16/17 要求 Python 3.14 以上,而我本地安装的是 Python 3.12,懒得折腾。
# --frappe-path https://gitee.com/mirrors/frappe
# 使用国内镜像,稍微提提速。
cd helloworld
bench start
➜ tree -L 2
.
├── Procfile
├── apps
│ └── frappe
├── config
│ └── pids
├── env
│ ├── CACHEDIR.TAG
│ ├── bin
│ ├── lib
│ ├── lib64 -> lib
│ ├── pyvenv.cfg
│ └── share
├── logs
│ └── bench.log
├── patches.txt
└── sites
├── apps.json
├── apps.txt
├── assets
└── common_site_config.json
13 directories, 8 files
默认生成的 helloworld/sites/common_site_config.json 文件:
{
"background_workers": 1,
"file_watcher_port": 6787,
"frappe_user": "catroll",
"gunicorn_workers": 41,
"live_reload": true,
"rebase_on_pull": false,
"redis_cache": "redis://127.0.0.1:13000",
"redis_queue": "redis://127.0.0.1:11000",
"redis_socketio": "redis://127.0.0.1:13000",
"restart_supervisor_on_update": false,
"restart_systemd_on_update": false,
"serve_default_site": true,
"shallow_clone": true,
"socketio_port": 9000,
"use_redis_auth": false,
"webserver_port": 8000
}
Frappe Products
Business Apps
-
ERPNext
Manage accounting, inventory, and operations
-
Frappe HR
Handle employees, attendance, payroll, and HR processes
-
Learning(在线课程 LMS)
Create, manage, and deliver structured online courses
-
Insights(BI)
Analyze data and create interactive dashboards and reports
-
CRM
Track leads, contacts, deals, and customer interactions
-
Helpdesk
Deliver customer support through tickets and workflows
-
Lending(金融借贷)
Manage loans, repayments, and borrower accounts
Developer Tools
Productivity Tools
-
Gameplan(项目管理)
Plan work, share updates, and track progress across teams
-
Drive(类似 Google Drive 的文件系统)
Store, share, and collaborate on documents
Infrastructure
Libraries
-
Bench(开发与运维工具)
Command-line tool for managing multi-tenant Frappe deployments
-
Datatable(表格库)
JavaScript library for building interactive data tables
-
Charts(图表库)
JavaScript library for creating interactive charts and visualizations
-
Gantt(甘特图库)
Gantt chart library for creating interactive project timelines
AI
2023-05-05
体验
获得 ChatGPT 4 的资格(购买 Pro)之后,就可以看到左边页面多了一个 Model 选项,选中了 GPT 4
如果 Model 选择 Plugin 那一项,右边又会多出来一个 Plugins 选项


右边的 Plugins 选项一直往下拖,最下面一栏是 Plugin store,点击进入。

上面的插件可以选中体验体验。
安装
可以看到下方有 Install an unverified plugin,Develop your own plugin 两项。
我们开发的插件就是服务器端 API + 相关声明文件,如果就只是放在自己的服务器上,那就算 unverified plugin。
第一选项就是用来安装这样未经验证的插件,可以在 https://www.gptplugins.app/ 找一个试一下。
输入域名,ChatGPT 自动去获取声明文件 https://域名/.well-known/ai-plugin.json。

第二项是用来注册插件到 ChatGPT,也可以用来调试本地插件。
如果是注册插件就填域名好了,如果是调试就输入 localhost:3000 这样的地址。
我用局域网 IP,似乎是不行的,可能只支持 localhost 这个主机名。

使用
现阶段最多能够同时勾选三个插件。
聊天过程中,ChatGPT 自动判断是否需要触发插件的使用。

开发
经过我的体验,开发非常简单,除了原本的服务之外,需要的额外工作就两项:清单文件,OpenAPI(如果原本没有的话)。
清单文件:
{
"schema_version": "v1",
"name_for_model": "todo",
"name_for_human": "Todo Plugin",
"description_for_model": "Simple task management, task description, task date, task completion. Supports adding, deleting, and querying.",
"description_for_human": "Simple task management.",
"auth": {
// 本地测试 Auth Type 必须是 none
"type": "none"
},
"api": {
"url": "http://localhost:8080/.well-known/openapi.yaml",
"has_user_authentication": true,
"type": "openapi"
},
"logo_url": "http://localhost:8080/.well-known/logo.png",
"contact_email": "hello@contact.com",
"legal_info_url": "hello@legal.com"
}
Redis
2023-04-30
我一两年前设计的一个通过 Redis ZSet 做事件广播的方案,刚用 Python 写了一个示例代码贴出来。
- 这是一个 Push / Pull 方式的广播机制。
- 推送方将消息推送到一个 zset key 中,score 为毫秒时间戳。
- key 名为 xxx:timestamp//10,也就是精确到 10 秒的时间戳。
也就是说每 10 秒一个 key,通过 TTL(5 分钟)实现历史数据自动清除,也避免 event 太多导致大 key 的问题。
- 拉取方用上一次拉取时间和当前时间做 score range,从最近的三个 zset 中读到这个时间段内的事件。
import logging
import threading
import time
import redis
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s %(message)s')
redis_host = '127.0.0.1'
redis_port = 6379
redis_db = 1
redis_password = None
redis_prefix = 'broadcast:'
redis_conn = redis.StrictRedis(host=redis_host, port=redis_port, db=redis_db, password=redis_password)
def handle_broadcast(data):
# 这里是处理收到的广播请求数据的函数
# 你需要根据具体需求来实现这个函数
logging.info(f'处理广播请求数据:{data} ===== ===== ===== =====')
def event_broadcast(data):
now = time.time()
now_ms = int(now * 1000)
now_10s = int(now) // 10
key = redis_prefix + str(now_10s)
score = now_ms
pipeline = redis_conn.pipeline()
pipeline.zadd(key, {data: score})
pipeline.expire(key, 300)
pipeline.execute()
# function event_broadcast(data) {
# const now = Date.now();
# const now_ms = now;
# const now_10s = Math.floor(now / 10000);
#
# const key = redis_prefix + now_10s;
# const score = now_ms;
#
# const pipeline = redis_conn.pipeline();
# pipeline.zadd(key, score, data);
# pipeline.expire(key, 300);
# pipeline.exec();
# }
last_score = 0
def event_fetch():
global last_score
now = time.time()
now_ms = int(now * 1000)
now_10s = int(now) // 10
keys = [
redis_prefix + str(now_10s - 2),
redis_prefix + str(now_10s - 1),
redis_prefix + str(now_10s),
]
pipeline = redis_conn.pipeline()
for key in keys:
logging.info('%s %20s %20s', key, last_score, now_ms)
pipeline.zrangebyscore(key, last_score, now_ms, withscores=True)
results = pipeline.execute()
for data_list in results:
for data, _ in data_list:
handle_broadcast(data.decode('utf-8'))
last_score = now_ms
def broadcast_loop():
i = 0
while True:
i += 1
data = f'广播请求数据 {i}'
event_broadcast(data)
logging.info(f'广播请求:{data}')
time.sleep(0.5)
def main():
broadcast_thread = threading.Thread(target=broadcast_loop, daemon=True)
broadcast_thread.start()
while True:
event_fetch()
time.sleep(5)
main()
Golang
2023-04-27
看到微信公众号 Go语言教程 的文章《golang 实现简单网关》才知道还有 httputil.ReverseProxy 这么个东西。
PS:这玩意儿有什么必要放在标准库?
还挺有趣,在作者示例的基础之上完善了一下,实现多服务,多后端节点的一个负载均衡(还可以再补上权重)。
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
func main() {
addr := "127.0.0.1:2002"
backends := map[string][]string{
"service1": {"http://127.0.0.1:2003", "http://127.0.0.1:2004"},
}
reversePorxy := NewReverseProxy(backends)
log.Println("Starting httpserver at " + addr)
log.Fatal(http.ListenAndServe(addr, reversePorxy))
}
func reqConvert(req *http.Request, target *url.URL) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
req.Header.Set("X-Real-Ip", strings.Split(req.RemoteAddr, ":")[0])
}
func NewReverseProxy(backends map[string][]string) *httputil.ReverseProxy {
var targets = make(map[string][]*url.URL)
for srv, nodes := range backends {
for _, nodeUrl := range nodes {
target, _ := url.Parse(nodeUrl)
targets[srv] = append(targets[srv], target)
}
}
director := func(req *http.Request) {
segments := strings.SplitN(req.URL.Path, "/", 3)
if len(segments) != 3 {
return
}
srv := segments[1]
req.URL.Path = segments[2]
if _, ok := targets[srv]; !ok {
log.Printf("unknown path: %s", req.URL.Path)
return
}
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(targets[srv]))
target := targets[srv][randomIndex]
reqConvert(req, target)
}
modifyFunc := func(res *http.Response) error {
if res.StatusCode != http.StatusOK {
oldPayLoad, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
newPayLoad := []byte("hello " + string(oldPayLoad))
res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad))
res.ContentLength = int64(len(newPayLoad))
res.Header.Set("Content-Length", fmt.Sprint(len(newPayLoad)))
}
return nil
}
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
Git
2023-04-26
开源中国文章 紧跟 AI 步伐, Gitee 已支持 AI 模型托管 中介绍了 Git LFS 的使用。
结合 git lfs help,记录如下:
- 需要先 install 一下,启用 LFS 功能
$ git lfs install
Updated Git hooks.
Git LFS initialized.
- 追踪大文件
git lfs trace path/to/largefile
# 会记录在 .gitattributes 文件中
# path/to/largefile filter=lfs diff=lfs merge=lfs -text
- 推送大文件
# 正常提交
git add .gitattributes path/to/largefile
git commit -m "..."
# 推送
git lfs push --all
git lfs push --all origin
- 拉取大文件
git lfs pull --all
# 指定文件
git lfs pull -I <filepath>
git lfs ls-files 列出大文件
i18n
2023-04-24
做开发有时会需要系统支持在多个地区提供支持,主要是语言,其实还应该包括时区,货币单位等其他信息。
这一套动作叫做国际化和本地化。
国际化是指不要把一些东西写死,是指能够简单灵活改造适应不同地区的需求。
而本地化是指,将这套国际化的产品,针对不同区域做一个适配。
也就是说国际化和本地化是相辅相成的,是一套组合拳。
国际化的英语单词是 internationalization,缩写成 i18n(i 和 n 之间有 18 个字母)。
本地话的英语单词是 localization,缩写成 l10n(l 和 n 之间有 10 个字母)。
明明是一件事,有些地方叫国际化,有些地方写本地化,混乱!我更喜欢全球化 Globalization 这个词。
在如微软及IBM等企业中,则会使用全球化(globalization)来表示此两者的合称。
在英文中,也会使用g11n做为简称。
也有使用缩写 GILT (globalization、internationalization、localization和translation),即“全球化、国际化、本地化和翻译”。
相关工作内容(继续参考维基百科):
- 语言
- 电子文件
- 字母。目前大部分的系统都采用Unicode为标准来解决字符编码。
- 不同的数字命名系统。
- 书写方向。譬如德语、英语是从左到右,而波斯语、希伯来语和阿拉伯语是由右到左。
- 相同语言在不同地区的拼法差异,如美国英语、加拿大英语使用localization,而英国英语和澳洲英语使用localisation。
- 文件处理上的差异,如某些文字存在大小写,其它则否。字母顺序。
- 文字的图像表示(打印物、内含在线图片)。
- 读法(音频)
- 视频的字幕
- 文化
- 图片和颜色:这牵涉到理解和文化适宜的议题。
- 名字和称谓
- 政府给定的编码(如美国的社会安全码,英国的National Insurance number,爱沙尼亚的Isikukood及其它各国的身份证号码)和护照
- 电话号码、地址和国际邮递区号
- 货币(符号、货币标志的位置)
- 度量衡
- 纸张大小
- 书写习惯
- 日期跟时间的格式,包含各式日历。
- 时区(在国际场合会使用世界标准时间)
- 数字格式(小数点、分隔点的位置、分隔所用的字符)
- 产品和服务所要面向的法规
只属于本地化的主题有:
- 翻译
- 针对特定语言(如东亚语言)的特别支持
- 符合当地习惯
- 符合当地的道德观念
- 针对当地撰写内容
- 符号
- 排序方法
- 美学
- 当地的文化价值和社会环境
我们绝大多数场景应该只能关注到上面标粗的三个点:翻译,货币,合规。
参考资料与拓展阅读