Explorar el Código

feat:1.密钥管理接口完成

wangkun hace 2 semanas
padre
commit
bb4a2cf635

+ 66 - 23
README.md

@@ -44,14 +44,14 @@
 # 下载依赖(首次执行会按 go.mod 自动获取 Go 1.25.10 工具链)
 go mod download
 
-# 启动服务,默认监听 0.0.0.0:8880
+# 启动服务,默认监听 0.0.0.0:9993
 go run udeserver.go -f etc/udeserver-api.yaml
 
 # 另开终端验证
-curl http://127.0.0.1:8880/api/v1/from/you
+curl http://127.0.0.1:9993/api/v1/from/you
 # {"message":"Hello you"}
 
-curl http://127.0.0.1:8880/api/v1/db/ping
+curl http://127.0.0.1:9993/api/v1/db/ping
 # {"ok":true,"version":"8.4.4","database":"ude"}
 ```
 
@@ -69,9 +69,12 @@ go build -o bin/udeserver.exe udeserver.go
 | 资源 | 新增 | 列表 | 详情 | 更新 | 删除 |
 | --- | --- | --- | --- | --- | --- |
 | 保护目录 `t_directory` | `POST /directories` | `GET /directories` | `GET /directories/:id` | `PUT /directories/:id` | `DELETE /directories/:id` |
-| 密钥 `t_key` | `POST /keys` | `GET /keys` | `GET /keys/:id` | `PUT /keys/:id` | `DELETE /keys/:id` |
+| 密钥 `t_key` | `POST /keys/create` | `POST /keys/list` | `POST /keys/detail` | `POST /keys/update` | `POST /keys/delete` |
 | 服务 `t_service` | `POST /services` | `GET /services` | `GET /services/:id` | `PUT /services/:id` | `DELETE /services/:id` |
 
+> 密钥接口已全部改为 `POST`,参数一律放请求体(`id` 也在 body 里,不再走路径)。
+> 目录、服务接口仍是 REST 风格。
+
 > 所有路由统一带 `/api/v1` 前缀,如 `GET /api/v1/keys/:id`。前缀在 `udeServer.api` 的 `@server(prefix: /api/v1)` 中配置,实际请求都需要加前缀;`/from/:name` 和 `/db/ping` 也不例外。
 
 ### 统一响应格式
@@ -100,51 +103,66 @@ go build -o bin/udeserver.exe udeserver.go
 
 ### 分页
 
-三个列表接口(目录/密钥/服务)统一支持分页,通过 query 参数传递:
-
-- `page`:页码,默认 `1`
-- `pageSize`:每页数量,默认 `10`,最大 `100`
-- 响应 `data`:`{"list":[...],"total":13}`,`total` 为总条数;超出范围时 `list` 为 `[]`
+列表接口统一支持分页,`page` 默认 `1`,`pageSize` 默认 `10`(最大 `100`),响应 `data` 为 `{"list":[...],"total":13}`(超出范围时 `list` 为 `[]`):
 
-示例:`GET /api/v1/keys?page=2&pageSize=5`
+- 目录、服务:query 参数,`GET /api/v1/directories?page=2&pageSize=5`
+- 密钥:请求体参数,`POST /api/v1/keys/list`,body 为 `{"page":2,"pageSize":5,"name":"搜索词","status":"1"}`;`name` 可选、为空则不过滤,用 `LIKE` 模糊匹配(大小写不敏感,`%`、`_`、`\` 已做转义);`status` 可选,`"1"` 启用 / `"2"` 停用,非法取值返回 `100001`
 
 服务接口示例:
 
 ```powershell
 # 新增服务:id 必填(t_service 主键没有 AUTO_INCREMENT)
