实现category service基础接口

This commit is contained in:
2025-05-31 16:27:01 +08:00
parent faa6a35475
commit e5446bf836
33 changed files with 1420 additions and 67 deletions

View File

@ -10,4 +10,10 @@ UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Key: user.rpc
CategoryRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: category.rpc

View File

@ -11,5 +11,6 @@ type Config struct {
AccessSecret string
AccessExpire int64
}
UserRpc zrpc.RpcClientConf
UserRpc zrpc.RpcClientConf
CategoryRpc zrpc.RpcClientConf
}

View File

@ -0,0 +1,50 @@
package handler
import (
"net/http"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func createCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateCategoryReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewCreateCategoryLogic(r.Context(), svcCtx)
resp, err := l.CreateCategory(&req)
if err != nil {
if s, ok := status.FromError(err); ok {
// 判断 gRPC 错误码,并自定义 HTTP 返回
var code int
switch s.Code() {
case codes.NotFound:
code = http.StatusNotFound
case codes.InvalidArgument:
code = http.StatusBadRequest
default:
code = http.StatusInternalServerError
}
// 自定义 JSON 错误格式
httpx.WriteJson(w, code, map[string]interface{}{
"code": code,
"message": s.Message(),
})
} else {
httpx.ErrorCtx(r.Context(), w, err)
}
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func deleteCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.DeleteCategoryReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewDeleteCategoryLogic(r.Context(), svcCtx)
resp, err := l.DeleteCategory(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func getCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetCategoryReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetCategoryLogic(r.Context(), svcCtx)
resp, err := l.GetCategory(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func getCategoryTreeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetCategoryTreeReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetCategoryTreeLogic(r.Context(), svcCtx)
resp, err := l.GetCategoryTree(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func getSystemCategoriesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.GetSystemCategoriesReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetSystemCategoriesLogic(r.Context(), svcCtx)
resp, err := l.GetSystemCategories(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func listCategoriesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ListCategoriesReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewListCategoriesLogic(r.Context(), svcCtx)
resp, err := l.ListCategories(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -16,30 +16,67 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
[]rest.Route{
{
Method: http.MethodPost,
Path: "/api/user/login",
Path: "/user/login",
Handler: loginHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/api/user/register",
Path: "/user/register",
Handler: registerHandler(serverCtx),
},
},
rest.WithPrefix("/api"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodPost,
Path: "/category/v1",
Handler: createCategoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/api/user/:user_id",
Path: "/category/v1",
Handler: listCategoriesHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/category/v1/:id",
Handler: getCategoryHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/category/v1/:id",
Handler: updateCategoryHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/category/v1/:id",
Handler: deleteCategoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/category/v1/:id/tree",
Handler: getCategoryTreeHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/category/v1/system/:system_id",
Handler: getSystemCategoriesHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/:user_id",
Handler: getUserInfoHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/api/user/logout",
Path: "/user/logout",
Handler: logoutHandler(serverCtx),
},
},
rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
rest.WithPrefix("/api"),
)
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"godemo/gateway/internal/logic"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
)
func updateCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateCategoryReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewUpdateCategoryLogic(r.Context(), svcCtx)
resp, err := l.UpdateCategory(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

View File

@ -0,0 +1,103 @@
package logic
import (
"context"
"strings"
"godemo/category/category"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type CreateCategoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCategoryLogic {
return &CreateCategoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateCategoryLogic) CreateCategory(req *types.CreateCategoryReq) (resp *types.CreateCategoryResp, err error) {
// 1. 参数验证
if err := validateCreateRequest(req); err != nil {
return nil, err
}
// 2. 准备 RPC 请求
rpcReq := &category.CreateCategoryRequest{
SystemId: req.SystemID,
Name: req.Name,
Description: req.Description,
}
// 可选参数处理
if req.Alias != "" {
rpcReq.Alias = req.Alias
}
if req.ParentID != "" {
rpcReq.ParentId = req.ParentID
}
// 3. 调用 RPC 服务
rpcResp, rpcErr := l.svcCtx.CategoryRpc.CreateCategory(l.ctx, rpcReq)
if rpcErr != nil {
return nil, rpcErr
}
// 4. 构建响应
resp = &types.CreateCategoryResp{
ID: rpcResp.Category.Id,
}
l.Logger.Infof("Category created successfully: ID=%s, Name=%s", resp.ID, req.Name)
return resp, nil
}
// validateCreateRequest 验证创建分类请求
func validateCreateRequest(req *types.CreateCategoryReq) error {
// 必需字段检查
if strings.TrimSpace(req.SystemID) == "" {
return status.Error(codes.InvalidArgument, "systemId不能为空")
}
if strings.TrimSpace(req.Name) == "" {
return status.Error(codes.InvalidArgument, "name不能为空")
}
// 名称长度限制
if len(req.Name) > 50 {
return status.Error(codes.InvalidArgument, "alias cannot exceed 50 characters")
}
// 别名格式验证 (只允许字母、数字、连字符和下划线)
// if req.Alias != "" {
// if len(req.Alias) > 50 {
// return errors.New("alias cannot exceed 50 characters")
// }
// for _, ch := range req.Alias {
// if !(ch >= 'a' && ch <= 'z') &&
// !(ch >= 'A' && ch <= 'Z') &&
// !(ch >= '0' && ch <= '9') &&
// ch != '-' && ch != '_' {
// return errors.New("alias can only contain letters, numbers, hyphens and underscores")
// }
// }
// }
// 描述长度限制
if len(req.Description) > 500 {
return status.Error(codes.InvalidArgument, "description cannot exceed 500 characters")
}
return nil
}

View File

@ -0,0 +1,30 @@
package logic
import (
"context"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteCategoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCategoryLogic {
return &DeleteCategoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteCategoryLogic) DeleteCategory(req *types.DeleteCategoryReq) (resp *types.BaseResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,59 @@
package logic
import (
"context"
"time"
"godemo/category/category"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetCategoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCategoryLogic {
return &GetCategoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetCategoryLogic) GetCategory(req *types.GetCategoryReq) (resp *types.CategoryDetailResp, err error) {
rpcResp, err := l.svcCtx.CategoryRpc.GetCategory(l.ctx, &category.GetCategoryRequest{
Id: req.ID,
})
if err != nil {
return nil, err
}
// 转换主分类的时间
createdAtStr := time.Unix(rpcResp.Category.CreatedAt, 0).Format(time.RFC3339)
updatedAtStr := ""
if rpcResp.Category.UpdatedAt != 0 {
updatedAtStr = time.Unix(rpcResp.Category.UpdatedAt, 0).Format(time.RFC3339)
}
resp = &types.CategoryDetailResp{
CategoryDetail: types.CategoryDetail{
BaseCategory: types.BaseCategory{
ID: rpcResp.Category.Id,
Name: rpcResp.Category.Name,
Alias: rpcResp.Category.Alias,
Description: rpcResp.Category.Description,
ParentID: rpcResp.Category.ParentId,
SystemID: rpcResp.Category.SystemId,
CreatedAt: createdAtStr,
UpdatedAt: updatedAtStr,
},
Parent: nil,
},
}
return
}

View File

@ -0,0 +1,30 @@
package logic
import (
"context"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetCategoryTreeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetCategoryTreeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCategoryTreeLogic {
return &GetCategoryTreeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetCategoryTreeLogic) GetCategoryTree(req *types.GetCategoryTreeReq) (resp *types.CategoryTreeResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,97 @@
package logic
import (
"context"
"strings"
"time"
"godemo/category/category"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type GetSystemCategoriesLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetSystemCategoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSystemCategoriesLogic {
return &GetSystemCategoriesLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetSystemCategoriesLogic) GetSystemCategories(req *types.GetSystemCategoriesReq) (resp *types.ListCategoriesResp, err error) {
// 1. 验证系统ID
if strings.TrimSpace(req.SystemID) == "" {
return nil, status.Error(codes.InvalidArgument, "system_id is required")
}
// 2. 准备 RPC 请求
rpcReq := &category.GetSystemCategoriesRequest{
SystemId: req.SystemID,
IncludeDescendants: req.IncludeDescendants,
}
// 添加父分类ID过滤如果提供
if req.ParentID != "" {
rpcReq.ParentId = req.ParentID
}
// 3. 调用 RPC 服务
rpcResp, rpcErr := l.svcCtx.CategoryRpc.GetSystemCategories(l.ctx, rpcReq)
if rpcErr != nil {
st, ok := status.FromError(rpcErr)
if ok && st.Code() == codes.NotFound {
// 返回空列表而不是错误
return &types.ListCategoriesResp{
Total: 0,
List: []types.BaseCategory{},
}, nil
}
l.Logger.Errorf("RPC error: %v", rpcErr)
return nil, rpcErr
}
// 4. 转换响应数据
categories := make([]types.BaseCategory, 0, len(rpcResp.Categories))
for _, cat := range rpcResp.Categories {
// 转换时间格式
createdAt := ""
if cat.CreatedAt > 0 {
createdAt = time.Unix(cat.CreatedAt, 0).Format(time.RFC3339)
}
updatedAt := ""
if cat.UpdatedAt > 0 {
updatedAt = time.Unix(cat.UpdatedAt, 0).Format(time.RFC3339)
}
categories = append(categories, types.BaseCategory{
ID: cat.Id,
SystemID: cat.SystemId,
Name: cat.Name,
Alias: cat.Alias,
ParentID: cat.ParentId,
Description: cat.Description,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
// 5. 构建响应
resp = &types.ListCategoriesResp{
Total: rpcResp.Total,
List: categories,
}
l.Logger.Infof("Retrieved %d categories for system %s (parent: %s)", len(categories), req.SystemID, req.ParentID)
return resp, nil
}

View File

@ -0,0 +1,30 @@
package logic
import (
"context"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ListCategoriesLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewListCategoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCategoriesLogic {
return &ListCategoriesLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ListCategoriesLogic) ListCategories(req *types.ListCategoriesReq) (resp *types.ListCategoriesResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -0,0 +1,30 @@
package logic
import (
"context"
"godemo/gateway/internal/svc"
"godemo/gateway/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UpdateCategoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUpdateCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateCategoryLogic {
return &UpdateCategoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateCategoryLogic) UpdateCategory(req *types.UpdateCategoryReq) (resp *types.BaseResp, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -1,6 +1,7 @@
package svc
import (
"godemo/category/category"
"godemo/gateway/internal/config"
"godemo/user/user"
@ -8,14 +9,15 @@ import (
)
type ServiceContext struct {
Config config.Config
UserRpc user.UserClient
Config config.Config
UserRpc user.UserClient
CategoryRpc category.CategoryClient
}
func NewServiceContext(c config.Config) *ServiceContext {
conn := zrpc.MustNewClient(c.UserRpc).Conn()
return &ServiceContext{
Config: c,
UserRpc: user.NewUserClient(conn),
Config: c,
UserRpc: user.NewUserClient(zrpc.MustNewClient(c.UserRpc).Conn()),
CategoryRpc: category.NewCategoryClient(zrpc.MustNewClient(c.CategoryRpc).Conn()),
}
}

View File

@ -3,6 +3,65 @@
package types
type BaseCategory struct {
ID string `json:"id"` // 分类ID (UUID)
SystemID string `json:"systemId"` // 所属系统ID
Name string `json:"name"` // 分类名称
Alias string `json:"alias"` // URL别名
ParentID string `json:"parentId"` // 父分类ID (可为空)
Description string `json:"description"` // 分类描述
CreatedAt string `json:"createdAt"` // 创建时间 (ISO8601)
UpdatedAt string `json:"updatedAt"` // 更新时间 (ISO8601)
}
type BaseResp struct {
Code int `json:"code"` // 状态码 (0=成功)
Message string `json:"message"` // 消息
}
type CategoryDetail struct {
BaseCategory
Parent *BaseCategory `json:"parent"` // 父分类信息
}
type CategoryDetailResp struct {
CategoryDetail
}
type CategoryTreeResp struct {
Root TreeNode `json:"root"` // 分类树根节点
}
type CreateCategoryReq struct {
SystemID string `json:"systemId" validate:"required"` // 所属系统ID
Name string `json:"name" validate:"required"` // 分类名称
Alias string `json:"alias,optional"` // URL别名
ParentID string `json:"parentId,optional"` // 父分类ID
Description string `json:"description,optional"` // 分类描述
}
type CreateCategoryResp struct {
ID string `json:"id"` // 新创建的分类ID
}
type DeleteCategoryReq struct {
ID string `path:"id"` // 分类ID
}
type GetCategoryReq struct {
ID string `path:"id"` // 分类ID
}
type GetCategoryTreeReq struct {
ID string `path:"id"` // 起始分类ID
}
type GetSystemCategoriesReq struct {
SystemID string `path:"system_id"` // 系统ID
ParentID string `form:"parentId,optional"` // 父分类ID (可选)
IncludeDescendants bool `form:"includeDescendants,default=true"` // 是否包含子分类
}
type GetUserInfoReq struct {
UserId string `path:"user_id"`
}
@ -15,6 +74,19 @@ type GetUserInfoResp struct {
Roles []string `json:"roles"`
}
type ListCategoriesReq struct {
SystemID string `form:"systemId,optional"` // 按系统ID过滤
ParentID string `form:"parentId,optional"` // 按父分类ID过滤
Name string `form:"name,optional"` // 按名称模糊搜索
Page int `form:"page,default=1"` // 页码
PageSize int `form:"pageSize,default=20"` // 每页数量
}
type ListCategoriesResp struct {
Total int64 `json:"total"` // 总数
List []BaseCategory `json:"list"` // 分类列表
}
type LoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
@ -42,3 +114,16 @@ type RegisterReq struct {
type RegisterResp struct {
UserId string `json:"user_id"`
}
type TreeNode struct {
BaseCategory
Children []*TreeNode `json:"children"` // 子分类列表
}
type UpdateCategoryReq struct {
ID string `path:"id"` // 分类ID
Name string `json:"name,optional"` // 新名称
Alias string `json:"alias,optional"` // 新别名
ParentID string `json:"parentId,optional"` // 新父分类ID
Description string `json:"description,optional"` // 新描述
}