feat(common): 新增公共模块 common/tools 及项目上下文记忆体系
- common/tools 工具模块,10 个子功能:md5/cryptox/uuid/random/timex/convertx/strx/slicex/ip/filex(薄封装 GoFrame 内置组件) - AGENTS.md 项目智能体说明书(架构、规范、命令、记忆体系索引) - docs/change-log/2026-08-24.md 本次请求与变更记录 - .gitignore 排除 .workbuddy/;PROJECT_STRUCTURE.md 补充目录树
This commit is contained in:
parent
54135d2be5
commit
8d5f2c65ae
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,3 +17,6 @@ temp/
|
|||||||
temp.yaml
|
temp.yaml
|
||||||
bin
|
bin
|
||||||
**/config/config.yaml
|
**/config/config.yaml
|
||||||
|
|
||||||
|
# WorkBuddy 本地记忆(不入库,跨账号上下文见 AGENTS.md 与 docs/change-log/)
|
||||||
|
.workbuddy/
|
||||||
89
AGENTS.md
Normal file
89
AGENTS.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# AGENTS.md — 项目智能体说明书
|
||||||
|
|
||||||
|
> 本文件是给所有 AI 编程助手(CodeBuddy / WorkBuddy / Claude Code / Codex / Cursor 等)看的项目级上下文。
|
||||||
|
> 任何账号 clone 本仓库后,助手都应先读本文件与 `docs/change-log/` 下最近的记录,即可无缝衔接。
|
||||||
|
> 请保持本文件**长期稳定**:只写「架构、规范、约定」,不要写一次性事项。
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
`service.xpcool.com`:个人多客户端(mini / h5 / app)后端服务,GoFrame v2 单体应用。
|
||||||
|
核心业务:用户认证(JWT)、内容、收藏、站内消息;后台管理(RBAC + 操作审计)。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 语言 | Go 1.23.0 |
|
||||||
|
| 框架 | github.com/gogf/gf/v2 v2.10.2 |
|
||||||
|
| 数据库 | MySQL(ORM 由 gf 生成 dao/do/entity) |
|
||||||
|
| 认证 | JWT(`internal/library/jwt`),admin 另有 `X-Permission` 校验 |
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
service.xpcool.com/
|
||||||
|
├── api/ # HTTP 契约与 Swagger 元数据(api/user/v1, api/admin/v1)
|
||||||
|
├── common/ # 公共可复用模块(不依赖 internal,可独立抽取成库)
|
||||||
|
│ └── tools/ # 工具模块,按子功能分包(见下文)
|
||||||
|
├── internal/
|
||||||
|
│ ├── cmd/ # 启动引导
|
||||||
|
│ ├── consts/ # 错误码与常量
|
||||||
|
│ ├── controller/ # API→service 适配层(不写业务)
|
||||||
|
│ ├── service/ # 领域用例(业务逻辑直接写这里,不用 logic/)
|
||||||
|
│ ├── dao/ # gf gen dao 生成,禁止手改
|
||||||
|
│ ├── model/ # entity/ do/ dto/ vo/(entity、do 生成,禁止手改)
|
||||||
|
│ ├── middleware/ # 路由中间件
|
||||||
|
│ ├── library/ # jwt / page / response 等内部基础件
|
||||||
|
│ └── table/ # 表列名常量
|
||||||
|
├── manifest/ # config.dev/test/prod.yaml、sql 迁移
|
||||||
|
├── docs/change-log/ # 每次请求与变更的记录(重要!见「上下文记忆」)
|
||||||
|
└── utility/ # (预留)跨切面辅助
|
||||||
|
```
|
||||||
|
|
||||||
|
## 公共工具模块 common/tools
|
||||||
|
|
||||||
|
- 规则:**不依赖 internal/**,只薄封装 GoFrame 内置组件;新工具优先复用内置(gmd5/gaes/gdes/guid/grand/gtime/gconv/gstr/gfile...),避免重复造轮子。
|
||||||
|
- 子功能:`md5`、`cryptox`(AES/DES)、`uuid`、`random`、`timex`、`convertx`、`strx`、`slicex`、`ip`、`filex`。
|
||||||
|
- 新增子功能:在 `common/tools/` 下建子包,更新 `common/tools/doc.go` 的布局清单。
|
||||||
|
|
||||||
|
## 分层与调用规范(必须遵守)
|
||||||
|
|
||||||
|
1. 调用链:`controller → service → dao → model(do)`;controller 不碰 dao。
|
||||||
|
2. DTO/VO 边界:跨层出入参走 `internal/model/dto` 与 `internal/model/vo`,API 类型与 entity 不得越界。
|
||||||
|
3. **数据库操作必须用 DO 对象**(`internal/model/do`),禁止 `g.Map`;未赋值字段保持 nil 自动忽略:
|
||||||
|
```go
|
||||||
|
dao.Users.Ctx(ctx).Where(cols.Id, id).Data(do.User{Uid: uid}).Update()
|
||||||
|
```
|
||||||
|
4. **时间字段自动维护**:`created_at/updated_at/deleted_at` 由 ORM 自动处理,禁止手动赋值;软删除用 `Delete()`,禁止手写 `WhereNull(cols.DeletedAt)`。
|
||||||
|
5. **错误处理一律用 gerror**(保留堆栈);响应统一走 `internal/library/response`。
|
||||||
|
6. 生成代码(dao/do/entity)**禁止手改**,改表后跑 `gf gen dao` 重新生成。
|
||||||
|
7. 声明 ≥3 个相关变量时,用 `var (...)` 块对齐。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行(dev)
|
||||||
|
GF_GCFG_FILE=config.dev.yaml DB_DSN="user:pass@tcp(127.0.0.1:3306)/db?loc=Local" JWT_SECRET=xxx go run main.go
|
||||||
|
# 数据库模型生成(唯一来源)
|
||||||
|
gf gen dao -p internal -g default -gt -c
|
||||||
|
# 构建 / 测试
|
||||||
|
go build ./...
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 上下文记忆(重要)
|
||||||
|
|
||||||
|
三层配合,保证「换个账号/换台机器也能无缝衔接」:
|
||||||
|
|
||||||
|
1. **本文件(AGENTS.md)**:长期稳定的架构与规范。
|
||||||
|
2. **`docs/change-log/YYYY-MM-DD.md`**:每次对话的「请求 + 变更 + 决策」记录,随 git 提交。
|
||||||
|
- 每次完成任务后,若 `docs/change-log/` 已有当日文件则**追加**,否则新建;
|
||||||
|
- 格式固定:`## 请求` / `## 变更`(含文件清单)/ `## 决策与理由` / `## 待办与风险`;
|
||||||
|
- 助手开工前先读最近 1-2 篇,快速恢复上下文。
|
||||||
|
3. **`.workbuddy/memory/`**:WorkBuddy 桌面端本机记忆(每日日志 + MEMORY.md),**已加入 .gitignore,不入库**,仅本机增强。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 配置文件按 `GF_GCFG_FILE` 切换;`manifest/config/config.yaml` 不入库。
|
||||||
|
- 生产环境强密码:`JWT_SECRET`、数据库口令。
|
||||||
|
- 变更涉及 API 时同步更新 `api/` 下的 Swagger 元数据注释。
|
||||||
@ -5,6 +5,9 @@ service.xpcool.com/
|
|||||||
├── api/ # HTTP contracts and Swagger metadata
|
├── api/ # HTTP contracts and Swagger metadata
|
||||||
│ ├── user/v1/ # /api/v1 - client-facing API
|
│ ├── user/v1/ # /api/v1 - client-facing API
|
||||||
│ └── admin/v1/ # /admin/v1 - administration API
|
│ └── admin/v1/ # /admin/v1 - administration API
|
||||||
|
├── common/ # public reusable module (no internal/ deps)
|
||||||
|
│ └── tools/ # utility toolbox: md5, cryptox, uuid, random,
|
||||||
|
│ # timex, convertx, strx, slicex, ip, filex
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── cmd/ # application bootstrap and route isolation
|
│ ├── cmd/ # application bootstrap and route isolation
|
||||||
│ ├── consts/ # application error codes and constants
|
│ ├── consts/ # application error codes and constants
|
||||||
@ -18,6 +21,7 @@ service.xpcool.com/
|
|||||||
│ │ └── vo/ # API view models
|
│ │ └── vo/ # API view models
|
||||||
│ ├── middleware/ # configurable route-group middleware
|
│ ├── middleware/ # configurable route-group middleware
|
||||||
│ └── library/ # JWT, response, pagination primitives
|
│ └── library/ # JWT, response, pagination primitives
|
||||||
|
├── docs/change-log/ # per-request change records (agent context)
|
||||||
├── manifest/
|
├── manifest/
|
||||||
│ ├── config/ # config.dev/test/prod.yaml
|
│ ├── config/ # config.dev/test/prod.yaml
|
||||||
│ └── sql/ # ordered MySQL migrations
|
│ └── sql/ # ordered MySQL migrations
|
||||||
@ -25,3 +29,5 @@ service.xpcool.com/
|
|||||||
```
|
```
|
||||||
|
|
||||||
`dao`, `model/do` and `model/entity` are generated after migration, so their schema never drifts from MySQL. Controllers do not access DAO; only services do.
|
`dao`, `model/do` and `model/entity` are generated after migration, so their schema never drifts from MySQL. Controllers do not access DAO; only services do.
|
||||||
|
|
||||||
|
See `AGENTS.md` for the agent-facing conventions and the context-memory system.
|
||||||
|
|||||||
10
common/doc.go
Normal file
10
common/doc.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Package common is the public, reusable module of service.xpcool.com.
|
||||||
|
//
|
||||||
|
// It contains only code that is independent of internal/ — anything here can
|
||||||
|
// be shared across services inside the repository, or extracted into a
|
||||||
|
// standalone library later without touching business code.
|
||||||
|
//
|
||||||
|
// Current layout:
|
||||||
|
//
|
||||||
|
// common/tools shared utility toolbox (md5, cryptox, uuid, ...)
|
||||||
|
package common
|
||||||
64
common/tools/convertx/convertx.go
Normal file
64
common/tools/convertx/convertx.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Package convertx provides type conversion helpers with default-value
|
||||||
|
// fallback, built on top of gconv.
|
||||||
|
//
|
||||||
|
// Note: gconv silently returns the zero value when conversion fails, so the
|
||||||
|
// helpers here only fall back to the default for nil / blank-string inputs.
|
||||||
|
// Pass pre-validated data when strict conversion is required.
|
||||||
|
package convertx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToInt converts v to int, returning def when v is nil or a blank string.
|
||||||
|
func ToInt(v any, def int) int {
|
||||||
|
if isEmpty(v) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return gconv.Int(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInt64 converts v to int64, returning def when v is nil or a blank string.
|
||||||
|
func ToInt64(v any, def int64) int64 {
|
||||||
|
if isEmpty(v) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return gconv.Int64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat64 converts v to float64, returning def when v is nil or a blank string.
|
||||||
|
func ToFloat64(v any, def float64) float64 {
|
||||||
|
if isEmpty(v) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return gconv.Float64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString converts v to string, returning def when v is nil.
|
||||||
|
func ToString(v any, def string) string {
|
||||||
|
if v == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return gconv.String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBool converts v to bool, returning def when v is nil or a blank string.
|
||||||
|
func ToBool(v any, def bool) bool {
|
||||||
|
if isEmpty(v) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return gconv.Bool(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isEmpty reports whether v is nil or a blank string.
|
||||||
|
func isEmpty(v any) bool {
|
||||||
|
if v == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
return strings.TrimSpace(s) == ""
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
80
common/tools/cryptox/cryptox.go
Normal file
80
common/tools/cryptox/cryptox.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Package cryptox provides AES/DES encryption helpers with base64 output,
|
||||||
|
// built on top of gaes and gdes.
|
||||||
|
//
|
||||||
|
// Keys: any-length secrets are accepted — they are normalized to the exact
|
||||||
|
// key size internally, so callers do not need to manage key lengths.
|
||||||
|
package cryptox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/crypto/gaes"
|
||||||
|
"github.com/gogf/gf/v2/crypto/gdes"
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
aesKeySize = 16 // AES-128 key size in bytes
|
||||||
|
desKeySize = 8 // DES key size in bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
// normalizeKey derives a fixed-size key from an arbitrary-length secret.
|
||||||
|
func normalizeKey(secret string, size int) []byte {
|
||||||
|
key := []byte(secret)
|
||||||
|
if len(key) == size {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
sum := md5.Sum([]byte(secret))
|
||||||
|
out := make([]byte, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
out[i] = sum[i%len(sum)]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// AesEncrypt encrypts plainText with AES-128-CBC using a key derived from
|
||||||
|
// secret, and returns the ciphertext encoded in base64.
|
||||||
|
func AesEncrypt(plainText, secret string) (string, error) {
|
||||||
|
out, err := gaes.Encrypt([]byte(plainText), normalizeKey(secret, aesKeySize))
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `AesEncrypt failed`)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AesDecrypt decrypts the base64-encoded cipherText produced by AesEncrypt.
|
||||||
|
func AesDecrypt(cipherText, secret string) (string, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `base64 decode failed`)
|
||||||
|
}
|
||||||
|
out, err := gaes.Decrypt(data, normalizeKey(secret, aesKeySize))
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `AesDecrypt failed`)
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DesEncrypt encrypts plainText with DES-ECB (PKCS5 padding) using a key
|
||||||
|
// derived from secret, and returns the ciphertext encoded in base64.
|
||||||
|
func DesEncrypt(plainText, secret string) (string, error) {
|
||||||
|
out, err := gdes.EncryptECB([]byte(plainText), normalizeKey(secret, desKeySize), gdes.PKCS5PADDING)
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `DesEncrypt failed`)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DesDecrypt decrypts the base64-encoded cipherText produced by DesEncrypt.
|
||||||
|
func DesDecrypt(cipherText, secret string) (string, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `base64 decode failed`)
|
||||||
|
}
|
||||||
|
out, err := gdes.DecryptECB(data, normalizeKey(secret, desKeySize), gdes.PKCS5PADDING)
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `DesDecrypt failed`)
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
24
common/tools/doc.go
Normal file
24
common/tools/doc.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Package tools is the shared utility toolbox of the project.
|
||||||
|
//
|
||||||
|
// Every sub-package is a thin, documented wrapper around GoFrame built-in
|
||||||
|
// components (gmd5, gaes, gdes, guid, grand, gtime, gconv, gstr, gfile, ...),
|
||||||
|
// so it stays small and consistent with the framework.
|
||||||
|
//
|
||||||
|
// Layout:
|
||||||
|
//
|
||||||
|
// common/tools/md5 MD5 digest helpers
|
||||||
|
// common/tools/cryptox AES/DES encryption with base64 output
|
||||||
|
// common/tools/uuid unique ID generation
|
||||||
|
// common/tools/random random numbers and strings
|
||||||
|
// common/tools/timex time formatting and computation
|
||||||
|
// common/tools/convertx type conversion with default fallback
|
||||||
|
// common/tools/strx string / naming / masking helpers
|
||||||
|
// common/tools/slicex generic slice utilities
|
||||||
|
// common/tools/ip IP address helpers
|
||||||
|
// common/tools/filex file system helpers
|
||||||
|
//
|
||||||
|
// Rules:
|
||||||
|
// - Never depend on internal/ — this module must stay self-contained.
|
||||||
|
// - Prefer reusing GoFrame built-in components over re-implementing.
|
||||||
|
// - Keep each helper small and add a Chinese doc comment.
|
||||||
|
package tools
|
||||||
28
common/tools/filex/filex.go
Normal file
28
common/tools/filex/filex.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Package filex provides common file system helpers on top of gfile.
|
||||||
|
package filex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/os/gfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exists reports whether the file or directory at path exists.
|
||||||
|
func Exists(path string) bool {
|
||||||
|
return gfile.Exists(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir reports whether path is a directory.
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
return gfile.IsDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadString returns the full content of the file at path as a string.
|
||||||
|
// Returns an empty string when the file does not exist.
|
||||||
|
func ReadString(path string) string {
|
||||||
|
return gfile.GetContents(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString writes content to the file at path, creating intermediate
|
||||||
|
// directories if needed.
|
||||||
|
func WriteString(path, content string) error {
|
||||||
|
return gfile.PutContents(path, content)
|
||||||
|
}
|
||||||
59
common/tools/ip/ip.go
Normal file
59
common/tools/ip/ip.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Package ip provides IP address helpers.
|
||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/net/gipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid reports whether s is a valid IPv4 address.
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
return gipv4.Validate(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalIP returns the first non-loopback IPv4 address of this host.
|
||||||
|
func LocalIP() (string, error) {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return "", gerror.Wrap(err, `net.InterfaceAddrs failed`)
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||||
|
if ipv4 := ipNet.IP.To4(); ipv4 != nil && !ipv4.IsLoopback() {
|
||||||
|
return ipv4.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInternal reports whether s is a private/internal IPv4 address
|
||||||
|
// (private ranges, loopback or link-local).
|
||||||
|
func IsInternal(s string) bool {
|
||||||
|
parsed := net.ParseIP(s)
|
||||||
|
if parsed == nil || parsed.To4() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return parsed.IsPrivate() || parsed.IsLoopback() || parsed.IsLinkLocalUnicast()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLong converts an IPv4 string to its uint32 representation
|
||||||
|
// (big-endian, same as inet_aton).
|
||||||
|
func ToLong(s string) (uint32, error) {
|
||||||
|
ipv4 := net.ParseIP(s).To4()
|
||||||
|
if ipv4 == nil {
|
||||||
|
return 0, gerror.Newf(`invalid IPv4 address: %s`, s)
|
||||||
|
}
|
||||||
|
return uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString converts a uint32 IPv4 value to its dotted-decimal string.
|
||||||
|
func ToString(v uint32) string {
|
||||||
|
return strconv.Itoa(int(v>>24)) + "." +
|
||||||
|
strconv.Itoa(int(v>>16&0xFF)) + "." +
|
||||||
|
strconv.Itoa(int(v>>8&0xFF)) + "." +
|
||||||
|
strconv.Itoa(int(v&0xFF))
|
||||||
|
}
|
||||||
23
common/tools/md5/md5.go
Normal file
23
common/tools/md5/md5.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Package md5 provides MD5 digest helpers.
|
||||||
|
package md5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Md5Hex returns the MD5 digest of s as a lowercase hex string.
|
||||||
|
// The underlying error is ignored because it never fails for in-memory input.
|
||||||
|
func Md5Hex(s string) string {
|
||||||
|
h, _ := gmd5.EncryptString(s)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Md5Bytes returns the MD5 digest of data as a lowercase hex string.
|
||||||
|
func Md5Bytes(data []byte) (string, error) {
|
||||||
|
return gmd5.Encrypt(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Md5File returns the MD5 digest of the local file at path as a hex string.
|
||||||
|
func Md5File(path string) (string, error) {
|
||||||
|
return gmd5.EncryptFile(path)
|
||||||
|
}
|
||||||
26
common/tools/random/random.go
Normal file
26
common/tools/random/random.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Package random provides random number and string generation helpers.
|
||||||
|
package random
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Int returns a random integer in [min, max].
|
||||||
|
func Int(min, max int) int {
|
||||||
|
return grand.N(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a random alphanumeric string of length n.
|
||||||
|
func String(n int) string {
|
||||||
|
return grand.S(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digits returns a random numeric-only string of length n, e.g. SMS codes.
|
||||||
|
func Digits(n int) string {
|
||||||
|
return grand.Digits(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letters returns a random letter-only string of length n.
|
||||||
|
func Letters(n int) string {
|
||||||
|
return grand.Letters(n)
|
||||||
|
}
|
||||||
64
common/tools/slicex/slicex.go
Normal file
64
common/tools/slicex/slicex.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Package slicex provides generic slice utilities built on the standard
|
||||||
|
// library (Go 1.23+).
|
||||||
|
package slicex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains reports whether v is present in items.
|
||||||
|
func Contains[T comparable](items []T, v T) bool {
|
||||||
|
return slices.Contains(items, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unique returns items with duplicates removed, preserving first-seen order.
|
||||||
|
func Unique[T comparable](items []T) []T {
|
||||||
|
seen := make(map[T]struct{}, len(items))
|
||||||
|
out := make([]T, 0, len(items))
|
||||||
|
for _, v := range items {
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk splits items into sub-slices of at most size elements.
|
||||||
|
// Returns nil when size <= 0 or items is empty.
|
||||||
|
func Chunk[T any](items []T, size int) [][]T {
|
||||||
|
if size <= 0 || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([][]T, 0, (len(items)+size-1)/size)
|
||||||
|
for len(items) > 0 {
|
||||||
|
n := size
|
||||||
|
if len(items) < n {
|
||||||
|
n = len(items)
|
||||||
|
}
|
||||||
|
out = append(out, items[:n])
|
||||||
|
items = items[n:]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies fn to every element and returns the results.
|
||||||
|
func Map[T, R any](items []T, fn func(T) R) []R {
|
||||||
|
out := make([]R, len(items))
|
||||||
|
for i, v := range items {
|
||||||
|
out[i] = fn(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter returns the elements for which fn returns true, preserving order.
|
||||||
|
func Filter[T any](items []T, fn func(T) bool) []T {
|
||||||
|
out := make([]T, 0, len(items))
|
||||||
|
for _, v := range items {
|
||||||
|
if fn(v) {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
57
common/tools/strx/strx.go
Normal file
57
common/tools/strx/strx.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Package strx provides string helpers on top of gstr, including naming
|
||||||
|
// conversion and sensitive-data masking.
|
||||||
|
package strx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/text/gstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SnakeCase converts s to snake_case, e.g. "UserName" -> "user_name".
|
||||||
|
func SnakeCase(s string) string {
|
||||||
|
return gstr.CaseSnake(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CamelCase converts s to CamelCase, e.g. "user_name" -> "UserName".
|
||||||
|
func CamelCase(s string) string {
|
||||||
|
return gstr.CaseCamel(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LowerCamelCase converts s to lowerCamelCase, e.g. "user_name" -> "userName".
|
||||||
|
func LowerCamelCase(s string) string {
|
||||||
|
return gstr.CaseCamelLower(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty reports whether s is empty or whitespace-only.
|
||||||
|
func IsEmpty(s string) bool {
|
||||||
|
return strings.TrimSpace(s) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskPhone masks a phone number, keeping the first 3 and last 4 characters.
|
||||||
|
// e.g. "13812345678" -> "138****5678".
|
||||||
|
func MaskPhone(s string) string {
|
||||||
|
if len(s) < 7 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:3] + "****" + s[len(s)-4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskIDCard masks a Chinese ID card number, keeping the first 6 and last 4
|
||||||
|
// characters. e.g. "110101199003074512" -> "110101********4512".
|
||||||
|
func MaskIDCard(s string) string {
|
||||||
|
if len(s) < 10 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:6] + "********" + s[len(s)-4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskName masks a Chinese name, keeping only the first character.
|
||||||
|
// e.g. "张三丰" -> "张**".
|
||||||
|
func MaskName(s string) string {
|
||||||
|
r := []rune(s)
|
||||||
|
if len(r) <= 1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return string(r[0]) + strings.Repeat("*", len(r)-1)
|
||||||
|
}
|
||||||
43
common/tools/timex/timex.go
Normal file
43
common/tools/timex/timex.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Package timex provides time formatting and computation helpers on top of gtime.
|
||||||
|
package timex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LayoutDateTime is the conventional datetime layout: 2006-01-02 15:04:05.
|
||||||
|
LayoutDateTime = "2006-01-02 15:04:05"
|
||||||
|
// LayoutDate is the conventional date layout: 2006-01-02.
|
||||||
|
LayoutDate = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now returns the current time.
|
||||||
|
func Now() *gtime.Time {
|
||||||
|
return gtime.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format returns t formatted with the given layout.
|
||||||
|
// When layout is empty, LayoutDateTime is used.
|
||||||
|
func Format(t *gtime.Time, layout ...string) string {
|
||||||
|
ly := LayoutDateTime
|
||||||
|
if len(layout) > 0 && layout[0] != "" {
|
||||||
|
ly = layout[0]
|
||||||
|
}
|
||||||
|
return t.Format(ly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp returns the current Unix timestamp in seconds.
|
||||||
|
func Timestamp() int64 {
|
||||||
|
return gtime.Now().Timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartOfDay returns the beginning (00:00:00) of the day containing t.
|
||||||
|
func StartOfDay(t *gtime.Time) *gtime.Time {
|
||||||
|
return gtime.NewFromStr(t.Format(LayoutDate) + " 00:00:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndOfDay returns the end (23:59:59) of the day containing t.
|
||||||
|
func EndOfDay(t *gtime.Time) *gtime.Time {
|
||||||
|
return gtime.NewFromStr(t.Format(LayoutDate) + " 23:59:59")
|
||||||
|
}
|
||||||
21
common/tools/uuid/uuid.go
Normal file
21
common/tools/uuid/uuid.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Package uuid provides unique ID generation helpers.
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
|
"github.com/gogf/gf/v2/util/guid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a 32-character unique ID without dashes.
|
||||||
|
func New() string {
|
||||||
|
return guid.S()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short returns a random alphanumeric ID of length n (defaults to 8 when n <= 0).
|
||||||
|
// Suitable for short invite codes / trace ids.
|
||||||
|
func Short(n int) string {
|
||||||
|
if n <= 0 {
|
||||||
|
n = 8
|
||||||
|
}
|
||||||
|
return grand.S(n)
|
||||||
|
}
|
||||||
48
docs/change-log/2026-08-24.md
Normal file
48
docs/change-log/2026-08-24.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Change Log — 2026-08-24
|
||||||
|
|
||||||
|
## 请求
|
||||||
|
|
||||||
|
1. 新增一个「公共模块」,模块下存放 `tools` 功能模块,`tools` 下再分各种子功能。
|
||||||
|
2. 建立项目上下文记忆体系:记录每次请求与更改,方案对比后落地;要求后续使用其他 CodeBuddy 账号也能无缝衔接。
|
||||||
|
|
||||||
|
## 变更
|
||||||
|
|
||||||
|
新增文件:
|
||||||
|
|
||||||
|
- `common/doc.go` — 公共模块说明(不依赖 internal、可独立抽取)
|
||||||
|
- `common/tools/doc.go` — tools 模块布局清单与封装规则
|
||||||
|
- `common/tools/md5/md5.go` — MD5 摘要(Md5Hex / Md5Bytes / Md5File)
|
||||||
|
- `common/tools/cryptox/cryptox.go` — AES-128-CBC / DES-ECB 加解密(base64 输出,密钥任意长度自动规范化)
|
||||||
|
- `common/tools/uuid/uuid.go` — 唯一 ID(New 32 位 / Short 短随机码)
|
||||||
|
- `common/tools/random/random.go` — 随机数/随机串(Int / String / Digits / Letters)
|
||||||
|
- `common/tools/timex/timex.go` — 时间工具(Format / Timestamp / StartOfDay / EndOfDay)
|
||||||
|
- `common/tools/convertx/convertx.go` — 类型转换带默认值(ToInt / ToInt64 / ToFloat64 / ToString / ToBool)
|
||||||
|
- `common/tools/strx/strx.go` — 字符串工具(命名转换 SnakeCase/CamelCase + 脱敏 MaskPhone/MaskIDCard/MaskName)
|
||||||
|
- `common/tools/slicex/slicex.go` — 泛型切片工具(Contains / Unique / Chunk / Map / Filter)
|
||||||
|
- `common/tools/ip/ip.go` — IP 工具(IsValid / LocalIP / IsInternal / ToLong / ToString)
|
||||||
|
- `common/tools/filex/filex.go` — 文件工具(Exists / IsDir / ReadString / WriteString)
|
||||||
|
- `AGENTS.md` — 项目智能体说明书(架构、规范、命令、记忆体系索引)
|
||||||
|
- `docs/change-log/2026-08-24.md` — 本文档
|
||||||
|
|
||||||
|
修改文件:
|
||||||
|
|
||||||
|
- `.gitignore` — 追加 `.workbuddy/`(本机记忆不入库)
|
||||||
|
- `PROJECT_STRUCTURE.md` — 目录树补充 `common/` 与 `docs/change-log/`(见后续提交)
|
||||||
|
|
||||||
|
验证:`go build ./...` 与 `go vet ./common/...` 全部通过。
|
||||||
|
|
||||||
|
## 决策与理由
|
||||||
|
|
||||||
|
- **公共模块放顶层 `common/` 而非 `internal/common/`**:Go 的 internal 包无法被外部模块引用,放顶层便于未来抽取为独立库/被同仓库其他服务复用。
|
||||||
|
- **tools 一律薄封装 GoFrame 内置组件**:v2.10.2 中 AES/DES 已拆为 `crypto/gaes`、`crypto/gdes`,UUID 为 `util/guid`,无 `gslicer`(用标准库 `slices` 替代);避免重复造轮子,保持与框架一致。
|
||||||
|
- **记忆体系三层方案**(对比见下):
|
||||||
|
1. `AGENTS.md`(根目录)— 长期稳定规范,跨工具标准(Claude Code/CodeBuddy/Codex 等均识别),随 git 走;
|
||||||
|
2. `docs/change-log/YYYY-MM-DD.md` — 每次请求变更的结构化记录,随 git 走,**这是跨账号衔接的关键**;
|
||||||
|
3. `.workbuddy/memory/` — WorkBuddy 本机增强,不入库。
|
||||||
|
- 对比过 `CLAUDE.md`(Claude Code 专属、已建议统一为 AGENTS.md)、`.cursor/rules`(Cursor 专属)、`.codebuddy/`(仅 CodeBuddy 读取)——它们都不是最大公约数,故不采用。
|
||||||
|
|
||||||
|
## 待办与风险
|
||||||
|
|
||||||
|
- 后续每次任务完成后:更新 `docs/change-log/`(当日文件追加)+ 提交 git,确保其他账号 clone 即恢复上下文。
|
||||||
|
- `convertx` 依赖 gconv 的"转换失败返回零值"行为(无法区分"0"与非法输入),需要严格转换的场景应在 service 层先校验。
|
||||||
|
- 本次变更尚未 git 提交,建议尽快 commit。
|
||||||
Loading…
Reference in New Issue
Block a user