-curl -X POST http://127.0.0.1:8880/api/v1/services `
+curl -X POST http://127.0.0.1:9993/api/v1/services `
   -H "Content-Type: application/json" `
   -d "{\"id\":1,\"name\":\"demo\",\"ip\":\"10.0.0.1\",\"port\":\"8080\",\"iden\":\"svc-1\",\"keyIden\":\"key-1\",\"keyAlgo\":\"AES\",\"status\":\"active\"}"
 
 # 列表 / 详情
-curl http://127.0.0.1:8880/api/v1/services
-curl http://127.0.0.1:8880/api/v1/services/1
+curl http://127.0.0.1:9993/api/v1/services
+curl http://127.0.0.1:9993/api/v1/services/1
 
 # 更新:id 放在路径中,请求体为完整字段
-curl -X PUT http://127.0.0.1:8880/api/v1/services/1 `
+curl -X PUT http://127.0.0.1:9993/api/v1/services/1 `
   -H "Content-Type: application/json" `
   -d "{\"name\":\"demo-updated\",\"ip\":\"10.0.0.2\",\"port\":\"9090\",\"iden\":\"svc-1\",\"keyIden\":\"key-1\",\"keyAlgo\":\"SM4\",\"status\":\"disabled\"}"
 
 # 删除
-curl -X DELETE http://127.0.0.1:8880/api/v1/services/1
+curl -X DELETE http://127.0.0.1:9993/api/v1/services/1
 ```
 
-密钥接口示例:
+密钥接口示例(全部为 `POST`,参数放请求体):
 
 ```powershell
-# 新增密钥:前端只需要传 name
-curl -X POST http://127.0.0.1:8880/api/v1/keys `
+# 新增密钥:前端只需要传 name;id 自增、iden/content 服务端生成、status 默认 1、createAt 由数据库生成
+curl -X POST http://127.0.0.1:9993/api/v1/keys/create `
   -H "Content-Type: application/json" `
   -d "{\"name\":\"demo-key\"}"
 
-# 返回:id 自增,iden/content 服务端生成,status 默认 1,createAt 由数据库生成
-# {"code":100000,"message":"操作成功","data":{"id":1,"iden":"4532bb4cefb944889d847020211ba7c7","name":"demo-key","content":"5e35...","status":"1","createAt":"2026-10-21T10:07:26+08:00"}}
+# 分页列表:page/pageSize/name/status 都是可选的;name 为模糊匹配(LIKE,大小写不敏感,%/_ 会自动转义)、status 过滤 "1" 启用 / "2" 停用(其他取值返回 100001)
+curl -X POST http://127.0.0.1:9993/api/v1/keys/list `
+  -H "Content-Type: application/json" `
+  -d "{\"page\":1,\"pageSize\":10,\"name\":\"fuzz\",\"status\":\"1\"}"
+
+# 详情
+curl -X POST http://127.0.0.1:9993/api/v1/keys/detail `
+  -H "Content-Type: application/json" `
+  -d "{\"id\":1}"
+
+# 更新状态:只允许更新 status,"1" 启用、"2" 停用;iden/name/content 不可改
+curl -X POST http://127.0.0.1:9993/api/v1/keys/update `
+  -H "Content-Type: application/json" `
+  -d "{\"id\":1,\"status\":\"2\"}"
+
+# 删除
+curl -X POST http://127.0.0.1:9993/api/v1/keys/delete `
+  -H "Content-Type: application/json" `
+  -d "{\"id\":1}"
 ```
 
 说明:
 
 - 密钥生成规则:`iden` 为去掉 `-` 的 UUIDv4;`content` 使用 PBKDF2-HMAC-SHA256(10 万次迭代、输出 32 字节、随机种子,`iden` 作盐)生成并转为十六进制;`status` 固定为 `"1"`;`id`、`create_at` 由数据库生成。
-- `PUT` 为全量更新,未传的可选字段会写成 `NULL`(空字符串、`serviceId=0` 均按 `NULL` 处理)。
+- 服务创建时 `id` 必填(`t_service` 主键没有 `AUTO_INCREMENT`);密钥接口如上全是 `POST` 请求体。
+- `PUT`(目录/服务)为全量更新,未传的可选字段会写成 `NULL`(空字符串、`serviceId=0` 均按 `NULL` 处理);`POST /keys/update` 只更新 `status`(`"1"` 启用、`"2"` 停用,其他取值返回 `100001`),`iden/name/content` 不可改。
 - 列表接口均已分页(见上),按 `id` 升序返回。
 - 错误响应见「统一响应格式」一节,业务码 `100001~100003`。
 
