diff --git a/.gitignore b/.gitignore index 18646b6..f943a49 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ manifest/output/ temp/ temp.yaml bin -**/config/config.yaml \ No newline at end of file +**/config/config.yaml + +# WorkBuddy 本地记忆(不入库,跨账号上下文见 AGENTS.md 与 docs/change-log/) +.workbuddy/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9c98f2b --- /dev/null +++ b/AGENTS.md @@ -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 元数据注释。 diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index cbee06a..cf942b9 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -5,6 +5,9 @@ service.xpcool.com/ ├── api/ # HTTP contracts and Swagger metadata │ ├── user/v1/ # /api/v1 - client-facing 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/ │ ├── cmd/ # application bootstrap and route isolation │ ├── consts/ # application error codes and constants @@ -18,6 +21,7 @@ service.xpcool.com/ │ │ └── vo/ # API view models │ ├── middleware/ # configurable route-group middleware │ └── library/ # JWT, response, pagination primitives +├── docs/change-log/ # per-request change records (agent context) ├── manifest/ │ ├── config/ # config.dev/test/prod.yaml │ └── 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. + +See `AGENTS.md` for the agent-facing conventions and the context-memory system. diff --git a/common/doc.go b/common/doc.go new file mode 100644 index 0000000..1bfcbf5 --- /dev/null +++ b/common/doc.go @@ -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 diff --git a/common/tools/convertx/convertx.go b/common/tools/convertx/convertx.go new file mode 100644 index 0000000..8b463c4 --- /dev/null +++ b/common/tools/convertx/convertx.go @@ -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 +} diff --git a/common/tools/cryptox/cryptox.go b/common/tools/cryptox/cryptox.go new file mode 100644 index 0000000..8045c1e --- /dev/null +++ b/common/tools/cryptox/cryptox.go @@ -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 +} diff --git a/common/tools/doc.go b/common/tools/doc.go new file mode 100644 index 0000000..9768911 --- /dev/null +++ b/common/tools/doc.go @@ -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 diff --git a/common/tools/filex/filex.go b/common/tools/filex/filex.go new file mode 100644 index 0000000..4853141 --- /dev/null +++ b/common/tools/filex/filex.go @@ -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) +} diff --git a/common/tools/ip/ip.go b/common/tools/ip/ip.go new file mode 100644 index 0000000..bb8913f --- /dev/null +++ b/common/tools/ip/ip.go @@ -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)) +} diff --git a/common/tools/md5/md5.go b/common/tools/md5/md5.go new file mode 100644 index 0000000..d7d0f93 --- /dev/null +++ b/common/tools/md5/md5.go @@ -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) +} diff --git a/common/tools/random/random.go b/common/tools/random/random.go new file mode 100644 index 0000000..330bff6 --- /dev/null +++ b/common/tools/random/random.go @@ -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) +} diff --git a/common/tools/slicex/slicex.go b/common/tools/slicex/slicex.go new file mode 100644 index 0000000..b71f312 --- /dev/null +++ b/common/tools/slicex/slicex.go @@ -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 +} diff --git a/common/tools/strx/strx.go b/common/tools/strx/strx.go new file mode 100644 index 0000000..5c51868 --- /dev/null +++ b/common/tools/strx/strx.go @@ -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) +} diff --git a/common/tools/timex/timex.go b/common/tools/timex/timex.go new file mode 100644 index 0000000..d21af18 --- /dev/null +++ b/common/tools/timex/timex.go @@ -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") +} diff --git a/common/tools/uuid/uuid.go b/common/tools/uuid/uuid.go new file mode 100644 index 0000000..16e33d7 --- /dev/null +++ b/common/tools/uuid/uuid.go @@ -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) +} diff --git a/docs/change-log/2026-08-24.md b/docs/change-log/2026-08-24.md new file mode 100644 index 0000000..3a058f4 --- /dev/null +++ b/docs/change-log/2026-08-24.md @@ -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。