Преглед на файлове

feat:1.封装响应
2.密钥列表接口

wangkun преди 2 седмици
родител
ревизия
e928ce1172

+ 63 - 14
README.md

@@ -44,14 +44,14 @@
 # 下载依赖(首次执行会按 go.mod 自动获取 Go 1.25.10 工具链)
 go mod download
 
-# 启动服务,默认监听 0.0.0.0:8080
+# 启动服务,默认监听 0.0.0.0:8880
 go run udeserver.go -f etc/udeserver-api.yaml
 
 # 另开终端验证
-curl http://127.0.0.1:8080/from/you
+curl http://127.0.0.1:8880/api/v1/from/you
 # {"message":"Hello you"}
 
-curl http://127.0.0.1:8080/db/ping
+curl http://127.0.0.1:8880/api/v1/db/ping
 # {"ok":true,"version":"8.4.4","database":"ude"}
 ```
 
@@ -72,32 +72,81 @@ go build -o bin/udeserver.exe udeserver.go
 | 密钥 `t_key` | `POST /keys` | `GET /keys` | `GET /keys/:id` | `PUT /keys/:id` | `DELETE /keys/:id` |
 | 服务 `t_service` | `POST /services` | `GET /services` | `GET /services/:id` | `PUT /services/:id` | `DELETE /services/:id` |
 
+> 所有路由统一带 `/api/v1` 前缀,如 `GET /api/v1/keys/:id`。前缀在 `udeServer.api` 的 `@server(prefix: /api/v1)` 中配置,实际请求都需要加前缀;`/from/:name` 和 `/db/ping` 也不例外。
+
+### 统一响应格式
+
+所有接口(成功/失败)HTTP 状态恒为 `200`,返回 `{code, message, data}` 封装:
+
+```json
+{
+  "code": 100000,
+  "message": "操作成功",
+  "data": { "ok": true }
+}
+```
+
+业务码:
+
+| code | 含义 |
+| --- | --- |
+| 100000 | 操作成功 |
+| 100001 | 参数错误(如缺少 `name`、`id` 非法) |
+| 100002 | 资源不存在 |
+| 100003 | 服务器内部错误 |
+
+- 失败响应:`{"code":100xxx,"message":"错误描述","data":null}`,前端只需判断 `code`。
+- 全局封装在 `udeserver.go` 的 `response.Register()` 中注册,实现见 `internal/response/`。
+
+### 分页
+
+三个列表接口(目录/密钥/服务)统一支持分页,通过 query 参数传递:
+
+- `page`:页码,默认 `1`
+- `pageSize`:每页数量,默认 `10`,最大 `100`
+- 响应 `data`:`{"list":[...],"total":13}`,`total` 为总条数;超出范围时 `list` 为 `[]`
+
+示例:`GET /api/v1/keys?page=2&pageSize=5`
+
 服务接口示例:
 
 ```powershell
-# 新增:id 必填,三张表的主键都没有 AUTO_INCREMENT
-curl -X POST http://127.0.0.1:8080/services `
+# 新增服务:id 必填(t_service 主键没有 AUTO_INCREMENT)
+curl -X POST http://127.0.0.1:8880/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:8080/services
-curl http://127.0.0.1:8080/services/1
+curl http://127.0.0.1:8880/api/v1/services
+curl http://127.0.0.1:8880/api/v1/services/1
 
 # 更新:id 放在路径中,请求体为完整字段
-curl -X PUT http://127.0.0.1:8080/services/1 `
+curl -X PUT http://127.0.0.1:8880/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:8080/services/1
+curl -X DELETE http://127.0.0.1:8880/api/v1/services/1
+```
+
+密钥接口示例:
+
+```powershell
+# 新增密钥:前端只需要传 name
+curl -X POST http://127.0.0.1:8880/api/v1/keys `
+  -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"}}
 ```
 
 说明:
 
+- 密钥生成规则:`iden` 为去掉 `-` 的 UUIDv4;`content` 使用 PBKDF2-HMAC-SHA256(10 万次迭代、输出 32 字节、随机种子,`iden` 作盐)生成并转为十六进制;`status` 固定为 `"1"`;`id`、`create_at` 由数据库生成。
 - `PUT` 为全量更新,未传的可选字段会写成 `NULL`(空字符串、`serviceId=0` 均按 `NULL` 处理)。
-- 列表接口返回全部数据,暂未分页。
-- 错误响应统一为 `{"code":404,"msg":"service id=1 not found"}` 格式。
+- 列表接口均已分页(见上),按 `id` 升序返回。
+- 错误响应见「统一响应格式」一节,业务码 `100001~100003`。
 
 ## 修改接口
 
@@ -117,13 +166,13 @@ C:\Users\Administrator\go\bin\goctl.exe api go -api udeServer.api -dir .
 ```yaml
 Name: udeServer-api
 Host: 0.0.0.0