@@ -159,6 +177,31 @@ C:\Users\Administrator\go\bin\goctl.exe api go -api udeServer.api -dir .
 
 `internal/logic` 下的已有业务逻辑不会被覆盖,可放心迭代 API 定义。
 
+## 生成 OpenAPI 文档(Apifox 导入)
+
+1. 首次安装 swagger 插件(已装过可跳过):
+
+```powershell
+go install github.com/zeromicro/goctl-swagger@latest
+```
+
+2. 修改 `udeServer.api` 后重新生成(`doc` 目录不存在时须先创建,不会自动建目录):
+
+```powershell
+New-Item -ItemType Directory -Force -Path doc | Out-Null
+C:\Users\Administrator\go\bin\goctl.exe api plugin --api udeServer.api --dir doc ^
+  -p 'goctl-swagger="swagger -filename udeServer-api.json -host 127.0.0.1:9993 -schemes http"'
+```
+
+生成 `doc/udeServer-api.json`(Swagger 2.0 格式)。
+
+3. Apifox 导入:**项目设置 → 导入数据 → OpenAPI/Swagger → 选择文件**上传即可,也可用 URL 方式导入本地文件。
+
+说明:
+
+- 文档路径已带 `/api/v1` 前缀(来自 `@server(prefix: /api/v1)`),所以命令不要再传 `-basepath`,否则 Apifox 会把两者叠加成 `/api/v1/api/v1/`。
+- 文档中的响应体为业务数据结构(如 `KeyResp`、`KeyListResp`),而实际接口统一封装为 `{code,message,data}`(见「统一响应格式」),导入后如需贴实际报文,可在 Apifox 中手动调整响应示例或加全局模板。
+
 ## 配置
 
 `etc/udeserver-api.yaml`:
@@ -166,13 +209,13 @@ C:\Users\Administrator\go\bin\goctl.exe api go -api udeServer.api -dir .
 ```yaml
 Name: udeServer-api
 Host: 0.0.0.0
-Port: 8880
+Port: 9993
 
 MySQL:
   DataSource: "ude:H3G8xzfYpAASYFxF@tcp(10.10.10.30:3306)/ude?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
 ```
 
-> 说明:goctl 生成的默认端口是 8888,当前配置使用 8880。
+> 说明:goctl 生成的默认端口是 8888,当前配置使用 9993。
 >
 > 数据库密码目前以明文写在配置中,请勿提交到公开仓库;生产环境建议使用环境变量或密钥管理服务注入。
 

+ 2 - 1
internal/handler/listdirectorieshandler.go

