diff --git a/api/v1/codegen/codegen.go b/api/v1/codegen/codegen.go new file mode 100644 index 0000000..5468fe8 --- /dev/null +++ b/api/v1/codegen/codegen.go @@ -0,0 +1,81 @@ +package codegen + +import ( + "sundynix-go/model/codegen" + "sundynix-go/model/commom/response" + "sundynix-go/service" + + "github.com/gin-gonic/gin" +) + +type CodegenApi struct{} + +var codegenService = service.ServiceGroupApp.CodegenServiceGroup.CodegenService + +// TestConnection +// @Tags 代码生成 +// @Summary 测试数据库连接 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body codegen.TestConnectionReq true "数据库连接配置" +// @Success 200 {object} response.Response "连接成功" +// @Router /api/codegen/testConnection [post] +func (a *CodegenApi) TestConnection(c *gin.Context) { + var req codegen.TestConnectionReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := codegenService.TestConnection(req.DbConfig); err != nil { + response.FailWithMsg("连接失败:"+err.Error(), c) + return + } + response.OkWithMsg("连接成功", c) +} + +// Preview +// @Tags 代码生成 +// @Summary 预览生成代码(不写文件) +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body codegen.PreviewReq true "代码生成配置" +// @Success 200 {object} response.Response{data=[]codegen.PreviewFile} "预览成功" +// @Router /api/codegen/preview [post] +func (a *CodegenApi) Preview(c *gin.Context) { + var req codegen.PreviewReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + files, err := codegenService.Preview(req.GenConfig) + if err != nil { + response.FailWithMsg("预览失败:"+err.Error(), c) + return + } + response.OkWithData(files, c) +} + +// Generate +// @Tags 代码生成 +// @Summary 生成并写入代码文件 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body codegen.GenConfig true "代码生成配置(含 outputDir)" +// @Success 200 {object} response.Response{data=codegen.GenResult} "生成成功" +// @Router /api/codegen/generate [post] +func (a *CodegenApi) Generate(c *gin.Context) { + var req codegen.GenConfig + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + result, err := codegenService.Generate(req) + if err != nil { + response.FailWithMsg("生成失败:"+err.Error(), c) + return + } + response.OkWithData(result, c) +} diff --git a/api/v1/codegen/enter.go b/api/v1/codegen/enter.go new file mode 100644 index 0000000..d2e18ec --- /dev/null +++ b/api/v1/codegen/enter.go @@ -0,0 +1,5 @@ +package codegen + +type ApiGroup struct { + CodegenApi +} diff --git a/api/v1/enter.go b/api/v1/enter.go index 9ee8a3f..887eb48 100644 --- a/api/v1/enter.go +++ b/api/v1/enter.go @@ -1,10 +1,16 @@ package v1 -import "sundynix-go/api/v1/system" +import ( + "sundynix-go/api/v1/codegen" + "sundynix-go/api/v1/order" + "sundynix-go/api/v1/system" +) var ApiGroupApp = new(ApiGroup) // ApiGroup 路由组 type ApiGroup struct { - SystemApiGroup system.ApiGroup + SystemApiGroup system.ApiGroup + CodegenApiGroup codegen.ApiGroup + OrderApiGroup order.ApiGroup } diff --git a/api/v1/order/enter.go b/api/v1/order/enter.go new file mode 100644 index 0000000..1fae9f2 --- /dev/null +++ b/api/v1/order/enter.go @@ -0,0 +1,7 @@ +package order + +type ApiGroup struct { + OrderApi + RefundApi + StockApi +} diff --git a/api/v1/order/order.go b/api/v1/order/order.go new file mode 100644 index 0000000..df6bb6a --- /dev/null +++ b/api/v1/order/order.go @@ -0,0 +1,134 @@ +package order + +import ( + "sundynix-go/global" + "sundynix-go/model/commom/response" + req "sundynix-go/model/order/request" + "sundynix-go/service" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type OrderApi struct{} + +var orderService = service.ServiceGroupApp.OrderServiceGroup.OrderService + +// Save +// @Tags 管理 +// @Summary 新增 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.SaveOrderReq true "新增参数" +// @Success 200 {object} response.Response "新增成功" +// @Router /order/order/save [post] +func (a *OrderApi) Save(c *gin.Context) { + var r req.SaveOrderReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := orderService.Save(r); err != nil { + global.Logger.Error("新增失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("新增成功", c) +} + +// Update +// @Tags 管理 +// @Summary 更新 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.UpdateOrderReq true "更新参数" +// @Success 200 {object} response.Response "更新成功" +// @Router /order/order/update [post] +func (a *OrderApi) Update(c *gin.Context) { + var r req.UpdateOrderReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := orderService.Update(r); err != nil { + global.Logger.Error("更新失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("更新成功", c) +} + +// Delete +// @Tags 管理 +// @Summary 删除(支持批量) +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body object{ids=[]string} true "id列表" +// @Success 200 {object} response.Response "删除成功" +// @Router /order/order/delete [post] +func (a *OrderApi) Delete(c *gin.Context) { + var req struct { + Ids []string `json:"ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := orderService.Delete(req.Ids); err != nil { + global.Logger.Error("删除失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("删除成功", c) +} + +// Detail +// @Tags 管理 +// @Summary 获取详情 +// @Security BasicAuth +// @Produce json +// @Param id query string true "ID" +// @Success 200 {object} response.Response "获取详情成功" +// @Router /order/order/detail [get] +func (a *OrderApi) Detail(c *gin.Context) { + id := c.Query("id") + data, err := orderService.Detail(id) + if err != nil { + global.Logger.Error("获取详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(data, c) +} + +// List +// @Tags 管理 +// @Summary 分页获取列表 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.ListOrderReq true "分页参数" +// @Success 200 {object} response.Response{data=response.PageResult} "获取列表成功" +// @Router /order/order/list [post] +func (a *OrderApi) List(c *gin.Context) { + var r req.ListOrderReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + list, total, err := orderService.List(r) + if err != nil { + global.Logger.Error("获取列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: r.Current, + PageSize: r.PageSize, + }, c) +} diff --git a/api/v1/order/refund.go b/api/v1/order/refund.go new file mode 100644 index 0000000..c81b12a --- /dev/null +++ b/api/v1/order/refund.go @@ -0,0 +1,134 @@ +package order + +import ( + "sundynix-go/global" + "sundynix-go/model/commom/response" + req "sundynix-go/model/order/request" + "sundynix-go/service" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type RefundApi struct{} + +var refundService = service.ServiceGroupApp.OrderServiceGroup.RefundService + +// Save +// @Tags 管理 +// @Summary 新增 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.SaveRefundReq true "新增参数" +// @Success 200 {object} response.Response "新增成功" +// @Router /order/refund/save [post] +func (a *RefundApi) Save(c *gin.Context) { + var r req.SaveRefundReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := refundService.Save(r); err != nil { + global.Logger.Error("新增失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("新增成功", c) +} + +// Update +// @Tags 管理 +// @Summary 更新 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.UpdateRefundReq true "更新参数" +// @Success 200 {object} response.Response "更新成功" +// @Router /order/refund/update [post] +func (a *RefundApi) Update(c *gin.Context) { + var r req.UpdateRefundReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := refundService.Update(r); err != nil { + global.Logger.Error("更新失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("更新成功", c) +} + +// Delete +// @Tags 管理 +// @Summary 删除(支持批量) +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body object{ids=[]string} true "id列表" +// @Success 200 {object} response.Response "删除成功" +// @Router /order/refund/delete [post] +func (a *RefundApi) Delete(c *gin.Context) { + var req struct { + Ids []string `json:"ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := refundService.Delete(req.Ids); err != nil { + global.Logger.Error("删除失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("删除成功", c) +} + +// Detail +// @Tags 管理 +// @Summary 获取详情 +// @Security BasicAuth +// @Produce json +// @Param id query string true "ID" +// @Success 200 {object} response.Response "获取详情成功" +// @Router /order/refund/detail [get] +func (a *RefundApi) Detail(c *gin.Context) { + id := c.Query("id") + data, err := refundService.Detail(id) + if err != nil { + global.Logger.Error("获取详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(data, c) +} + +// List +// @Tags 管理 +// @Summary 分页获取列表 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.ListRefundReq true "分页参数" +// @Success 200 {object} response.Response{data=response.PageResult} "获取列表成功" +// @Router /order/refund/list [post] +func (a *RefundApi) List(c *gin.Context) { + var r req.ListRefundReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + list, total, err := refundService.List(r) + if err != nil { + global.Logger.Error("获取列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: r.Current, + PageSize: r.PageSize, + }, c) +} diff --git a/api/v1/order/stock.go b/api/v1/order/stock.go new file mode 100644 index 0000000..d0d5b7b --- /dev/null +++ b/api/v1/order/stock.go @@ -0,0 +1,134 @@ +package order + +import ( + "sundynix-go/global" + "sundynix-go/model/commom/response" + req "sundynix-go/model/order/request" + "sundynix-go/service" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type StockApi struct{} + +var stockService = service.ServiceGroupApp.OrderServiceGroup.StockService + +// Save +// @Tags 管理 +// @Summary 新增 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.SaveStockReq true "新增参数" +// @Success 200 {object} response.Response "新增成功" +// @Router /order/stock/save [post] +func (a *StockApi) Save(c *gin.Context) { + var r req.SaveStockReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := stockService.Save(r); err != nil { + global.Logger.Error("新增失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("新增成功", c) +} + +// Update +// @Tags 管理 +// @Summary 更新 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.UpdateStockReq true "更新参数" +// @Success 200 {object} response.Response "更新成功" +// @Router /order/stock/update [post] +func (a *StockApi) Update(c *gin.Context) { + var r req.UpdateStockReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := stockService.Update(r); err != nil { + global.Logger.Error("更新失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("更新成功", c) +} + +// Delete +// @Tags 管理 +// @Summary 删除(支持批量) +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body object{ids=[]string} true "id列表" +// @Success 200 {object} response.Response "删除成功" +// @Router /order/stock/delete [post] +func (a *StockApi) Delete(c *gin.Context) { + var req struct { + Ids []string `json:"ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := stockService.Delete(req.Ids); err != nil { + global.Logger.Error("删除失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("删除成功", c) +} + +// Detail +// @Tags 管理 +// @Summary 获取详情 +// @Security BasicAuth +// @Produce json +// @Param id query string true "ID" +// @Success 200 {object} response.Response "获取详情成功" +// @Router /order/stock/detail [get] +func (a *StockApi) Detail(c *gin.Context) { + id := c.Query("id") + data, err := stockService.Detail(id) + if err != nil { + global.Logger.Error("获取详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(data, c) +} + +// List +// @Tags 管理 +// @Summary 分页获取列表 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.ListStockReq true "分页参数" +// @Success 200 {object} response.Response{data=response.PageResult} "获取列表成功" +// @Router /order/stock/list [post] +func (a *StockApi) List(c *gin.Context) { + var r req.ListStockReq + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + list, total, err := stockService.List(r) + if err != nil { + global.Logger.Error("获取列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: r.Current, + PageSize: r.PageSize, + }, c) +} diff --git a/api/v1/system/auth.go b/api/v1/system/auth.go index c61334b..7e97401 100644 --- a/api/v1/system/auth.go +++ b/api/v1/system/auth.go @@ -32,24 +32,30 @@ func (a *AuthApi) Login(c *gin.Context) { response.FailWithMsg(err.Error(), c) return } - if l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true) { - u := &system.User{Account: l.Account, Password: l.Password} - user, err := userService.Login(u) - if err != nil { - global.Logger.Error("登录失败! 用户名不存在或者密码错误!", zap.Error(err)) - response.FailWithMsg("用户名不存在或者密码错误", c) - return - } - a.GetToken(c, *user) + + // 验证码校验:enable-captcha=0 时跳过 + captchaOk := global.Config.System.EnableCaptcha == 0 || + (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) + + if !captchaOk { + response.FailWithMsg("验证码错误", c) return } - response.FailWithMsg("验证码错误", c) + + u := &system.User{Account: l.Account, Password: l.Password} + user, err := userService.Login(u) + if err != nil { + global.Logger.Error("登录失败! 用户名不存在或者密码错误!", zap.Error(err)) + response.FailWithMsg("用户名不存在或者密码错误", c) + return + } + a.GetToken(c, *user) } // Logout // @Tags 登录相关 // @Summary pc登出 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Success 200 {object} response.Response{msg=string} "登出成功" // @Router /api/auth/logout [get] diff --git a/api/v1/system/oss.go b/api/v1/system/oss.go index f166602..166ff2e 100644 --- a/api/v1/system/oss.go +++ b/api/v1/system/oss.go @@ -18,7 +18,7 @@ type OssApi struct { // UploadFile // @tags 文件相关 // @Summary 文件上传 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept multipart/form-data // @Produce application/json // @Param file formData file true "上传文件" @@ -44,7 +44,7 @@ func (o *OssApi) UploadFile(c *gin.Context) { // DeleteFile // @tags 文件相关 // @Summary 删除文件 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除文件" @@ -69,7 +69,7 @@ func (o *OssApi) DeleteFile(c *gin.Context) { // GetFileList // @tags 文件相关 // @Summary 文件列表 -// @Security ApiKeyAuth +// @Security BasicAuth // @Accept application/json // @Produce application/json // @Param data body sysReq.GetOssFileList true "文件列表" @@ -99,7 +99,7 @@ func (o *OssApi) GetFileList(c *gin.Context) { // Detail // @tags 文件相关 // @Summary 文件详情 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "文件id" // @Success 200 {object} response.Response{data=string} "文件详情" diff --git a/api/v1/system/sys_client.go b/api/v1/system/sys_client.go index b40ef90..a074250 100644 --- a/api/v1/system/sys_client.go +++ b/api/v1/system/sys_client.go @@ -17,7 +17,7 @@ type ClientApi struct { // SaveClient // @Tags 客户端管理 // @Summary 创建client -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.Client true "client" @@ -42,7 +42,7 @@ func (s *ClientApi) SaveClient(c *gin.Context) { // UpdateClient // @Tags 客户端管理 // @Summary 更新client -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.Client true "client" @@ -67,7 +67,7 @@ func (s *ClientApi) UpdateClient(c *gin.Context) { // GetClientList // @Tags 客户端管理 // @Summary 获取client列表 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body systemReq.GetClientList true "client" @@ -97,7 +97,7 @@ func (s *ClientApi) GetClientList(c *gin.Context) { // Delete // @Tags 客户端管理 // @Summary 删除client -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "ids" @@ -123,7 +123,7 @@ func (s *ClientApi) Delete(c *gin.Context) { // @Tags 客户端管理 // @Summary 获取client详情 // @Description id获取详情 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "id" // @Success 200 {object} response.Response{data=system.Client,msg=string} "获取client详情" diff --git a/api/v1/system/sys_menu.go b/api/v1/system/sys_menu.go index 8a90b87..7408bd5 100644 --- a/api/v1/system/sys_menu.go +++ b/api/v1/system/sys_menu.go @@ -17,7 +17,7 @@ type MenuApi struct { // SaveMenu // @Tags 菜单管理 // @Summary 新增菜单 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.Menu false "menu" @@ -42,7 +42,7 @@ func (m *MenuApi) SaveMenu(c *gin.Context) { // UpdateMenu // @Tags 菜单管理 // @Summary 更新菜单 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.Menu false "menu" @@ -68,7 +68,7 @@ func (m *MenuApi) UpdateMenu(c *gin.Context) { // @Tags 菜单管理 // @Summary 删除menu // @Description 删除menu -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "id" // @Success 200 {object} response.Response{msg=string} "详情" @@ -88,7 +88,7 @@ func (m *MenuApi) DeleteMenu(c *gin.Context) { // @Tags 菜单管理 // @Summary 获取menu详情 // @Description id获取详情 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "id" // @Success 200 {object} response.Response{data=system.Menu,msg=string} "详情" @@ -107,7 +107,7 @@ func (m *MenuApi) Detail(c *gin.Context) { // GetAllMenuTree // @Tags 菜单管理 // @Summary 获取所有菜单树 -// @Security ApiKeyAuth +// @Security BasicAuth // @Accept json // @Produce json // @Param data body systemReq.GetMenuTree true "菜单信息" @@ -132,7 +132,7 @@ func (m *MenuApi) GetAllMenuTree(c *gin.Context) { // GetUserMenuTree // @Tags 菜单管理 // @Summary 用户菜单数据 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce json // @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户菜单数据" // @Router /api/menu/getUserMenuTree [get] @@ -143,7 +143,7 @@ func (m *MenuApi) GetUserMenuTree(c *gin.Context) { // Route // @Tags 菜单管理 // @Summary 用户路由 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce json // @Success 200 {object} response.Response{data=[]system.Menu,msg=string} "用户route" // @Router /api/menu/route [get] diff --git a/api/v1/system/sys_role.go b/api/v1/system/sys_role.go index a97d52a..4691007 100644 --- a/api/v1/system/sys_role.go +++ b/api/v1/system/sys_role.go @@ -17,7 +17,7 @@ type RoleApi struct { // SaveRole // @tags 角色管理 // @Summary 创建角色 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept json // @Produce json // @Param data body system.Role true "角色信息" @@ -42,7 +42,7 @@ func (a *RoleApi) SaveRole(context *gin.Context) { // UpdateRole // @tags 角色管理 // @Summary 修改角色 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.Role true "角色ID" @@ -121,7 +121,7 @@ func (a *RoleApi) Delete(context *gin.Context) { // Detail // @Tags 角色管理 // @Summary 角色详情 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "id" // @Success 200 {object} response.Response{data=system.Role} "角色详情" @@ -140,7 +140,7 @@ func (a *RoleApi) Detail(context *gin.Context) { // GrantMenu // @tags 角色管理 // @Summary 授权菜单给角色 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body systemreq.GrantMenu true "授权菜单给角色" diff --git a/api/v1/system/sys_user.go b/api/v1/system/sys_user.go index 3e330e5..e9b02c9 100644 --- a/api/v1/system/sys_user.go +++ b/api/v1/system/sys_user.go @@ -17,7 +17,7 @@ type UserApi struct { // SaveUser // @tags 用户管理 // @Summary 新增用户 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept json // @Produce json // @Param data body system.User true "用户信息" @@ -41,7 +41,7 @@ func (u *UserApi) SaveUser(c *gin.Context) { // UpdateUser // @tags 用户管理 // @Summary 更新用户 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body system.User true "用户ID,用户信息" @@ -65,7 +65,7 @@ func (u *UserApi) UpdateUser(c *gin.Context) { // GetUserList // @tags 用户管理 // @Summary 获取用户列表 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body systemReq.GetUserList true "页码, 每页大小, 搜索条件" @@ -95,7 +95,7 @@ func (u *UserApi) GetUserList(c *gin.Context) { // Delete // @Tags 用户管理 // @Summary 删除用户 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body request.IdsReq true "批量删除用户" @@ -120,7 +120,7 @@ func (u *UserApi) Delete(c *gin.Context) { // Detail // @Tags 用户管理 // @Summary 获取用户详情 -// @Security ApiKeyAuth +// @Security BasicAuth // @Produce application/json // @Param id query string true "id" // @Success 200 {object} response.Response{data=system.User} "获取用户详情成功" @@ -139,7 +139,7 @@ func (u *UserApi) Detail(c *gin.Context) { // ChangePassword // @Tags 用户管理 // @Summary 修改密码 -// @Security ApiKeyAuth +// @Security BasicAuth // @Description 修改密码 // @accept json // @Produce application/json @@ -165,7 +165,7 @@ func (u *UserApi) ChangePassword(c *gin.Context) { // GrantRole // @Tags 用户管理 // @Summary 给用户分配角色 -// @Security ApiKeyAuth +// @Security BasicAuth // @accept application/json // @Produce application/json // @Param data body systemReq.GrantRole true "用户ID, 角色ID" diff --git a/initialize/gorm.go b/initialize/gorm.go index ceca5f6..4378059 100644 --- a/initialize/gorm.go +++ b/initialize/gorm.go @@ -5,6 +5,8 @@ import ( "sundynix-go/global" "sundynix-go/model/system" + "sundynix-go/model/order" + "go.uber.org/zap" "gorm.io/gorm" ) @@ -37,6 +39,12 @@ func MigrateTable() { system.Menu{}, system.SysOperationRecord{}, system.Oss{}, + + order.Order{}, + + order.Refund{}, + + order.Stock{}, ) if err != nil { global.Logger.Error("Migrate table failed,err:", zap.Error(err)) diff --git a/initialize/router.go b/initialize/router.go index fb414b0..d287833 100644 --- a/initialize/router.go +++ b/initialize/router.go @@ -26,6 +26,9 @@ func Routers() { // 系统组路由 systemRouter := router.GroupApp.System + codegenRouter := router.GroupApp.Codegen + + orderRouter := router.GroupApp.Order NeedAuthGroup := Router.Group(global.Config.System.RouterPrefix) PublicGroup := Router.Group(global.Config.System.RouterPrefix) @@ -40,10 +43,15 @@ func Routers() { { //需要鉴权的路由 - systemRouter.InitUserRouter(NeedAuthGroup) //用户相关 - systemRouter.InitClientRouter(NeedAuthGroup) //客户端相关 - systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关 - systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关 + systemRouter.InitUserRouter(NeedAuthGroup) //用户相关 + systemRouter.InitClientRouter(NeedAuthGroup) //客户端相关 + systemRouter.InitRoleRouter(NeedAuthGroup) //角色相关 + systemRouter.InitMenuRouter(NeedAuthGroup) //菜单相关 + codegenRouter.InitCodegenRouter(NeedAuthGroup) //代码生成 + orderRouter.InitOrderRouter(NeedAuthGroup) + orderRouter.InitRefundRouter(NeedAuthGroup) + orderRouter.InitStockRouter(NeedAuthGroup) + } address := fmt.Sprintf(":%d", global.Config.System.Addr) diff --git a/main.go b/main.go index 837fd88..c32f073 100644 --- a/main.go +++ b/main.go @@ -13,9 +13,7 @@ import ( // @title RBAC Swagger API接口文档 // @version v1.0.0 // @description 使用gin+gorm进行极速开发的全栈开发基础平台 -// @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization +// @securityDefinitions.basic BasicAuth // @BasePath / func main() { //初始化viper @@ -28,7 +26,9 @@ func main() { global.DB = initialize.Gorm() //redis连接 initialize.Redis() - global.Redis.Set(context.Background(), "test", 0, -1) + if global.Redis != nil { + global.Redis.Set(context.Background(), "test", 0, -1) + } //迁移数据库 if global.DB != nil { diff --git a/model/codegen/codegen_request.go b/model/codegen/codegen_request.go new file mode 100644 index 0000000..74830f2 --- /dev/null +++ b/model/codegen/codegen_request.go @@ -0,0 +1,67 @@ +package codegen + +// GenConfig 代码生成完整配置 +type GenConfig struct { + DbConfig DbConfig `json:"dbConfig"` + OutputDir string `json:"outputDir"` // 后端输出目录,绝对路径,为空则输出到项目根目录 + FrontendOutputDir string `json:"frontendOutputDir"` // 前端输出目录,绝对路径,为空则跳过前端生成 + Overwrite bool `json:"overwrite"` // true=覆盖已有文件 false=增量模式(跳过已存在文件) + Modules []Module `json:"modules"` +} + +// DbConfig 目标数据库连接配置 +type DbConfig struct { + Host string `json:"host"` + Port string `json:"port"` + User string `json:"user"` + Password string `json:"password"` + DbName string `json:"dbName"` + DbType string `json:"dbType"` // mysql / postgres / sqlite +} + +// Module 模块定义(对应一组功能) +type Module struct { + Name string `json:"name"` // 模块名(PascalCase),如 "Order" + PackageName string `json:"packageName"` // 包名(lowercase),如 "order" + Description string `json:"description"` + Features []Feature `json:"features"` +} + +// Feature 功能单元(对应一张表 / 一个实体) +type Feature struct { + Name string `json:"name"` // 实体名(PascalCase),如 "Product" + TableName string `json:"tableName"` // 数据库表名,如 "product" + Comment string `json:"comment"` + Fields []Field `json:"fields"` + Relations []Relation `json:"relations"` +} + +// Field 字段定义 +type Field struct { + Name string `json:"name"` // Go 字段名(PascalCase),如 "OrderNo" + ColumnName string `json:"columnName"` // 数据库列名(snake_case),如 "order_no" + Type string `json:"type"` // Go 类型,如 string / int / float64 / bool / time.Time + GormTag string `json:"gormTag"` // gorm tag 附加内容,如 "size:100;not null" + JsonTag string `json:"jsonTag"` // json tag,如 "orderNo" + Comment string `json:"comment"` // 字段注释 + Required bool `json:"required"` // 是否必填(影响 not null) +} + +// Relation 表关系定义 +type Relation struct { + Type string `json:"type"` // OneToOne / OneToMany / ManyToMany + TargetFeature string `json:"targetFeature"` // 目标实体名(PascalCase) + ForeignKey string `json:"foreignKey"` // 外键字段名,如 "UserId" + JoinTable string `json:"joinTable"` // ManyToMany 中间表名 + FieldName string `json:"fieldName"` // Go 中关联字段名,如 "Tags" +} + +// TestConnectionReq 测试连接请求 +type TestConnectionReq struct { + DbConfig DbConfig `json:"dbConfig"` +} + +// PreviewReq 预览代码请求 +type PreviewReq struct { + GenConfig +} diff --git a/model/codegen/codegen_response.go b/model/codegen/codegen_response.go new file mode 100644 index 0000000..69fde76 --- /dev/null +++ b/model/codegen/codegen_response.go @@ -0,0 +1,15 @@ +package codegen + +// PreviewFile 预览的单个文件 +type PreviewFile struct { + FilePath string `json:"filePath"` // 相对于输出目录的路径 + Content string `json:"content"` // 文件内容 + AlwaysOverwrite bool `json:"alwaysOverwrite,omitempty"` // 聚合文件(如 enter.go),始终覆盖 +} + +// GenResult 代码生成结果 +type GenResult struct { + OutputDir string `json:"outputDir"` // 实际输出目录 + Files []PreviewFile `json:"files"` // 生成的文件列表 + Message string `json:"message"` // 生成结果说明 +} diff --git a/model/order/order.go b/model/order/order.go new file mode 100644 index 0000000..fb3d0e5 --- /dev/null +++ b/model/order/order.go @@ -0,0 +1,13 @@ +package order + +import "sundynix-go/global" + +// Order +type Order struct { + global.BaseModel + UserId string `gorm:"column:user_id;size:50" json:"userId"` // +} + +func (Order) TableName() string { + return "sundynix_order" +} diff --git a/model/order/refund.go b/model/order/refund.go new file mode 100644 index 0000000..7d21c72 --- /dev/null +++ b/model/order/refund.go @@ -0,0 +1,13 @@ +package order + +import "sundynix-go/global" + +// Refund +type Refund struct { + global.BaseModel + UserId string `gorm:"column:user_id;size:50" json:"userId"` // +} + +func (Refund) TableName() string { + return "sundynix_refund" +} diff --git a/model/order/request/request.go b/model/order/request/request.go new file mode 100644 index 0000000..428d856 --- /dev/null +++ b/model/order/request/request.go @@ -0,0 +1,41 @@ +package request + +// ---- ---- + +// SaveRefundReq 新增请求 +type SaveRefundReq struct { + UserId string `json:"userId" form:"userId"` // +} + +// UpdateRefundReq 更新请求 +type UpdateRefundReq struct { + Id string `json:"id" binding:"required"` // ID + UserId string `json:"userId" form:"userId"` // +} + +// ListRefundReq 分页查询请求 +type ListRefundReq struct { + Current int `json:"current" form:"current"` // 页码 + PageSize int `json:"pageSize" form:"pageSize"` // 每页大小 + Keyword string `json:"keyword" form:"keyword"` // 关键字搜索 +} + +// ---- ---- + +// SaveStockReq 新增请求 +type SaveStockReq struct { + Amount int64 `json:"amount" form:"amount"` // +} + +// UpdateStockReq 更新请求 +type UpdateStockReq struct { + Id string `json:"id" binding:"required"` // ID + Amount int64 `json:"amount" form:"amount"` // +} + +// ListStockReq 分页查询请求 +type ListStockReq struct { + Current int `json:"current" form:"current"` // 页码 + PageSize int `json:"pageSize" form:"pageSize"` // 每页大小 + Keyword string `json:"keyword" form:"keyword"` // 关键字搜索 +} diff --git a/model/order/stock.go b/model/order/stock.go new file mode 100644 index 0000000..decc07b --- /dev/null +++ b/model/order/stock.go @@ -0,0 +1,13 @@ +package order + +import "sundynix-go/global" + +// Stock +type Stock struct { + global.BaseModel + Amount int64 `gorm:"column:amount" json:"amount"` // +} + +func (Stock) TableName() string { + return "sundynix_stock" +} diff --git a/router/codegen/codegen_router.go b/router/codegen/codegen_router.go new file mode 100644 index 0000000..2dd4e8d --- /dev/null +++ b/router/codegen/codegen_router.go @@ -0,0 +1,14 @@ +package codegen + +import "github.com/gin-gonic/gin" + +type CodegenRouter struct{} + +func (r *CodegenRouter) InitCodegenRouter(Router *gin.RouterGroup) { + codegenRouter := Router.Group("codegen") + { + codegenRouter.POST("testConnection", codegenApi.TestConnection) + codegenRouter.POST("preview", codegenApi.Preview) + codegenRouter.POST("generate", codegenApi.Generate) + } +} diff --git a/router/codegen/enter.go b/router/codegen/enter.go new file mode 100644 index 0000000..2d02047 --- /dev/null +++ b/router/codegen/enter.go @@ -0,0 +1,11 @@ +package codegen + +import v1 "sundynix-go/api/v1" + +type RouterGroup struct { + CodegenRouter +} + +var ( + codegenApi = v1.ApiGroupApp.CodegenApiGroup.CodegenApi +) diff --git a/router/enter.go b/router/enter.go index bd2f44c..bae07d4 100644 --- a/router/enter.go +++ b/router/enter.go @@ -1,6 +1,8 @@ package router import ( + "sundynix-go/router/codegen" + "sundynix-go/router/order" "sundynix-go/router/system" ) @@ -8,5 +10,7 @@ var GroupApp = new(Group) // Group 路由组 type Group struct { - System system.RouterGroup + System system.RouterGroup + Codegen codegen.RouterGroup + Order order.RouterGroup } diff --git a/router/order/enter.go b/router/order/enter.go new file mode 100644 index 0000000..fa9d47d --- /dev/null +++ b/router/order/enter.go @@ -0,0 +1,15 @@ +package order + +import v1 "sundynix-go/api/v1" + +type RouterGroup struct { + OrderRouter + RefundRouter + StockRouter +} + +var ( + orderApi = v1.ApiGroupApp.OrderApiGroup.OrderApi + refundApi = v1.ApiGroupApp.OrderApiGroup.RefundApi + stockApi = v1.ApiGroupApp.OrderApiGroup.StockApi +) diff --git a/router/order/order_router.go b/router/order/order_router.go new file mode 100644 index 0000000..3abc563 --- /dev/null +++ b/router/order/order_router.go @@ -0,0 +1,16 @@ +package order + +import "github.com/gin-gonic/gin" + +type OrderRouter struct{} + +func (r *OrderRouter) InitOrderRouter(Router *gin.RouterGroup) { + orderRouter := Router.Group("order") + { + orderRouter.POST("save", orderApi.Save) + orderRouter.POST("update", orderApi.Update) + orderRouter.POST("delete", orderApi.Delete) + orderRouter.GET("detail", orderApi.Detail) + orderRouter.POST("list", orderApi.List) + } +} diff --git a/router/order/refund_router.go b/router/order/refund_router.go new file mode 100644 index 0000000..d477be5 --- /dev/null +++ b/router/order/refund_router.go @@ -0,0 +1,16 @@ +package order + +import "github.com/gin-gonic/gin" + +type RefundRouter struct{} + +func (r *RefundRouter) InitRefundRouter(Router *gin.RouterGroup) { + refundRouter := Router.Group("refund") + { + refundRouter.POST("save", refundApi.Save) + refundRouter.POST("update", refundApi.Update) + refundRouter.POST("delete", refundApi.Delete) + refundRouter.GET("detail", refundApi.Detail) + refundRouter.POST("list", refundApi.List) + } +} diff --git a/router/order/stock_router.go b/router/order/stock_router.go new file mode 100644 index 0000000..45e6f49 --- /dev/null +++ b/router/order/stock_router.go @@ -0,0 +1,16 @@ +package order + +import "github.com/gin-gonic/gin" + +type StockRouter struct{} + +func (r *StockRouter) InitStockRouter(Router *gin.RouterGroup) { + stockRouter := Router.Group("stock") + { + stockRouter.POST("save", stockApi.Save) + stockRouter.POST("update", stockApi.Update) + stockRouter.POST("delete", stockApi.Delete) + stockRouter.GET("detail", stockApi.Detail) + stockRouter.POST("list", stockApi.List) + } +} diff --git a/service/codegen/codegen_register.go b/service/codegen/codegen_register.go new file mode 100644 index 0000000..e9c0cd4 --- /dev/null +++ b/service/codegen/codegen_register.go @@ -0,0 +1,211 @@ +package codegen + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + codegenModel "sundynix-go/model/codegen" +) + +// autoRegister 在代码生成后自动修改项目的注册文件 +// 包括:initialize/gorm.go, initialize/router.go, api/v1/enter.go, router/enter.go, service/enter.go +// 以及子模块的 enter.go 文件(增量追加新 feature) +func (s *CodegenService) autoRegister(outputDir string, config codegenModel.GenConfig) error { + for _, module := range config.Modules { + if strings.TrimSpace(module.PackageName) == "" || strings.TrimSpace(module.Name) == "" { + continue + } + + pkg := module.PackageName + pascalModule := module.Name + + // 判断是否新模块:检查 router/{pkg}/enter.go 是否已存在 + routerEnterPath := filepath.Join(outputDir, "router", pkg, "enter.go") + isNewModule := !fileExistsOnDisk(routerEnterPath) + + if isNewModule { + // 新模块:注册到 5 个顶级文件 + if err := s.registerGorm(outputDir, module); err != nil { + return fmt.Errorf("注册 gorm 迁移失败: %w", err) + } + if err := s.registerTopEnter(outputDir, pkg, pascalModule); err != nil { + return fmt.Errorf("注册顶级 enter 失败: %w", err) + } + if err := s.registerRouter(outputDir, pkg, pascalModule, module.Features); err != nil { + return fmt.Errorf("注册路由初始化失败: %w", err) + } + } else { + // 已有模块:增量注册新 feature + if err := s.registerGormIncremental(outputDir, module); err != nil { + return fmt.Errorf("增量注册 gorm 迁移失败: %w", err) + } + if err := s.registerRouterIncremental(outputDir, pkg, pascalModule, module.Features); err != nil { + return fmt.Errorf("增量注册路由失败: %w", err) + } + } + } + return nil +} + +// registerGorm 向 initialize/gorm.go 的 AutoMigrate 中追加所有 feature +func (s *CodegenService) registerGorm(outputDir string, module codegenModel.Module) error { + gormPath := filepath.Join(outputDir, "initialize", "gorm.go") + content, err := os.ReadFile(gormPath) + if err != nil { + return fmt.Errorf("读取 %s 失败: %w", gormPath, err) + } + text := string(content) + pkg := module.PackageName + + // 1. 添加 import + importLine := fmt.Sprintf("\t\"sundynix-go/model/%s\"", pkg) + if !strings.Contains(text, importLine) { + text = strings.Replace(text, "\"go.uber.org/zap\"", importLine+"\n\n\t\"go.uber.org/zap\"", 1) + } + + // 2. 在 AutoMigrate 闭合 ) 前插入 model + for _, feature := range module.Features { + if strings.TrimSpace(feature.Name) == "" { + continue + } + modelEntry := fmt.Sprintf("\t\t%s.%s{},", pkg, feature.Name) + if !strings.Contains(text, modelEntry) { + // 找最后一个 ) 之前的位置(AutoMigrate 结束) + closeIdx := strings.LastIndex(text, "\t)") + if closeIdx > 0 { + text = text[:closeIdx] + "\n" + modelEntry + "\n" + text[closeIdx:] + } + } + } + + return os.WriteFile(gormPath, []byte(text), 0644) +} + +// registerGormIncremental 增量模式:只追加新 feature 到 AutoMigrate +func (s *CodegenService) registerGormIncremental(outputDir string, module codegenModel.Module) error { + return s.registerGorm(outputDir, module) // 逻辑相同,Contains 检查保证幂等 +} + +// registerTopEnter 向 api/v1/enter.go、router/enter.go、service/enter.go 追加新模块 +func (s *CodegenService) registerTopEnter(outputDir string, pkg, pascalModule string) error { + // --- api/v1/enter.go --- + apiEnterPath := filepath.Join(outputDir, "api", "v1", "enter.go") + if err := appendToEnterFile(apiEnterPath, + fmt.Sprintf("\t\"sundynix-go/api/v1/%s\"", pkg), + fmt.Sprintf("\t%sApiGroup %s.ApiGroup", pascalModule, pkg), + ); err != nil { + return err + } + + // --- router/enter.go --- + routerEnterPath := filepath.Join(outputDir, "router", "enter.go") + if err := appendToEnterFile(routerEnterPath, + fmt.Sprintf("\t\"sundynix-go/router/%s\"", pkg), + fmt.Sprintf("\t%s %s.RouterGroup", pascalModule, pkg), + ); err != nil { + return err + } + + // --- service/enter.go --- + svcEnterPath := filepath.Join(outputDir, "service", "enter.go") + if err := appendToEnterFile(svcEnterPath, + fmt.Sprintf("\t\"sundynix-go/service/%s\"", pkg), + fmt.Sprintf("\t%sServiceGroup %s.ServiceGroup", pascalModule, pkg), + ); err != nil { + return err + } + + return nil +} + +// appendToEnterFile 向 enter.go 文件追加 import 和 struct 字段 +func appendToEnterFile(filePath, importLine, structField string) error { + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("读取 %s 失败: %w", filePath, err) + } + text := string(content) + + // 添加 import(如果不存在) + if !strings.Contains(text, importLine) { + // 在最后一个 import 之前插入 + lastImportIdx := strings.LastIndex(text, "\t\"") + if lastImportIdx > 0 { + // 找这行的换行符 + nlIdx := strings.Index(text[lastImportIdx:], "\n") + if nlIdx > 0 { + insertPos := lastImportIdx + nlIdx + 1 + text = text[:insertPos] + importLine + "\n" + text[insertPos:] + } + } + } + + // 添加 struct 字段(如果不存在) + if !strings.Contains(text, structField) { + // 在 struct 的 } 之前插入 + closeBrace := strings.Index(text, "\n}") + if closeBrace > 0 { + text = text[:closeBrace] + "\n" + structField + text[closeBrace:] + } + } + + return os.WriteFile(filePath, []byte(text), 0644) +} + +// registerRouter 向 initialize/router.go 追加路由注册代码 +func (s *CodegenService) registerRouter(outputDir, pkg, pascalModule string, features []codegenModel.Feature) error { + routerPath := filepath.Join(outputDir, "initialize", "router.go") + content, err := os.ReadFile(routerPath) + if err != nil { + return fmt.Errorf("读取 %s 失败: %w", routerPath, err) + } + text := string(content) + + // 1. 添加变量声明:xxxRouter := router.GroupApp.Xxx + varLine := fmt.Sprintf("\t%sRouter := router.GroupApp.%s", lowerFirst(pkg), pascalModule) + if !strings.Contains(text, varLine) { + // 在 NeedAuthGroup 行之前插入 + needAuthIdx := strings.Index(text, "NeedAuthGroup := Router.Group") + if needAuthIdx > 0 { + text = text[:needAuthIdx] + varLine + "\n\n\t" + text[needAuthIdx:] + } + } + + // 2. 添加路由注册:xxxRouter.InitXxxRouter(NeedAuthGroup) + for _, feature := range features { + if strings.TrimSpace(feature.Name) == "" { + continue + } + routerCall := fmt.Sprintf("\t\t%sRouter.Init%sRouter(NeedAuthGroup)", lowerFirst(pkg), feature.Name) + if !strings.Contains(text, routerCall) { + // 在最后一个 Init...Router 行之后插入 + lastInitIdx := strings.LastIndex(text, "Router(NeedAuthGroup)") + if lastInitIdx > 0 { + nlIdx := strings.Index(text[lastInitIdx:], "\n") + if nlIdx > 0 { + insertPos := lastInitIdx + nlIdx + comment := fmt.Sprintf(" //%s", feature.Comment) + if feature.Comment == "" { + comment = "" + } + text = text[:insertPos] + "\n" + routerCall + comment + text[insertPos:] + } + } + } + } + + return os.WriteFile(routerPath, []byte(text), 0644) +} + +// registerRouterIncremental 增量模式:只追加新 feature 的路由注册 +func (s *CodegenService) registerRouterIncremental(outputDir, pkg, pascalModule string, features []codegenModel.Feature) error { + return s.registerRouter(outputDir, pkg, pascalModule, features) // Contains 检查保证幂等 +} + +// fileExistsOnDisk 检查文件是否已存在 +func fileExistsOnDisk(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/service/codegen/codegen_service.go b/service/codegen/codegen_service.go new file mode 100644 index 0000000..fb0d1b7 --- /dev/null +++ b/service/codegen/codegen_service.go @@ -0,0 +1,979 @@ +package codegen + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + codegenModel "sundynix-go/model/codegen" + + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type CodegenService struct{} + +var CodegenServiceApp = new(CodegenService) + +// ---------------------------------------- +// 模板:model 实体(继承 global.BaseModel) +// ---------------------------------------- + +const modelTmpl = `package {{.PackageName}} + +import "sundynix-go/global" +{{if .HasTimeField}}import "time" +{{end}} +// {{.Feature.Name}} {{.Feature.Comment}} +type {{.Feature.Name}} struct { + global.BaseModel +{{- range .Feature.Fields}} + {{.Name}} {{.Type}} ` + "`" + `gorm:"column:{{.ColumnName}}{{if .GormTag}};{{.GormTag}}{{end}}" json:"{{.JsonTag}}"` + "`" + ` // {{.Comment}} +{{- end}} +{{- range .Feature.Relations}} +{{- if eq .Type "OneToOne"}} + {{.FieldName}} *{{.TargetFeature}} ` + "`" + `gorm:"foreignKey:{{.ForeignKey}}" json:"{{lowerFirst .FieldName}}"` + "`" + ` +{{- else if eq .Type "OneToMany"}} + {{.FieldName}} []{{.TargetFeature}} ` + "`" + `gorm:"foreignKey:{{.ForeignKey}}" json:"{{lowerFirst .FieldName}}"` + "`" + ` +{{- else if eq .Type "ManyToMany"}} + {{.FieldName}} []{{.TargetFeature}} ` + "`" + `gorm:"many2many:{{.JoinTable}};" json:"{{lowerFirst .FieldName}}"` + "`" + ` +{{- end}} +{{- end}} +} + +func ({{.Feature.Name}}) TableName() string { + return "{{.Feature.TableName}}" +} +` + +// ---------------------------------------- +// 模板:request 结构体(每个模块一个文件,包含所有实体的请求结构体) +// ---------------------------------------- + +const requestTmpl = `package request +{{range .Features}} +// ---- {{.Comment}} ---- + +// Save{{.Name}}Req 新增{{.Comment}}请求 +type Save{{.Name}}Req struct { +{{- range .Fields}} + {{.Name}} {{.Type}} ` + "`" + `json:"{{.JsonTag}}" form:"{{.JsonTag}}"{{if .Required}} binding:"required"{{end}}` + "`" + ` // {{.Comment}} +{{- end}} +} + +// Update{{.Name}}Req 更新{{.Comment}}请求 +type Update{{.Name}}Req struct { + Id string ` + "`" + `json:"id" binding:"required"` + "`" + ` // ID +{{- range .Fields}} + {{.Name}} {{.Type}} ` + "`" + `json:"{{.JsonTag}}" form:"{{.JsonTag}}"` + "`" + ` // {{.Comment}} +{{- end}} +} + +// List{{.Name}}Req 分页查询{{.Comment}}请求 +type List{{.Name}}Req struct { + Current int ` + "`" + `json:"current" form:"current"` + "`" + ` // 页码 + PageSize int ` + "`" + `json:"pageSize" form:"pageSize"` + "`" + ` // 每页大小 + Keyword string ` + "`" + `json:"keyword" form:"keyword"` + "`" + ` // 关键字搜索 +} +{{end}}` + +// ---------------------------------------- +// 模板:service 层(使用 request 包的结构体) +// ---------------------------------------- + +const serviceTmpl = `package {{.PackageName}} + +import ( + "sundynix-go/global" + model "sundynix-go/model/{{.PackageName}}" + req "sundynix-go/model/{{.PackageName}}/request" +) + +type {{.Feature.Name}}Service struct{} + +var {{.Feature.Name}}ServiceApp = new({{.Feature.Name}}Service) + +func (s *{{.Feature.Name}}Service) Save(r req.Save{{.Feature.Name}}Req) error { + data := model.{{.Feature.Name}}{ +{{- range .Feature.Fields}} + {{.Name}}: r.{{.Name}}, +{{- end}} + } + return global.DB.Create(&data).Error +} + +func (s *{{.Feature.Name}}Service) Update(r req.Update{{.Feature.Name}}Req) error { + updates := map[string]any{ +{{- range .Feature.Fields}} + "{{.ColumnName}}": r.{{.Name}}, +{{- end}} + } + return global.DB.Model(&model.{{.Feature.Name}}{}).Where("id = ?", r.Id).Updates(updates).Error +} + +func (s *{{.Feature.Name}}Service) Delete(ids []string) error { + return global.DB.Where("id IN ?", ids).Delete(&model.{{.Feature.Name}}{}).Error +} + +func (s *{{.Feature.Name}}Service) Detail(id string) (data *model.{{.Feature.Name}}, err error) { + var record model.{{.Feature.Name}} + err = global.DB.Where("id = ?", id).First(&record).Error + return &record, err +} + +func (s *{{.Feature.Name}}Service) List(r req.List{{.Feature.Name}}Req) (list []model.{{.Feature.Name}}, total int64, err error) { + if r.PageSize <= 0 { + r.PageSize = 10 + } + if r.Current <= 0 { + r.Current = 1 + } + db := global.DB.Model(&model.{{.Feature.Name}}{}) + if r.Keyword != "" { + db = db.Where("id LIKE ?", "%"+r.Keyword+"%") + } + if err = db.Count(&total).Error; err != nil { + return + } + err = db.Limit(r.PageSize).Offset(r.PageSize * (r.Current - 1)).Find(&list).Error + return +} +` + +// ---------------------------------------- +// 模板:service enter.go +// ---------------------------------------- + +const serviceEnterTmpl = `package {{.PackageName}} + +type ServiceGroup struct { +{{- range .Features}} + {{.Name}}Service +{{- end}} +} +` + +// ---------------------------------------- +// 模板:api handler 层(使用 request 包的结构体) +// ---------------------------------------- + +const apiTmpl = `package {{.PackageName}} + +import ( + "sundynix-go/global" + "sundynix-go/model/commom/response" + req "sundynix-go/model/{{.PackageName}}/request" + "sundynix-go/service" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type {{.Feature.Name}}Api struct{} + +var {{.LowerName}}Service = service.ServiceGroupApp.{{.PascalModuleName}}ServiceGroup.{{.Feature.Name}}Service + +// Save +// @Tags {{.Feature.Comment}}管理 +// @Summary 新增{{.Feature.Comment}} +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.Save{{.Feature.Name}}Req true "新增{{.Feature.Comment}}参数" +// @Success 200 {object} response.Response "新增成功" +// @Router /{{.RouterPrefix}}/save [post] +func (a *{{.Feature.Name}}Api) Save(c *gin.Context) { + var r req.Save{{.Feature.Name}}Req + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := {{.LowerName}}Service.Save(r); err != nil { + global.Logger.Error("新增{{.Feature.Comment}}失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("新增成功", c) +} + +// Update +// @Tags {{.Feature.Comment}}管理 +// @Summary 更新{{.Feature.Comment}} +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.Update{{.Feature.Name}}Req true "更新{{.Feature.Comment}}参数" +// @Success 200 {object} response.Response "更新成功" +// @Router /{{.RouterPrefix}}/update [post] +func (a *{{.Feature.Name}}Api) Update(c *gin.Context) { + var r req.Update{{.Feature.Name}}Req + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := {{.LowerName}}Service.Update(r); err != nil { + global.Logger.Error("更新{{.Feature.Comment}}失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("更新成功", c) +} + +// Delete +// @Tags {{.Feature.Comment}}管理 +// @Summary 删除{{.Feature.Comment}}(支持批量) +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body object{ids=[]string} true "id列表" +// @Success 200 {object} response.Response "删除成功" +// @Router /{{.RouterPrefix}}/delete [post] +func (a *{{.Feature.Name}}Api) Delete(c *gin.Context) { + var req struct { + Ids []string ` + "`" + `json:"ids" binding:"required,min=1"` + "`" + ` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + if err := {{.LowerName}}Service.Delete(req.Ids); err != nil { + global.Logger.Error("删除{{.Feature.Comment}}失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithMsg("删除成功", c) +} + +// Detail +// @Tags {{.Feature.Comment}}管理 +// @Summary 获取{{.Feature.Comment}}详情 +// @Security BasicAuth +// @Produce json +// @Param id query string true "ID" +// @Success 200 {object} response.Response "获取详情成功" +// @Router /{{.RouterPrefix}}/detail [get] +func (a *{{.Feature.Name}}Api) Detail(c *gin.Context) { + id := c.Query("id") + data, err := {{.LowerName}}Service.Detail(id) + if err != nil { + global.Logger.Error("获取{{.Feature.Comment}}详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(data, c) +} + +// List +// @Tags {{.Feature.Comment}}管理 +// @Summary 分页获取{{.Feature.Comment}}列表 +// @Security BasicAuth +// @accept json +// @Produce json +// @Param data body req.List{{.Feature.Name}}Req true "分页参数" +// @Success 200 {object} response.Response{data=response.PageResult} "获取列表成功" +// @Router /{{.RouterPrefix}}/list [post] +func (a *{{.Feature.Name}}Api) List(c *gin.Context) { + var r req.List{{.Feature.Name}}Req + if err := c.ShouldBindJSON(&r); err != nil { + response.FailWithMsg("参数错误:"+err.Error(), c) + return + } + list, total, err := {{.LowerName}}Service.List(r) + if err != nil { + global.Logger.Error("获取{{.Feature.Comment}}列表失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: r.Current, + PageSize: r.PageSize, + }, c) +} +` + +// ---------------------------------------- +// 模板:api enter.go +// ---------------------------------------- + +const apiEnterTmpl = `package {{.PackageName}} + +type ApiGroup struct { +{{- range .Features}} + {{.Name}}Api +{{- end}} +} +` + +// ---------------------------------------- +// 模板:router +// ---------------------------------------- + +const routerTmpl = `package {{.PackageName}} + +import "github.com/gin-gonic/gin" + +type {{.Feature.Name}}Router struct{} + +func (r *{{.Feature.Name}}Router) Init{{.Feature.Name}}Router(Router *gin.RouterGroup) { + {{.LowerName}}Router := Router.Group("{{.RouterGroup}}") + { + {{.LowerName}}Router.POST("save", {{.LowerName}}Api.Save) + {{.LowerName}}Router.POST("update", {{.LowerName}}Api.Update) + {{.LowerName}}Router.POST("delete", {{.LowerName}}Api.Delete) + {{.LowerName}}Router.GET("detail", {{.LowerName}}Api.Detail) + {{.LowerName}}Router.POST("list", {{.LowerName}}Api.List) + } +} +` + +// ---------------------------------------- +// 模板:router enter.go +// ---------------------------------------- + +const routerEnterTmpl = `package {{.PackageName}} + +import v1 "sundynix-go/api/v1" + +type RouterGroup struct { +{{- range .Features}} + {{.Name}}Router +{{- end}} +} + +var ( +{{- range .Features}} + {{lowerFirst .Name}}Api = v1.ApiGroupApp.{{.PascalModuleName}}ApiGroup.{{.Name}}Api +{{- end}} +) +` + +// ---------------------------------------- +// 前端模板:TypeScript 类型定义 +// ---------------------------------------- + +const feTsTypeTmpl = `// {{.Module.Name}} 模块类型定义(自动生成,请勿手动修改) +import type { BaseModel } from '@/types' +{{range .Features}} +// ---- {{.Comment}} ---- + +/** {{.Comment}}实体 */ +export interface {{.Name}} extends BaseModel { +{{- range .Fields}} + {{.JsonTag}}: {{goTypeToTs .Type}} // {{.Comment}} +{{- end}} +} + +/** 新增{{.Comment}}请求 */ +export interface Save{{.Name}}Req { +{{- range .Fields}} + {{.JsonTag}}{{if not .Required}}?{{end}}: {{goTypeToTs .Type}} // {{.Comment}} +{{- end}} +} + +/** 更新{{.Comment}}请求 */ +export interface Update{{.Name}}Req { + id: string +{{- range .Fields}} + {{.JsonTag}}?: {{goTypeToTs .Type}} // {{.Comment}} +{{- end}} +} + +/** 分页查询{{.Comment}}请求 */ +export interface List{{.Name}}Req { + current?: number + pageSize?: number + keyword?: string +} +{{end}}` + +// ---------------------------------------- +// 前端模板:API service (Axios) +// ---------------------------------------- + +const feApiTmpl = `import { get, post } from '@/lib/request' +import type { + {{.Feature.Name}}, + Save{{.Feature.Name}}Req, + Update{{.Feature.Name}}Req, + List{{.Feature.Name}}Req, +} from '@/types/{{.Module.PackageName}}' +import type { PageResult } from '@/types' + +const BASE = '/{{.Module.PackageName}}/{{.SnakeName}}' + +export const {{lowerFirst .Feature.Name}}Api = { + save: (data: Save{{.Feature.Name}}Req) => + post(` + "`" + `${BASE}/save` + "`" + `, data), + + update: (data: Update{{.Feature.Name}}Req) => + post(` + "`" + `${BASE}/update` + "`" + `, data), + + delete: (ids: string[]) => + post(` + "`" + `${BASE}/delete` + "`" + `, { ids }), + + detail: (id: string) => + get<{{.Feature.Name}}>(` + "`" + `${BASE}/detail` + "`" + `, { id }), + + list: (params: List{{.Feature.Name}}Req) => + post>(` + "`" + `${BASE}/list` + "`" + `, params), +} +` + +// ---------------------------------------- +// 前端模板:React 管理页面 +// ---------------------------------------- + +const fePageTmpl = `import { useState, useEffect } from 'react' +import { {{lowerFirst .Feature.Name}}Api } from '@/api/{{.Module.PackageName}}/{{.SnakeName}}' +import type { {{.Feature.Name}}, List{{.Feature.Name}}Req } from '@/types/{{.Module.PackageName}}' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent } from '@/components/ui/card' +import { toast } from 'sonner' +import { Plus, Pencil, Trash2, Search, RefreshCw } from 'lucide-react' + +export default function {{.Feature.Name}}Page() { + const [list, setList] = useState<{{.Feature.Name}}[]>([]) + const [total, setTotal] = useState(0) + const [loading, setLoading] = useState(false) + const [params, setParams] = useState({ current: 1, pageSize: 10, keyword: '' }) + + const load = async () => { + setLoading(true) + try { + const res = await {{lowerFirst .Feature.Name}}Api.list(params) + setList(res.list) + setTotal(res.total) + } catch {} + finally { setLoading(false) } + } + + useEffect(() => { load() }, [params.current]) + + const handleDelete = async (id: string) => { + if (!confirm('确认删除?')) return + try { + await {{lowerFirst .Feature.Name}}Api.delete([id]) + toast.success('删除成功') + load() + } catch {} + } + + const totalPages = Math.ceil(total / (params.pageSize ?? 10)) + + return ( +
+
+
+