-Port: 8080
+Port: 8880
 
 MySQL:
   DataSource: "ude:H3G8xzfYpAASYFxF@tcp(10.10.10.30:3306)/ude?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
 ```
 
-> 说明:goctl 生成的默认端口是 8888,但本机 8888 已被另一个 GoLand 服务占用,因此改为 8080。
+> 说明:goctl 生成的默认端口是 8888,当前配置使用 8880。
 >
 > 数据库密码目前以明文写在配置中,请勿提交到公开仓库;生产环境建议使用环境变量或密钥管理服务注入。
 
@@ -141,7 +190,7 @@ err := l.svcCtx.DB.QueryRowCtx(l.ctx, &count, "SELECT COUNT(*) FROM your_table")
 ```
 
 - 已为三张表生成 model(`internal/model/`):`TDirectoryModel`、`TKeyModel`、`TServiceModel`,并在自定义 model 文件中补充了列表查询 `FindAll`。
-- 三张表的主键都是普通 `int`,没有 `AUTO_INCREMENT`,因此新增接口必须显式传 `id`。
+- `t_directory`、`t_service` 的主键是普通 `int`,没有 `AUTO_INCREMENT`,新增时必须显式传 `id`;`t_key.id` 为自增主键,新增时不需要传。
 - 新增表后可按表继续生成 model:
 
 ```powershell

+ 1 - 1
etc/udeserver-api.yaml

@@ -1,6 +1,6 @@
 Name: udeServer-api
 Host: 0.0.0.0
-Port: 8080
+Port: 9993
 
 MySQL:
   DataSource: "ude:H3G8xzfYpAASYFxF@tcp(10.10.10.30:3306)/ude?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"

+ 4 - 2
go.mod

@@ -2,7 +2,10 @@ module udeServer
 
 go 1.25.10
 
-require github.com/zeromicro/go-zero v1.10.3
+require (
+	github.com/google/uuid v1.6.0
+	github.com/zeromicro/go-zero v1.10.3
+)
 
 require (
 	filippo.io/edwards25519 v1.2.0 // indirect
@@ -14,7 +17,6 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.10.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/grafana/pyroscope-go v1.3.0 // indirect
 	github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect

+ 4 - 2
internal/apierr/apierr.go

@@ -32,12 +32,14 @@ func NotFound(kind string, id int64) error {
 	return New(http.StatusNotFound, fmt.Sprintf("%s id=%d not found", kind, id))
 }
 
-// Status resolves the HTTP status code of err.
+// Status resolves the HTTP status code of err. Errors wrapping unrecovered
+// failures (database, network, ...) must have no apierr.Error in their
+// chain, hence they resolve to 500.
 func Status(err error) int {
 	var e *Error
 	if errors.As(err, &e) {
 		return e.Status
 	}
 
-	return http.StatusBadRequest
+	return http.StatusInternalServerError
 }

+ 8 - 1
internal/handler/listdirectorieshandler.go

@@ -9,12 +9,19 @@ import (
 	"github.com/zeromicro/go-zero/rest/httpx"
 	"udeServer/internal/logic"
 	"udeServer/internal/svc"
+	"udeServer/internal/types"
 )
 
 func ListDirectoriesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ListDirectoryReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
 		l := logic.NewListDirectoriesLogic(r.Context(), svcCtx)
-		resp, err := l.ListDirectories()
+		resp, err := l.ListDirectories(&req)
 		if err != nil {
 			httpx.ErrorCtx(r.Context(), w, err)
 		} else {

+ 8 - 1
internal/handler/listkeyshandler.go

@@ -9,12 +9,19 @@ import (
 	"github.com/zeromicro/go-zero/rest/httpx"
 	"udeServer/internal/logic"
 	"udeServer/internal/svc"
+	"udeServer/internal/types"
 )
 
 func ListKeysHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ListKeysReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
 		l := logic.NewListKeysLogic(r.Context(), svcCtx)
-		resp, err := l.ListKeys()
+		resp, err := l.ListKeys(&req)
 		if err != nil {
 			httpx.ErrorCtx(r.Context(), w, err)
 		} else {

+ 8 - 1
internal/handler/listserviceshandler.go

@@ -9,12 +9,19 @@ import (
 	"github.com/zeromicro/go-zero/rest/httpx"
 	"udeServer/internal/logic"
 	"udeServer/internal/svc"
+	"udeServer/internal/types"
 )
 
 func ListServicesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ListServiceReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
 		l := logic.NewListServicesLogic(r.Context(), svcCtx)
-		resp, err := l.ListServices()
+		resp, err := l.ListServices(&req)
 		if err != nil {
 			httpx.ErrorCtx(r.Context(), w, err)
 		} else {

+ 3 - 2
internal/handler/routes.go

@@ -55,8 +55,8 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 				Handler: CreateKeyHandler(serverCtx),
 			},
 			{
-				Method:  http.MethodGet,
-				Path:    "/keys",
+				Method:  http.MethodPost,
+				Path:    "/keys/list",
 				Handler: ListKeysHandler(serverCtx),
 			},
 			{
@@ -100,5 +100,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 				Handler: DeleteServiceHandler(serverCtx),
 			},
 		},
+		rest.WithPrefix("/api/v1"),
 	)
 }

+ 26 - 9
internal/logic/createkeylogic.go

@@ -5,7 +5,9 @@ package logic
 
 import (
 	"context"
+	"strings"
 
+	"udeServer/internal/apierr"
 	"udeServer/internal/model"
 	"udeServer/internal/svc"
 	"udeServer/internal/types"
@@ -28,23 +30,38 @@ func NewCreateKeyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateK
 }
 
 func (l *CreateKeyLogic) CreateKey(req *types.CreateKeyReq) (*types.KeyResp, error) {
-	if err := checkId(req.Id); err != nil {
+	name := strings.TrimSpace(req.Name)
+	if name == "" {
+		return nil, apierr.BadRequest("name is required")
+	}
+
+	iden := newKeyIden()
+	content, err := deriveKeyContent(iden)
+	if err != nil {
+		l.Errorf("derive key content failed: %v", err)
 		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),
+		Iden:    nullString(iden),
+		Name:    nullString(name),
+		Content: nullString(content),
+		Status:  nullString(keyDefaultStatus),
 	}
-	if _, err := l.svcCtx.KeyModel.Insert(l.ctx, data); err != nil {
-		l.Errorf("insert key %d failed: %v", req.Id, err)
+
+	result, err := l.svcCtx.KeyModel.Insert(l.ctx, data)
+	if err != nil {
+		l.Errorf("insert key failed: %v", err)
+		return nil, err
+	}
+
+	id, err := result.LastInsertId()
+	if err != nil {
+		l.Errorf("get inserted key id failed: %v", err)
 		return nil, err
 	}
 
-	created, err := findKey(l.ctx, l.svcCtx.KeyModel, req.Id)
+	created, err := findKey(l.ctx, l.svcCtx.KeyModel, id)
 	if err != nil {
 		return nil, err
 	}

+ 40 - 0
internal/logic/keygen.go

@@ -0,0 +1,40 @@
+package logic
+
+import (
+	"crypto/pbkdf2"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"strings"
+
+	"github.com/google/uuid"
+)
+
+const (
+	keyKDFIterations = 100_000
+	keyContentBytes  = 32
+	keyDefaultStatus = "1"
+)
+
+// newKeyIden returns a UUIDv4 string without dashes.
+func newKeyIden() string {
+	return strings.ReplaceAll(uuid.NewString(), "-", "")
+}
+
+// deriveKeyContent derives a 256-bit key with PBKDF2-HMAC-SHA256.
+// The password is a fresh random seed and iden is the salt, so every
+// key record gets an unpredictable content.
+func deriveKeyContent(iden string) (string, error) {
+	seed := make([]byte, 32)
+	if _, err := rand.Read(seed); err != nil {
+		return "", fmt.Errorf("generate random seed: %w", err)
+	}
+
+	derived, err := pbkdf2.Key(sha256.New, string(seed), []byte(iden), keyKDFIterations, keyContentBytes)
+	if err != nil {
+		return "", fmt.Errorf("derive key content: %w", err)
+	}
+
+	return hex.EncodeToString(derived), nil
+}

+ 39 - 0
internal/logic/keygen_test.go

@@ -0,0 +1,39 @@
+package logic
+
+import (
+	"encoding/hex"
+	"testing"
+)
+
+func TestNewKeyIden(t *testing.T) {
+	iden := newKeyIden()
+	if len(iden) != 32 {
+		t.Fatalf("unexpected iden length: %d", len(iden))
+	}
+	if _, err := hex.DecodeString(iden); err != nil {
+		t.Fatalf("iden is not hex: %v", err)
+	}
+}
+
+func TestDeriveKeyContent(t *testing.T) {
+	iden := newKeyIden()
+
+	first, err := deriveKeyContent(iden)
+	if err != nil {
+		t.Fatalf("derive first content: %v", err)
+	}
+	second, err := deriveKeyContent(iden)
+	if err != nil {
+		t.Fatalf("derive second content: %v", err)
+	}
+
+	if len(first) != 64 {
+		t.Fatalf("unexpected content length: %d", len(first))
+	}
+	if _, err := hex.DecodeString(first); err != nil {
+		t.Fatalf("content is not hex: %v", err)
+	}
+	if first == second {
+		t.Fatal("content should differ because the seed is random")
+	}
+}

+ 6 - 3
internal/logic/listdirectorieslogic.go

@@ -26,15 +26,18 @@ func NewListDirectoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *L
 	}
 }
 
-func (l *ListDirectoriesLogic) ListDirectories() (*types.DirectoryListResp, error) {
-	list, err := l.svcCtx.DirectoryModel.FindAll(l.ctx)
+func (l *ListDirectoriesLogic) ListDirectories(req *types.ListDirectoryReq) (*types.DirectoryListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	list, total, err := l.svcCtx.DirectoryModel.FindPage(l.ctx, page, pageSize)
 	if err != nil {
 		l.Errorf("list directories failed: %v", err)
 		return nil, err
 	}
 
 	resp := &types.DirectoryListResp{
-		List: make([]types.DirectoryResp, 0, len(list)),
+		List:  make([]types.DirectoryResp, 0, len(list)),
+		Total: total,
 	}
 	for i := range list {
 		resp.List = append(resp.List, *toDirectoryResp(&list[i]))

+ 6 - 3
internal/logic/listkeyslogic.go

@@ -26,15 +26,18 @@ func NewListKeysLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListKeys
 	}
 }
 
-func (l *ListKeysLogic) ListKeys() (*types.KeyListResp, error) {
-	list, err := l.svcCtx.KeyModel.FindAll(l.ctx)
+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)
 	if err != nil {
 		l.Errorf("list keys failed: %v", err)
 		return nil, err
 	}
 
 	resp := &types.KeyListResp{
-		List: make([]types.KeyResp, 0, len(list)),
+		List:  make([]types.KeyResp, 0, len(list)),
+		Total: total,
 	}
 	for i := range list {
 		resp.List = append(resp.List, *toKeyResp(&list[i]))

+ 6 - 3
internal/logic/listserviceslogic.go

@@ -26,15 +26,18 @@ func NewListServicesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *List
 	}
 }
 
-func (l *ListServicesLogic) ListServices() (*types.ServiceListResp, error) {
-	list, err := l.svcCtx.ServiceModel.FindAll(l.ctx)
+func (l *ListServicesLogic) ListServices(req *types.ListServiceReq) (*types.ServiceListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	list, total, err := l.svcCtx.ServiceModel.FindPage(l.ctx, page, pageSize)
 	if err != nil {
 		l.Errorf("list services failed: %v", err)
 		return nil, err
 	}
 
 	resp := &types.ServiceListResp{
-		List: make([]types.ServiceResp, 0, len(list)),
+		List:  make([]types.ServiceResp, 0, len(list)),
+		Total: total,
 	}
 	for i := range list {
 		resp.List = append(resp.List, *toServiceResp(&list[i]))

+ 30 - 0
internal/logic/pagination.go

@@ -0,0 +1,30 @@
+package logic
+
+import "math"
+
+const (
+	defaultPage     = 1
+	defaultPageSize = 10
+	maxPageSize     = 100
+)
+
+// normalizePage applies defaults and bounds to the requested page/pageSize,
+// and keeps the computed offset within int64 even for absurd page numbers.
+func normalizePage(page, pageSize int64) (int64, int64) {
+	if page < 1 {
+		page = defaultPage
+	}
+	if pageSize < 1 {
+		pageSize = defaultPageSize
+	}
+	if pageSize > maxPageSize {
+		pageSize = maxPageSize
+	}
+
+	const safeOffset = math.MaxInt64 / 2
+	if page-1 > safeOffset/pageSize {
+		page = safeOffset/pageSize + 1
+	}
+
+	return page, pageSize
+}

+ 16 - 6
internal/model/tdirectorymodel.go

@@ -14,7 +14,7 @@ type (
 	// and implement the added methods in customTDirectoryModel.
 	TDirectoryModel interface {
 		tDirectoryModel
-		FindAll(ctx context.Context) ([]TDirectory, error)
+		FindPage(ctx context.Context, page, pageSize int64) ([]TDirectory, int64, error)
 		withSession(session sqlx.Session) TDirectoryModel
 	}
 
@@ -30,14 +30,24 @@ func NewTDirectoryModel(conn sqlx.SqlConn) TDirectoryModel {
 	}
 }
 
-func (m *customTDirectoryModel) FindAll(ctx context.Context) ([]TDirectory, error) {
-	query := fmt.Sprintf("select %s from %s order by `id`", tDirectoryRows, m.table)
+func (m *customTDirectoryModel) FindPage(ctx context.Context, page, pageSize int64) ([]TDirectory, int64, error) {
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s", m.table)); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	query := fmt.Sprintf("select %s from %s order by `id` limit ? offset ?", tDirectoryRows, m.table)
 	var resp []TDirectory
-	if err := m.conn.QueryRowsCtx(ctx, &resp, query); err != nil {
-		return nil, err
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, pageSize, offset); err != nil {
+		return nil, 0, err
 	}
 
-	return resp, nil
+	return resp, total, nil
 }
 
 func (m *customTDirectoryModel) withSession(session sqlx.Session) TDirectoryModel {

+ 16 - 6
internal/model/tkeymodel.go

@@ -14,7 +14,7 @@ type (
 	// and implement the added methods in customTKeyModel.
 	TKeyModel interface {
 		tKeyModel
-		FindAll(ctx context.Context) ([]TKey, error)
+		FindPage(ctx context.Context, page, pageSize int64) ([]TKey, int64, error)
 		withSession(session sqlx.Session) TKeyModel
 	}
 
@@ -30,14 +30,24 @@ func NewTKeyModel(conn sqlx.SqlConn) TKeyModel {
 	}
 }
 
-func (m *customTKeyModel) FindAll(ctx context.Context) ([]TKey, error) {
-	query := fmt.Sprintf("select %s from %s order by `id`", tKeyRows, m.table)
+func (m *customTKeyModel) FindPage(ctx context.Context, page, pageSize int64) ([]TKey, int64, error) {
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s", m.table)); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	query := fmt.Sprintf("select %s from %s order by `id` limit ? offset ?", tKeyRows, m.table)
 	var resp []TKey
-	if err := m.conn.QueryRowsCtx(ctx, &resp, query); err != nil {
-		return nil, err
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, pageSize, offset); err != nil {
+		return nil, 0, err
 	}
 
-	return resp, nil
+	return resp, total, nil
 }
 
 func (m *customTKeyModel) withSession(session sqlx.Session) TKeyModel {

+ 3 - 3
internal/model/tkeymodel_gen.go

@@ -19,7 +19,7 @@ import (
 var (
 	tKeyFieldNames          = builder.RawFieldNames(&TKey{})
 	tKeyRows                = strings.Join(tKeyFieldNames, ",")
-	tKeyRowsExpectAutoSet   = strings.Join(stringx.Remove(tKeyFieldNames, "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+	tKeyRowsExpectAutoSet   = strings.Join(stringx.Remove(tKeyFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
 	tKeyRowsWithPlaceHolder = strings.Join(stringx.Remove(tKeyFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
 )
 
@@ -74,8 +74,8 @@ func (m *defaultTKeyModel) FindOne(ctx context.Context, id int64) (*TKey, error)
 }
 
 func (m *defaultTKeyModel) Insert(ctx context.Context, data *TKey) (sql.Result, error) {
-	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?)", m.table, tKeyRowsExpectAutoSet)
-	ret, err := m.conn.ExecCtx(ctx, query, data.Id, data.Iden, data.Name, data.Content, data.Status)
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?)", m.table, tKeyRowsExpectAutoSet)
+	ret, err := m.conn.ExecCtx(ctx, query, data.Iden, data.Name, data.Content, data.Status)
 	return ret, err
 }
 

+ 16 - 6
internal/model/tservicemodel.go

@@ -14,7 +14,7 @@ type (
 	// and implement the added methods in customTServiceModel.
 	TServiceModel interface {
 		tServiceModel
-		FindAll(ctx context.Context) ([]TService, error)
+		FindPage(ctx context.Context, page, pageSize int64) ([]TService, int64, error)
 		withSession(session sqlx.Session) TServiceModel
 	}
 
@@ -30,14 +30,24 @@ func NewTServiceModel(conn sqlx.SqlConn) TServiceModel {
 	}
 }
 
-func (m *customTServiceModel) FindAll(ctx context.Context) ([]TService, error) {
-	query := fmt.Sprintf("select %s from %s order by `id`", tServiceRows, m.table)
+func (m *customTServiceModel) FindPage(ctx context.Context, page, pageSize int64) ([]TService, int64, error) {
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s", m.table)); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	query := fmt.Sprintf("select %s from %s order by `id` limit ? offset ?", tServiceRows, m.table)
 	var resp []TService
-	if err := m.conn.QueryRowsCtx(ctx, &resp, query); err != nil {
-		return nil, err
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, pageSize, offset); err != nil {
+		return nil, 0, err
 	}
 
-	return resp, nil
+	return resp, total, nil
 }
 
 func (m *customTServiceModel) withSession(session sqlx.Session) TServiceModel {

+ 87 - 0
internal/response/response.go

@@ -0,0 +1,87 @@
+// Package response provides the unified response envelope {code, message, data}
+// returned by every endpoint of this service.
+package response
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"strings"
+
+	"udeServer/internal/apierr"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+)
+
+// Business codes carried in the unified response body.
+const (
+	CodeOK          = 100000
+	CodeBadRequest  = 100001
+	CodeNotFound    = 100002
+	CodeServerError = 100003
+)
+
+// MsgOK is the message of every successful response.
+const MsgOK = "操作成功"
+
+// Body is the unified response envelope.
+type Body struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+	Data    any    `json:"data"`
+}
+
+// OK builds a successful response body.
+func OK(data any) Body {
+	return Body{Code: CodeOK, Message: MsgOK, Data: data}
+}
+
+// Register installs the global success and error handlers on the framework,
+// so every endpoint response is wrapped in the unified envelope:
+//   - success: HTTP 200 + {"code":100000,...,"data":<resp>}
+//   - failure: HTTP 200 + {"code":100xxx,...,"data":null}
+func Register() {
+	httpx.SetOkHandler(func(_ context.Context, data any) any {
+		return OK(data)
+	})
+
+	// HTTP status is always 200; the client decides by body code only.
+	httpx.SetErrorHandler(func(err error) (int, any) {
+		return http.StatusOK, Body{
+			Code:    Code(err),
+			Message: err.Error(),
+			Data:    nil,
+		}
+	})
+}
+
+// Code maps an error to its business code. Errors without an HTTP-aware
+// status, e.g. failures from the framework request parser, fall back to
+// CodeServerError unless they describe a missing/unset field.
+func Code(err error) int {
+	var apiErr *apierr.Error
+	if errors.As(err, &apiErr) {
+		return statusToCode(apiErr.Status)
+	}
+
+	// The request parser reports missing required fields as a plain error
+	// like `field "name" is not set` (core/mapping/unmarshaler.go), which is
+	// a client-side problem rather than a server error.
+	if strings.HasSuffix(err.Error(), " is not set") {
+		return CodeBadRequest
+	}
+
+	return CodeServerError
+}
+
+// statusToCode maps the HTTP-aware status of apierr.Error to the body code.
+func statusToCode(status int) int {
+	switch status {
+	case http.StatusBadRequest:
+		return CodeBadRequest
+	case http.StatusNotFound:
+		return CodeNotFound
+	default:
+		return CodeServerError
+	}
+}

+ 22 - 8
internal/types/types.go

@@ -10,11 +10,7 @@ type CreateDirectoryReq struct {
 }
 
 type CreateKeyReq struct {
-	Id      int64  `json:"id"`
-	Iden    string `json:"iden,optional"`
-	Name    string `json:"name,optional"`
-	Content string `json:"content,optional"`
-	Status  string `json:"status,optional"`
+	Name string `json:"name"`
 }
 
 type CreateServiceReq struct {
@@ -43,7 +39,8 @@ type DirectoryIdReq struct {
 }
 
 type DirectoryListResp struct {
-	List []DirectoryResp `json:"list"`
+	List  []DirectoryResp `json:"list"`
+	Total int64           `json:"total"`
 }
 
 type DirectoryResp struct {
@@ -57,7 +54,8 @@ type KeyIdReq struct {
 }
 
 type KeyListResp struct {
-	List []KeyResp `json:"list"`
+	List  []KeyResp `json:"list"`
+	Total int64     `json:"total"`
 }
 
 type KeyResp struct {
@@ -69,6 +67,21 @@ type KeyResp struct {
 	CreateAt string `json:"createAt"`
 }
 
+type ListDirectoryReq struct {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
+type ListKeysReq struct {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
+type ListServiceReq struct {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
 type Request struct {
 	Name string `path:"name,options=you|me"`
 }
@@ -82,7 +95,8 @@ type ServiceIdReq struct {
 }
 
 type ServiceListResp struct {
-	List []ServiceResp `json:"list"`
+	List  []ServiceResp `json:"list"`
+	Total int64         `json:"total"`
 }
 
 type ServiceResp struct {

+ 28 - 11
udeServer.api

@@ -40,16 +40,18 @@ type DirectoryResp {
 	Directory string `json:"directory"`
 }
 
+type ListDirectoryReq {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
 type DirectoryListResp {
-	List []DirectoryResp `json:"list"`
+	List  []DirectoryResp `json:"list"`
+	Total int64           `json:"total"`
 }
 
 type CreateKeyReq {
-	Id      int64  `json:"id"`
-	Iden    string `json:"iden,optional"`
-	Name    string `json:"name,optional"`
-	Content string `json:"content,optional"`
-	Status  string `json:"status,optional"`
+	Name string `json:"name"`
 }
 
 type UpdateKeyReq {
@@ -73,8 +75,14 @@ type KeyResp {
 	CreateAt string `json:"createAt"`
 }
 
+type ListKeysReq {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
 type KeyListResp {
-	List []KeyResp `json:"list"`
+	List  []KeyResp `json:"list"`
+	Total int64     `json:"total"`
 }
 
 type CreateServiceReq {
@@ -115,10 +123,19 @@ type ServiceResp {
 	CreateAt string `json:"createAt"`
 }
 
+type ListServiceReq {
+	Page     int64 `form:"page,optional"`
+	PageSize int64 `form:"pageSize,optional"`
+}
+
 type ServiceListResp {
-	List []ServiceResp `json:"list"`
+	List  []ServiceResp `json:"list"`
+	Total int64         `json:"total"`
 }
 
+@server (
+	prefix: /api/v1
+)
 service udeServer-api {
 	@handler UdeServerHandler
 	get /from/:name (Request) returns (Response)
@@ -130,7 +147,7 @@ service udeServer-api {
 	post /directories (CreateDirectoryReq) returns (DirectoryResp)
 
 	@handler ListDirectoriesHandler
-	get /directories returns (DirectoryListResp)
+	get /directories (ListDirectoryReq) returns (DirectoryListResp)
 
 	@handler GetDirectoryHandler
 	get /directories/:id (DirectoryIdReq) returns (DirectoryResp)
@@ -145,7 +162,7 @@ service udeServer-api {
 	post /keys (CreateKeyReq) returns (KeyResp)
 
 	@handler ListKeysHandler
-	get /keys returns (KeyListResp)
+	get /keys (ListKeysReq) returns (KeyListResp)
 
 	@handler GetKeyHandler
 	get /keys/:id (KeyIdReq) returns (KeyResp)
@@ -160,7 +177,7 @@ service udeServer-api {
 	post /services (CreateServiceReq) returns (ServiceResp)
 
 	@handler ListServicesHandler
-	get /services returns (ServiceListResp)
+	get /services (ListServiceReq) returns (ServiceListResp)
 
 	@handler GetServiceHandler
 	get /services/:id (ServiceIdReq) returns (ServiceResp)

+ 4 - 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,9 @@ 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。
+	response.Register()
 
 	server := rest.MustNewServer(c.RestConf)
 	defer server.Stop()