@@ -6,10 +6,11 @@ package handler
 import (
 	"net/http"
 
-	"github.com/zeromicro/go-zero/rest/httpx"
 	"udeServer/internal/logic"
 	"udeServer/internal/svc"
 	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
 )
 
 func ListDirectoriesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {

+ 11 - 11
internal/handler/routes.go

@@ -51,28 +51,28 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 			},
 			{
 				Method:  http.MethodPost,
-				Path:    "/keys",
+				Path:    "/keys/create",
 				Handler: CreateKeyHandler(serverCtx),
 			},
 			{
 				Method:  http.MethodPost,
-				Path:    "/keys/list",
-				Handler: ListKeysHandler(serverCtx),
+				Path:    "/keys/delete",
+				Handler: DeleteKeyHandler(serverCtx),
 			},
 			{
-				Method:  http.MethodGet,
-				Path:    "/keys/:id",
+				Method:  http.MethodPost,
+				Path:    "/keys/detail",
 				Handler: GetKeyHandler(serverCtx),
 			},
 			{
-				Method:  http.MethodPut,
-				Path:    "/keys/:id",
-				Handler: UpdateKeyHandler(serverCtx),
+				Method:  http.MethodPost,
+				Path:    "/keys/list",
+				Handler: ListKeysHandler(serverCtx),
 			},
 			{
-				Method:  http.MethodDelete,
-				Path:    "/keys/:id",
-				Handler: DeleteKeyHandler(serverCtx),
+				Method:  http.MethodPost,
+				Path:    "/keys/update",
+				Handler: UpdateKeyHandler(serverCtx),
 			},
 			{
 				Method:  http.MethodPost,

+ 5 - 1
internal/logic/keygen.go

@@ -14,7 +14,11 @@ import (
 const (
 	keyKDFIterations = 100_000
 	keyContentBytes  = 32
-	keyDefaultStatus = "1"
+
+	keyStatusEnabled  = "1"
+	keyStatusDisabled = "2"
+
+	keyDefaultStatus = keyStatusEnabled
 )
 
 // newKeyIden returns a UUIDv4 string without dashes.

+ 11 - 1
internal/logic/listkeyslogic.go

@@ -5,7 +5,9 @@ package logic
 
 import (
 	"context"
+	"strings"
 
+	"udeServer/internal/apierr"
 	"udeServer/internal/svc"
 	"udeServer/internal/types"
 
@@ -26,10 +28,18 @@ func NewListKeysLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListKeys
 	}
 }
 
+// ListKeys lists keys with paging and optional filters.
+// name supports fuzzy matching; status accepts "1" (enabled) or "2" (disabled).
 func (l *ListKeysLogic) ListKeys(req *types.ListKeysReq) (*types.KeyListResp, error) {
 	page, pageSize := normalizePage(req.Page, req.PageSize)
 
-	list, total, err := l.svcCtx.KeyModel.FindPage(l.ctx, page, pageSize)
+	status := strings.TrimSpace(req.Status)
+	if status != "" && status != keyStatusEnabled && status != keyStatusDisabled {
+		return nil, apierr.BadRequest("status must be %q (enabled) or %q (disabled)",
+			keyStatusEnabled, keyStatusDisabled)
+	}
+
+	list, total, err := l.svcCtx.KeyModel.FindPage(l.ctx, page, pageSize, req.Name, status)
 	if err != nil {
 		l.Errorf("list keys failed: %v", err)
 		return nil, err

+ 11 - 10
internal/logic/updatekeylogic.go

@@ -5,8 +5,9 @@ package logic
 
 import (
 	"context"
+	"strings"
 
-	"udeServer/internal/model"
+	"udeServer/internal/apierr"
 	"udeServer/internal/svc"
 	"udeServer/internal/types"
 
@@ -27,23 +28,23 @@ func NewUpdateKeyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateK
 	}
 }
 
+// UpdateKey only toggles the key status: "1" enabled, "2" disabled.
+// Other fields (iden/name/content) can never be changed through this API.
 func (l *UpdateKeyLogic) UpdateKey(req *types.UpdateKeyReq) (*types.KeyResp, error) {
 	if err := checkId(req.Id); err != nil {
 		return nil, err
 	}
+	status := strings.TrimSpace(req.Status)
+	if status != keyStatusEnabled && status != keyStatusDisabled {
+		return nil, apierr.BadRequest("status must be %q (enabled) or %q (disabled)",
+			keyStatusEnabled, keyStatusDisabled)
+	}
 	if _, err := findKey(l.ctx, l.svcCtx.KeyModel, req.Id); err != nil {
 		return nil, err
 	}
 
-	data := &model.TKey{
-		Id:      req.Id,
-		Iden:    nullString(req.Iden),
-		Name:    nullString(req.Name),
-		Content: nullString(req.Content),
-		Status:  nullString(req.Status),
-	}
-	if err := l.svcCtx.KeyModel.Update(l.ctx, data); err != nil {
-		l.Errorf("update key %d failed: %v", req.Id, err)
+	if err := l.svcCtx.KeyModel.UpdateStatus(l.ctx, req.Id, status); err != nil {
+		l.Errorf("update key %d status failed: %v", req.Id, err)
 		return nil, err
 	}
 

+ 52 - 5
internal/model/tkeymodel.go

@@ -3,6 +3,7 @@ package model
 import (
 	"context"
 	"fmt"
+	"strings"
 
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
 )
@@ -14,7 +15,8 @@ type (
 	// and implement the added methods in customTKeyModel.
 	TKeyModel interface {
 		tKeyModel
-		FindPage(ctx context.Context, page, pageSize int64) ([]TKey, int64, error)
+		FindPage(ctx context.Context, page, pageSize int64, name, status string) ([]TKey, int64, error)
+		UpdateStatus(ctx context.Context, id int64, status string) error
 		withSession(session sqlx.Session) TKeyModel
 	}
 
@@ -30,9 +32,42 @@ func NewTKeyModel(conn sqlx.SqlConn) TKeyModel {
 	}
 }
 
-func (m *customTKeyModel) FindPage(ctx context.Context, page, pageSize int64) ([]TKey, int64, error) {
+// escapeLike escapes LIKE wildcards so the search value matches literally:
+// "50%_x" searches for the exact characters, not "50<any>..." patterns.
+func escapeLike(name string) string {
+	return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(name)
+}
+
+// trimName normalizes the search keyword; empty means no filtering.
+func trimName(name string) (string, bool) {
+	name = strings.TrimSpace(name)
+	return name, name != ""
+}
+
+// FindPage returns one page of rows ordered by id together with the total
+// count. When name is non-empty, rows are filtered by a name LIKE match;
+// when status is non-empty, rows are filtered by exact status ("1"/"2").
+func (m *customTKeyModel) FindPage(ctx context.Context, page, pageSize int64, name, status string) ([]TKey, int64, error) {
+	keyword, nameFilter := trimName(name)
+
+	var conds []string
+	var args []any
+	if nameFilter {
+		conds = append(conds, "`name` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(keyword)+"%")
+	}
+	if status != "" {
+		conds = append(conds, "`status` = ?")
+		args = append(args, status)
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
 	var total int64
-	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s", m.table)); err != nil {
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s%s", m.table, where), args...); err != nil {
 		return nil, 0, err
 	}
 
@@ -41,15 +76,27 @@ func (m *customTKeyModel) FindPage(ctx context.Context, page, pageSize int64) ([
 	}
 
 	offset := (page - 1) * pageSize
-	query := fmt.Sprintf("select %s from %s order by `id` limit ? offset ?", tKeyRows, m.table)
+	args = append(args, pageSize, offset)
+	query := fmt.Sprintf("select %s from %s%s order by `id` limit ? offset ?", tKeyRows, m.table, where)
 	var resp []TKey
-	if err := m.conn.QueryRowsCtx(ctx, &resp, query, pageSize, offset); err != nil {
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
 		return nil, 0, err
 	}
 
 	return resp, total, nil
 }
 
+// UpdateStatus enables (status "1") or disables (status "2") a key
+// without touching any other column.
+func (m *customTKeyModel) UpdateStatus(ctx context.Context, id int64, status string) error {
+	query := fmt.Sprintf("update %s set `status` = ? where `id` = ?", m.table)
+	if _, err := m.conn.ExecCtx(ctx, query, status, id); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (m *customTKeyModel) withSession(session sqlx.Session) TKeyModel {
 	return NewTKeyModel(sqlx.NewSqlConnFromSession(session))
 }

+ 7 - 8
internal/types/types.go

@@ -50,7 +50,7 @@ type DirectoryResp struct {
 }
 
 type KeyIdReq struct {
-	Id int64 `path:"id"`
+	Id int64 `json:"id"`
 }
 
 type KeyListResp struct {
@@ -73,8 +73,10 @@ type ListDirectoryReq struct {
 }
 
 type ListKeysReq struct {
-	Page     int64 `form:"page,optional"`
-	PageSize int64 `form:"pageSize,optional"`
+	Page     int64  `json:"page,optional"`
+	PageSize int64  `json:"pageSize,optional"`
+	Name     string `json:"name,optional"`
+	Status   string `json:"status,optional"`
 }
 
 type ListServiceReq struct {
@@ -118,11 +120,8 @@ type UpdateDirectoryReq struct {
 }
 
 type UpdateKeyReq struct {
-	Id      int64  `path:"id"`
-	Iden    string `json:"iden,optional"`
-	Name    string `json:"name,optional"`
-	Content string `json:"content,optional"`
-	Status  string `json:"status,optional"`
+	Id     int64  `json:"id"`
+	Status string `json:"status"`
 }
 
 type UpdateServiceReq struct {

+ 12 - 13
udeServer.api

@@ -55,15 +55,12 @@ type CreateKeyReq {
 }
 
 type UpdateKeyReq {
-	Id      int64  `path:"id"`
-	Iden    string `json:"iden,optional"`
-	Name    string `json:"name,optional"`
-	Content string `json:"content,optional"`
-	Status  string `json:"status,optional"`
+	Id     int64  `json:"id"`
+	Status string `json:"status"`
 }
 
 type KeyIdReq {
-	Id int64 `path:"id"`
+	Id int64 `json:"id"`
 }
 
 type KeyResp {
@@ -76,8 +73,10 @@ type KeyResp {
 }
 
 type ListKeysReq {
-	Page     int64 `form:"page,optional"`
-	PageSize int64 `form:"pageSize,optional"`
+	Page     int64  `json:"page,optional"`
+	PageSize int64  `json:"pageSize,optional"`
+	Name     string `json:"name,optional"`
+	Status   string `json:"status,optional"`
 }
 
 type KeyListResp {
@@ -159,19 +158,19 @@ service udeServer-api {
 	delete /directories/:id (DirectoryIdReq) returns (DeleteResp)
 
 	@handler CreateKeyHandler
-	post /keys (CreateKeyReq) returns (KeyResp)
+	post /keys/create (CreateKeyReq) returns (KeyResp)
 
 	@handler ListKeysHandler
-	get /keys (ListKeysReq) returns (KeyListResp)
+	post /keys/list (ListKeysReq) returns (KeyListResp)
 
 	@handler GetKeyHandler
-	get /keys/:id (KeyIdReq) returns (KeyResp)
+	post /keys/detail (KeyIdReq) returns (KeyResp)
 
 	@handler UpdateKeyHandler
-	put /keys/:id (UpdateKeyReq) returns (KeyResp)
+	post /keys/update (UpdateKeyReq) returns (KeyResp)
 
 	@handler DeleteKeyHandler
-	delete /keys/:id (KeyIdReq) returns (DeleteResp)
+	post /keys/delete (KeyIdReq) returns (DeleteResp)
 
 	@handler CreateServiceHandler
 	post /services (CreateServiceReq) returns (ServiceResp)

+ 5 - 9
udeserver.go

@@ -7,14 +7,13 @@ import (
 	"flag"
 	"fmt"
 
-	"udeServer/internal/apierr"
 	"udeServer/internal/config"
 	"udeServer/internal/handler"
+	"udeServer/internal/response"
 	"udeServer/internal/svc"
 
 	"github.com/zeromicro/go-zero/core/conf"
 	"github.com/zeromicro/go-zero/rest"
-	"github.com/zeromicro/go-zero/rest/httpx"
 )
 
 var configFile = flag.String("f", "etc/udeserver-api.yaml", "the config file")
@@ -25,13 +24,10 @@ func main() {
 	var c config.Config
 	conf.MustLoad(*configFile, &c)
 
-	httpx.SetErrorHandler(func(err error) (int, any) {
-		status := apierr.Status(err)
-		return status, map[string]any{
-			"code": status,
-			"msg":  err.Error(),
-		}
-	})
+	// 所有成功/失败响应统一封装为 {"code":100000/100xxx,"message":...,"data":...},
+	// HTTP 状态恒为 200;必须在 server.Start 之前注册。
+	response.Register()
+
 	//  rest.WithCors() 解决跨域
 	server := rest.MustNewServer(c.RestConf, rest.WithCors())
 	defer server.Stop()