feat: 添加微服务模板基础架构

- 创建基于 CloudWego Hertz 的 Go 微服务脚手架
- 集成 Nacos 服务注册/发现功能
- 添加 gRPC 客户端支持
- 实现环境变量配置管理 (.env.example)
- 添加 HTTP 中间件 (Recovery, AccessLog, CORS)
- 配置 Gitea CI/CD 构建部署流程

BREAKING CHANGE: 项目结构调整,从简单的 API 服务升级为完整的微服务架构
This commit is contained in:
shiran
2026-04-15 11:13:38 +08:00
parent 8654cd6e5c
commit 6050d11f27
30 changed files with 1643 additions and 358 deletions
+132
View File
@@ -0,0 +1,132 @@
package httplog
import (
"encoding/json"
"mime"
"strings"
"github.com/cloudwego/hertz/pkg/app"
)
var sensitiveKeys = []string{"password", "secret", "token", "key", "passwd"}
const redactedValue = "[REDACTED]"
// desensitization:只对第一层 key 做脱敏,不递归
func desensitization(data map[string]any) ([]byte, error) {
out := make(map[string]any, len(data))
for k, v := range data {
if isSensitiveKey(k) {
out[k] = redactedValue
} else {
out[k] = v
}
}
return json.Marshal(out)
}
// 大小写不敏感、包含式匹配
func isSensitiveKey(key string) bool {
k := strings.ToLower(key)
for _, s := range sensitiveKeys {
if strings.Contains(k, s) {
return true
}
}
return false
}
// FormBodyToJSONWithFilePlaceholder
// - 普通表单:完整转 JSON
// - multipart 文件字段:不取任何文件信息,只输出 "[file]" 或 ["[file]", ...]
func FormBodyToJSONWithFilePlaceholder(c *app.RequestContext) (jsonBytes []byte, ok bool, err error) {
ct := string(c.Request.Header.ContentType())
mediaType, _, _ := mime.ParseMediaType(ct)
mediaType = strings.ToLower(mediaType)
out := make(map[string]any)
switch mediaType {
case "application/x-www-form-urlencoded":
args := c.PostArgs()
args.VisitAll(func(k, v []byte) {
key := string(k)
// 同名 key 多次出现时,转成数组
if old, exists := out[key]; exists {
switch x := old.(type) {
case string:
out[key] = []string{x, string(v)}
case []string:
out[key] = append(x, string(v))
default:
out[key] = string(v)
}
} else {
out[key] = string(v)
}
})
b, e := desensitization(out)
return b, true, e
case "multipart/form-data":
form, e := c.MultipartForm()
if e != nil {
return nil, false, e
}
// 普通字段
for k, vv := range form.Value {
if len(vv) == 1 {
out[k] = vv[0]
} else {
out[k] = vv
}
}
// 文件字段:只做占位,不读取任何文件信息
for k, files := range form.File {
if len(files) <= 1 {
out[k] = "[file]"
} else {
arr := make([]string, 0, len(files))
for range files {
arr = append(arr, "[file]")
}
out[k] = arr
}
}
b, e2 := desensitization(out)
return b, true, e2
default:
// 不是表单就不处理
return nil, false, nil
}
}
// ResponseBodySnippet :获取响应内容
func ResponseBodySnippet(c *app.RequestContext, maxBytes int) (body string, ok bool, truncated bool) {
b := c.Response.Body()
if len(b) == 0 {
return "", false, false
}
// 只采集“看起来像文本/JSON”的响应,其他(图片/zip等)直接跳过
ct := strings.ToLower(string(c.Response.Header.ContentType()))
if ct != "" && !(strings.HasPrefix(ct, "text/") ||
strings.Contains(ct, "application/json") ||
strings.Contains(ct, "application/xml") ||
strings.Contains(ct, "application/javascript")) {
return "", false, false
}
if maxBytes > 0 && len(b) > maxBytes {
b = b[:maxBytes]
truncated = true
}
body = string(b)
return body, true, truncated
}