{{.Feature.Comment}}管理

+

共 {total} 条记录

+
+ +
+ + + +
+ setParams(p => ({ ...p, keyword: e.target.value, current: 1 }))} + className="max-w-60" + /> + + +
+
+
+ + + + + + + {{- range .Feature.Fields}}{{if .Comment}} + + {{- end}}{{end}} + + + + + + {loading ? ( + + ) : list.length === 0 ? ( + + ) : list.map(item => ( + + {{- range .Feature.Fields}}{{if .JsonTag}}{{end}}{{end}} + + + + ))} + +
{{.Comment}}创建时间操作
加载中…
暂无数据
{String(item.{{.JsonTag}} ?? '—')}{new Date(item.createdAt).toLocaleString()} +
+ + +
+
+ + {totalPages > 1 && ( +
+ 第 {params.current} / {totalPages} 页 +
+ + +
+
+ )} +
+
+
+ ) +} +` + +// ---------------------------------------- +// 辅助函数 +// ---------------------------------------- + +func lowerFirst(s string) string { + if s == "" { + return s + } + return strings.ToLower(s[:1]) + s[1:] +} + +func hasTimeField(feature codegenModel.Feature) bool { + for _, f := range feature.Fields { + if f.Type == "time.Time" { + return true + } + } + return false +} + +// goTypeToTs 将 Go 类型映射为 TypeScript 类型 +func goTypeToTs(goType string) string { + switch goType { + case "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", + "float32", "float64": + return "number" + case "bool": + return "boolean" + case "time.Time": + return "string" + default: + return "string" + } +} + +// ---------------------------------------- +// TestConnection 测试数据库连接 +// ---------------------------------------- + +func (s *CodegenService) TestConnection(cfg codegenModel.DbConfig) error { + _, err := openDB(cfg) + return err +} + +func openDB(cfg codegenModel.DbConfig) (*gorm.DB, error) { + switch strings.ToLower(cfg.DbType) { + case "mysql": + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DbName) + return gorm.Open(mysql.Open(dsn), &gorm.Config{}) + case "postgres", "postgresql": + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", + cfg.Host, cfg.User, cfg.Password, cfg.DbName, cfg.Port) + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) + case "sqlite": + return gorm.Open(sqlite.Open(cfg.DbName), &gorm.Config{}) + default: + return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.DbType) + } +} + +// ---------------------------------------- +// Preview 预览生成代码 +// ---------------------------------------- + +func (s *CodegenService) Preview(config codegenModel.GenConfig) ([]codegenModel.PreviewFile, error) { + backendFiles, err := s.renderAll(config, config.OutputDir) + if err != nil { + return nil, err + } + if config.FrontendOutputDir != "" { + feFiles, err := s.renderFrontend(config) + if err != nil { + return nil, err + } + backendFiles = append(backendFiles, feFiles...) + } + return backendFiles, nil +} + +// ---------------------------------------- +// Generate 生成并写入文件 +// ---------------------------------------- + +func (s *CodegenService) Generate(config codegenModel.GenConfig) (*codegenModel.GenResult, error) { + outputDir := config.OutputDir + if outputDir == "" { + cwd, _ := os.Getwd() + outputDir = cwd + } + + // 1. 渲染后端文件 + backendFiles, err := s.renderAll(config, outputDir) + if err != nil { + return nil, err + } + + // 2. 自动注册(必须在写入文件之前,否则 enter.go 已存在导致误判为已有模块) + regMsg := "" + if err := s.autoRegister(outputDir, config); err != nil { + regMsg = fmt.Sprintf("⚠️ 自动注册失败: %s", err.Error()) + } else { + regMsg = "✅ 自动注册成功(gorm/router/enter)" + } + + // 3. 写入后端文件(增量模式下跳过已存在文件) + written, skipped := writeFilesIncremental(outputDir, backendFiles, config.Overwrite) + + allFiles := backendFiles + + // 4. 生成前端文件(如果配置了前端输出目录) + if config.FrontendOutputDir != "" { + feFiles, err := s.renderFrontend(config) + if err != nil { + return nil, err + } + w, sk := writeFilesIncremental(config.FrontendOutputDir, feFiles, config.Overwrite) + written += w + skipped += sk + allFiles = append(allFiles, feFiles...) + } + + return &codegenModel.GenResult{ + OutputDir: outputDir, + Files: allFiles, + Message: fmt.Sprintf("写入 %d 个文件,跳过 %d 个已存在文件。%s", written, skipped, regMsg), + }, nil +} + +// writeFilesIncremental 写入文件,支持增量模式 +// overwrite=false 时,已存在的文件会被跳过 +// 返回 (写入数, 跳过数) +func writeFilesIncremental(baseDir string, files []codegenModel.PreviewFile, overwrite bool) (written int, skipped int) { + for _, f := range files { + fullPath := filepath.Join(baseDir, f.FilePath) + // 增量模式下跳过已存在文件(但 AlwaysOverwrite 的聚合文件始终覆盖) + if !overwrite && !f.AlwaysOverwrite { + if _, err := os.Stat(fullPath); err == nil { + skipped++ + continue + } + } + if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { + continue + } + if err := os.WriteFile(fullPath, []byte(f.Content), 0644); err != nil { + continue + } + written++ + } + return +} + +// renderFrontend 渲染前端 TypeScript / React 文件 +func (s *CodegenService) renderFrontend(config codegenModel.GenConfig) ([]codegenModel.PreviewFile, error) { + var result []codegenModel.PreviewFile + + funcMap := template.FuncMap{ + "lowerFirst": lowerFirst, + "goTypeToTs": goTypeToTs, + "not": func(b bool) bool { return !b }, + } + + for _, module := range config.Modules { + // 跳过未填写包名的模块 + if strings.TrimSpace(module.PackageName) == "" || strings.TrimSpace(module.Name) == "" { + continue + } + // ---- TypeScript 类型(模块级,所有实体合并到 index.ts)---- + tsData := struct { + Module codegenModel.Module + Features []codegenModel.Feature + }{module, module.Features} + tsContent, err := renderTmpl("fe_ts_type", feTsTypeTmpl, funcMap, tsData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("src", "types", module.PackageName, "index.ts"), + Content: tsContent, + AlwaysOverwrite: true, + }) + + for _, feature := range module.Features { + // 跳过未命名的实体 + if strings.TrimSpace(feature.Name) == "" { + continue + } + snakeName := toSnake(feature.Name) + + feData := struct { + Module codegenModel.Module + Feature codegenModel.Feature + SnakeName string + ColCount int + }{module, feature, snakeName, len(feature.Fields) + 2} + + // API service + apiContent, err := renderTmpl("fe_api", feApiTmpl, funcMap, feData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("src", "api", module.PackageName, snakeName+".ts"), + Content: apiContent, + }) + + // React page + pageContent, err := renderTmpl("fe_page", fePageTmpl, funcMap, feData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("src", "pages", module.PackageName, feature.Name+"Page.tsx"), + Content: pageContent, + }) + } + } + return result, nil +} + +// ---------------------------------------- +// renderAll 渲染所有模块的所有文件 +// ---------------------------------------- + +func (s *CodegenService) renderAll(config codegenModel.GenConfig, outputDir string) ([]codegenModel.PreviewFile, error) { + var result []codegenModel.PreviewFile + + funcMap := template.FuncMap{ + "lowerFirst": lowerFirst, + } + + for _, module := range config.Modules { + // 跳过未填写包名的模块 + if strings.TrimSpace(module.PackageName) == "" || strings.TrimSpace(module.Name) == "" { + continue + } + pkg := module.PackageName + pascalModule := module.Name + + type featureEntry struct { + Name string + PascalModuleName string + } + + // 发现已存在的 feature(扫描 service/{pkg}/ 下的 *_service.go 文件) + existingFeatures := discoverExistingFeatures(outputDir, pkg) + + // 合并:已有 feature + 当前配置中的 feature(去重) + featureSet := make(map[string]bool) + var allFeatureEntries []featureEntry + for _, name := range existingFeatures { + if !featureSet[name] { + featureSet[name] = true + allFeatureEntries = append(allFeatureEntries, featureEntry{name, pascalModule}) + } + } + for _, f := range module.Features { + if strings.TrimSpace(f.Name) == "" { + continue + } + if !featureSet[f.Name] { + featureSet[f.Name] = true + allFeatureEntries = append(allFeatureEntries, featureEntry{f.Name, pascalModule}) + } + } + + enterData := struct { + PackageName string + Features []featureEntry + }{pkg, allFeatureEntries} + + // service/enter.go(始终覆盖,包含所有已有+新增 feature) + svcEnter, err := renderTmpl("service_enter", serviceEnterTmpl, funcMap, enterData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("service", pkg, "enter.go"), + Content: svcEnter, + AlwaysOverwrite: true, + }) + + // api/v1/enter.go + apiEnter, err := renderTmpl("api_enter", apiEnterTmpl, funcMap, enterData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("api", "v1", pkg, "enter.go"), + Content: apiEnter, + AlwaysOverwrite: true, + }) + + // router/enter.go + routerEnter, err := renderTmpl("router_enter", routerEnterTmpl, funcMap, enterData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("router", pkg, "enter.go"), + Content: routerEnter, + AlwaysOverwrite: true, + }) + + // ---- request 结构体(按 feature 分文件,增量安全)---- + for _, feature := range module.Features { + if strings.TrimSpace(feature.Name) == "" { + continue + } + reqData := struct { + PackageName string + Features []codegenModel.Feature + }{pkg, []codegenModel.Feature{feature}} + reqContent, err := renderTmpl("request", requestTmpl, funcMap, reqData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("model", pkg, "request", toSnake(feature.Name)+"_request.go"), + Content: reqContent, + }) + } + + // 每个 Feature 生成:model / service / api / router + for _, feature := range module.Features { + // 跳过未命名的实体 + if strings.TrimSpace(feature.Name) == "" { + continue + } + lower := lowerFirst(feature.Name) + snakeName := toSnake(feature.Name) + routerGroup := snakeName + routerPrefix := pkg + "/" + snakeName + + // ---- model ---- + modelData := struct { + PackageName string + Feature codegenModel.Feature + HasTimeField bool + }{pkg, feature, hasTimeField(feature)} + modelContent, err := renderTmpl("model", modelTmpl, funcMap, modelData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("model", pkg, snakeName+".go"), + Content: modelContent, + }) + + // ---- service ---- + svcData := struct { + PackageName string + Feature codegenModel.Feature + }{pkg, feature} + svcContent, err := renderTmpl("service", serviceTmpl, funcMap, svcData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("service", pkg, snakeName+"_service.go"), + Content: svcContent, + }) + + // ---- api handler ---- + apiData := struct { + PackageName string + Feature codegenModel.Feature + LowerName string + RouterPrefix string + PascalModuleName string + }{pkg, feature, lower, routerPrefix, pascalModule} + apiContent, err := renderTmpl("api", apiTmpl, funcMap, apiData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("api", "v1", pkg, snakeName+".go"), + Content: apiContent, + }) + + // ---- router ---- + routerData := struct { + PackageName string + Feature codegenModel.Feature + LowerName string + RouterGroup string + }{pkg, feature, lower, routerGroup} + routerContent, err := renderTmpl("router", routerTmpl, funcMap, routerData) + if err != nil { + return nil, err + } + result = append(result, codegenModel.PreviewFile{ + FilePath: filepath.Join("router", pkg, snakeName+"_router.go"), + Content: routerContent, + }) + } + } + + return result, nil +} + +func renderTmpl(name, tmplStr string, funcMap template.FuncMap, data any) (string, error) { + t, err := template.New(name).Funcs(funcMap).Parse(tmplStr) + if err != nil { + return "", fmt.Errorf("解析模板 [%s] 失败: %w", name, err) + } + var buf bytes.Buffer + if err = t.Execute(&buf, data); err != nil { + return "", fmt.Errorf("渲染模板 [%s] 失败: %w", name, err) + } + return buf.String(), nil +} + +// toSnake 将 PascalCase 转为 snake_case +func toSnake(s string) string { + var result []rune + for i, r := range s { + if r >= 'A' && r <= 'Z' { + if i > 0 { + result = append(result, '_') + } + result = append(result, r+32) + } else { + result = append(result, r) + } + } + return string(result) +} diff --git a/service/codegen/enter.go b/service/codegen/enter.go new file mode 100644 index 0000000..574d944 --- /dev/null +++ b/service/codegen/enter.go @@ -0,0 +1,5 @@ +package codegen + +type ServiceGroup struct { + CodegenService +} diff --git a/service/enter.go b/service/enter.go index d7cd944..a00d84d 100644 --- a/service/enter.go +++ b/service/enter.go @@ -1,9 +1,15 @@ package service -import "sundynix-go/service/system" +import ( + "sundynix-go/service/codegen" + "sundynix-go/service/order" + "sundynix-go/service/system" +) var ServiceGroupApp = new(ServiceGroup) type ServiceGroup struct { - SystemServiceGroup system.ServiceGroup + SystemServiceGroup system.ServiceGroup + CodegenServiceGroup codegen.ServiceGroup + OrderServiceGroup order.ServiceGroup } diff --git a/service/order/enter.go b/service/order/enter.go new file mode 100644 index 0000000..66ccebb --- /dev/null +++ b/service/order/enter.go @@ -0,0 +1,7 @@ +package order + +type ServiceGroup struct { + OrderService + RefundService + StockService +} diff --git a/service/order/order_service.go b/service/order/order_service.go new file mode 100644 index 0000000..1b9916a --- /dev/null +++ b/service/order/order_service.go @@ -0,0 +1,53 @@ +package order + +import ( + "sundynix-go/global" + model "sundynix-go/model/order" + req "sundynix-go/model/order/request" +) + +type OrderService struct{} + +var OrderServiceApp = new(OrderService) + +func (s *OrderService) Save(r req.SaveOrderReq) error { + data := model.Order{ + UserId: r.UserId, + } + return global.DB.Create(&data).Error +} + +func (s *OrderService) Update(r req.UpdateOrderReq) error { + updates := map[string]any{ + "user_id": r.UserId, + } + return global.DB.Model(&model.Order{}).Where("id = ?", r.Id).Updates(updates).Error +} + +func (s *OrderService) Delete(ids []string) error { + return global.DB.Where("id IN ?", ids).Delete(&model.Order{}).Error +} + +func (s *OrderService) Detail(id string) (data *model.Order, err error) { + var record model.Order + err = global.DB.Where("id = ?", id).First(&record).Error + return &record, err +} + +func (s *OrderService) List(r req.ListOrderReq) (list []model.Order, total int64, err error) { + if r.PageSize <= 0 { + r.PageSize = 10 + } + if r.Current <= 0 { + r.Current = 1 + } + db := global.DB.Model(&model.Order{}) + if r.Keyword != "" { + db = db.Where("id LIKE ?", "%"+r.Keyword+"%") + } + if err = db.Count(&total).Error; err != nil { + return + } + err = db.Limit(r.PageSize).Offset(r.PageSize * (r.Current - 1)).Find(&list).Error + return +} diff --git a/service/order/refund_service.go b/service/order/refund_service.go new file mode 100644 index 0000000..b3556b9 --- /dev/null +++ b/service/order/refund_service.go @@ -0,0 +1,53 @@ +package order + +import ( + "sundynix-go/global" + model "sundynix-go/model/order" + req "sundynix-go/model/order/request" +) + +type RefundService struct{} + +var RefundServiceApp = new(RefundService) + +func (s *RefundService) Save(r req.SaveRefundReq) error { + data := model.Refund{ + UserId: r.UserId, + } + return global.DB.Create(&data).Error +} + +func (s *RefundService) Update(r req.UpdateRefundReq) error { + updates := map[string]any{ + "user_id": r.UserId, + } + return global.DB.Model(&model.Refund{}).Where("id = ?", r.Id).Updates(updates).Error +} + +func (s *RefundService) Delete(ids []string) error { + return global.DB.Where("id IN ?", ids).Delete(&model.Refund{}).Error +} + +func (s *RefundService) Detail(id string) (data *model.Refund, err error) { + var record model.Refund + err = global.DB.Where("id = ?", id).First(&record).Error + return &record, err +} + +func (s *RefundService) List(r req.ListRefundReq) (list []model.Refund, total int64, err error) { + if r.PageSize <= 0 { + r.PageSize = 10 + } + if r.Current <= 0 { + r.Current = 1 + } + db := global.DB.Model(&model.Refund{}) + if r.Keyword != "" { + db = db.Where("id LIKE ?", "%"+r.Keyword+"%") + } + if err = db.Count(&total).Error; err != nil { + return + } + err = db.Limit(r.PageSize).Offset(r.PageSize * (r.Current - 1)).Find(&list).Error + return +} diff --git a/service/order/stock_service.go b/service/order/stock_service.go new file mode 100644 index 0000000..5bc84cc --- /dev/null +++ b/service/order/stock_service.go @@ -0,0 +1,53 @@ +package order + +import ( + "sundynix-go/global" + model "sundynix-go/model/order" + req "sundynix-go/model/order/request" +) + +type StockService struct{} + +var StockServiceApp = new(StockService) + +func (s *StockService) Save(r req.SaveStockReq) error { + data := model.Stock{ + Amount: r.Amount, + } + return global.DB.Create(&data).Error +} + +func (s *StockService) Update(r req.UpdateStockReq) error { + updates := map[string]any{ + "amount": r.Amount, + } + return global.DB.Model(&model.Stock{}).Where("id = ?", r.Id).Updates(updates).Error +} + +func (s *StockService) Delete(ids []string) error { + return global.DB.Where("id IN ?", ids).Delete(&model.Stock{}).Error +} + +func (s *StockService) Detail(id string) (data *model.Stock, err error) { + var record model.Stock + err = global.DB.Where("id = ?", id).First(&record).Error + return &record, err +} + +func (s *StockService) List(r req.ListStockReq) (list []model.Stock, total int64, err error) { + if r.PageSize <= 0 { + r.PageSize = 10 + } + if r.Current <= 0 { + r.Current = 1 + } + db := global.DB.Model(&model.Stock{}) + if r.Keyword != "" { + db = db.Where("id LIKE ?", "%"+r.Keyword+"%") + } + if err = db.Count(&total).Error; err != nil { + return + } + err = db.Limit(r.PageSize).Offset(r.PageSize * (r.Current - 1)).Find(&list).Error + return +} diff --git a/utils/template/.gitkeep b/utils/template/.gitkeep new file mode 100644 index 0000000..8a42e59 --- /dev/null +++ b/utils/template/.gitkeep @@ -0,0 +1,4 @@ +package main + +// This file is ONLY used as a template reference for the codegen service. +// The actual templates are embedded as string constants in the service.