diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 3b2f5e7..70b011e 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/device" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller/user" "git.huangwc.com/pig/pig-farm-controller/internal/app/service/token" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" @@ -20,18 +21,19 @@ import ( // API 结构体定义了 HTTP 服务器及其依赖 type API struct { - engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 - logger *logs.Logger // 日志记录器,用于输出日志信息 - userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 - tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 - httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 - config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig - userController *user.Controller // 用户控制器实例 + engine *gin.Engine // Gin 引擎实例,用于处理 HTTP 请求 + logger *logs.Logger // 日志记录器,用于输出日志信息 + userRepo repository.UserRepository // 用户数据仓库接口,用于用户数据操作 + tokenService token.TokenService // Token 服务接口,用于 JWT token 的生成和解析 + httpServer *http.Server // 标准库的 HTTP 服务器实例,用于启动和停止服务 + config config.ServerConfig // API 服务器的配置,使用 infra/config 包中的 ServerConfig + userController *user.Controller // 用户控制器实例 + deviceController *device.Controller // 设备控制器实例 } // NewAPI 创建并返回一个新的 API 实例 // 负责初始化 Gin 引擎、设置全局中间件,并注入所有必要的依赖。 -func NewAPI(cfg config.ServerConfig, logger *logs.Logger, userRepo repository.UserRepository, tokenService token.TokenService) *API { +func NewAPI(cfg config.ServerConfig, logger *logs.Logger, userRepo repository.UserRepository, deviceRepository repository.DeviceRepository, tokenService token.TokenService) *API { // 设置 Gin 模式,例如 gin.ReleaseMode (生产模式) 或 gin.DebugMode (开发模式) // 从配置中获取 Gin 模式 gin.SetMode(cfg.Mode) @@ -53,6 +55,8 @@ func NewAPI(cfg config.ServerConfig, logger *logs.Logger, userRepo repository.Us config: cfg, // 在 NewAPI 中初始化用户控制器,并将其作为 API 结构体的成员 userController: user.NewController(userRepo, logger, tokenService), + // 在 NewAPI 中初始化设备控制器,并将其作为 API 结构体的成员 + deviceController: device.NewController(deviceRepository, logger), } api.setupRoutes() // 设置所有路由 @@ -71,6 +75,16 @@ func (a *API) setupRoutes() { userGroup.POST("", a.userController.CreateUser) // 注册创建用户接口 (POST /api/v1/users) userGroup.POST("/login", a.userController.Login) // 注册用户登录接口 (POST /api/v1/users/login) } + + // 设备相关路由组 + deviceGroup := v1.Group("/devices") + { + deviceGroup.POST("", a.deviceController.CreateDevice) + deviceGroup.GET("", a.deviceController.ListDevices) + deviceGroup.GET("/:id", a.deviceController.GetDevice) + deviceGroup.PUT("/:id", a.deviceController.UpdateDevice) + deviceGroup.DELETE("/:id", a.deviceController.DeleteDevice) + } } // 添加 Swagger UI 路由 diff --git a/internal/app/controller/device/device_controller.go b/internal/app/controller/device/device_controller.go new file mode 100644 index 0000000..0b3bc9c --- /dev/null +++ b/internal/app/controller/device/device_controller.go @@ -0,0 +1,224 @@ +package device + +import ( + "errors" + "net/http" + "strconv" + "strings" + + "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" + "github.com/gin-gonic/gin" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// Controller 设备控制器,封装了所有与设备相关的业务逻辑 +type Controller struct { + repo repository.DeviceRepository + logger *logs.Logger +} + +// NewController 创建一个新的设备控制器实例 +func NewController(repo repository.DeviceRepository, logger *logs.Logger) *Controller { + return &Controller{ + repo: repo, + logger: logger, + } +} + +// --- Request & Response Structs --- + +// CreateDeviceRequest 定义了创建设备时需要传入的参数 +type CreateDeviceRequest struct { + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties datatypes.JSON `json:"properties,omitempty"` +} + +// UpdateDeviceRequest 定义了更新设备时需要传入的参数 +type UpdateDeviceRequest struct { + Name string `json:"name" binding:"required"` + Type models.DeviceType `json:"type" binding:"required"` + SubType models.DeviceSubType `json:"sub_type,omitempty"` + ParentID *uint `json:"parent_id,omitempty"` + Location string `json:"location,omitempty"` + Properties datatypes.JSON `json:"properties,omitempty"` +} + +// --- Controller Methods --- + +// CreateDevice godoc +// @Summary 创建新设备 +// @Description 根据提供的信息创建一个新设备,可以是区域主控或普通设备 +// @Tags 设备管理 +// @Accept json +// @Produce json +// @Param device body CreateDeviceRequest true "设备信息" +// @Success 200 {object} controller.Response{data=models.Device} "业务码为201代表创建成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" +// @Router /devices [post] +func (c *Controller) CreateDevice(ctx *gin.Context) { + var req CreateDeviceRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("创建设备: 参数绑定失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + + device := &models.Device{ + Name: req.Name, + Type: req.Type, + SubType: req.SubType, + ParentID: req.ParentID, + Location: req.Location, + Properties: req.Properties, + } + + if err := c.repo.Create(device); err != nil { + c.logger.Errorf("创建设备: 数据库操作失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "创建设备失败") + return + } + + controller.SendResponse(ctx, http.StatusCreated, "设备创建成功", device) +} + +// GetDevice godoc +// @Summary 获取设备信息 +// @Description 根据设备ID获取单个设备的详细信息 +// @Tags 设备管理 +// @Produce json +// @Param id path string true "设备ID" +// @Success 200 {object} controller.Response{data=models.Device} "业务码为200代表获取成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" +// @Router /devices/{id} [get] +func (c *Controller) GetDevice(ctx *gin.Context) { + deviceID := ctx.Param("id") + + device, err := c.repo.FindByIDString(deviceID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorResponse(ctx, http.StatusNotFound, "设备未找到") + return + } + // 检查是否是ID格式错误 + if strings.Contains(err.Error(), "无效的设备ID格式") { + controller.SendErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + c.logger.Errorf("获取设备: 数据库操作失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "获取设备信息失败") + return + } + + controller.SendResponse(ctx, http.StatusOK, "获取设备信息成功", device) +} + +// ListDevices godoc +// @Summary 获取设备列表 +// @Description 获取系统中所有设备的列表 +// @Tags 设备管理 +// @Produce json +// @Success 200 {object} controller.Response{data=[]models.Device} "业务码为200代表获取成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如500)" +// @Router /devices [get] +func (c *Controller) ListDevices(ctx *gin.Context) { + devices, err := c.repo.ListAll() + if err != nil { + c.logger.Errorf("获取设备列表: 数据库操作失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "获取设备列表失败") + return + } + + controller.SendResponse(ctx, http.StatusOK, "获取设备列表成功", devices) +} + +// UpdateDevice godoc +// @Summary 更新设备信息 +// @Description 根据设备ID更新一个已存在的设备信息 +// @Tags 设备管理 +// @Accept json +// @Produce json +// @Param id path string true "设备ID" +// @Param device body UpdateDeviceRequest true "要更新的设备信息" +// @Success 200 {object} controller.Response{data=models.Device} "业务码为200代表更新成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 404, 500)" +// @Router /devices/{id} [put] +func (c *Controller) UpdateDevice(ctx *gin.Context) { + deviceID := ctx.Param("id") + + // 1. 检查设备是否存在 + existingDevice, err := c.repo.FindByIDString(deviceID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + controller.SendErrorResponse(ctx, http.StatusNotFound, "设备未找到") + return + } + if strings.Contains(err.Error(), "无效的设备ID格式") { + controller.SendErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + c.logger.Errorf("更新设备: 查找设备失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "更新设备失败") + return + } + + // 2. 绑定请求参数 + var req UpdateDeviceRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + c.logger.Errorf("更新设备: 参数绑定失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusBadRequest, err.Error()) + return + } + + // 3. 更新字段 + existingDevice.Name = req.Name + existingDevice.Type = req.Type + existingDevice.SubType = req.SubType + existingDevice.ParentID = req.ParentID + existingDevice.Location = req.Location + existingDevice.Properties = req.Properties + + // 4. 保存到数据库 + if err := c.repo.Update(existingDevice); err != nil { + c.logger.Errorf("更新设备: 数据库操作失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "更新设备失败") + return + } + + controller.SendResponse(ctx, http.StatusOK, "设备更新成功", existingDevice) +} + +// DeleteDevice godoc +// @Summary 删除设备 +// @Description 根据设备ID删除一个设备(软删除) +// @Tags 设备管理 +// @Produce json +// @Param id path string true "设备ID" +// @Success 200 {object} controller.Response "业务码为200代表删除成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 500)" +// @Router /devices/{id} [delete] +func (c *Controller) DeleteDevice(ctx *gin.Context) { + deviceID := ctx.Param("id") + + // 我们需要先将字符串ID转换为uint,因为Delete方法需要uint类型 + idUint, err := strconv.ParseUint(deviceID, 10, 64) + if err != nil { + controller.SendErrorResponse(ctx, http.StatusBadRequest, "无效的设备ID格式") + return + } + + if err := c.repo.Delete(uint(idUint)); err != nil { + c.logger.Errorf("删除设备: 数据库操作失败: %v", err) + controller.SendErrorResponse(ctx, http.StatusInternalServerError, "删除设备失败") + return + } + + controller.SendResponse(ctx, http.StatusOK, "设备删除成功", nil) +} diff --git a/internal/app/controller/user/user_controller.go b/internal/app/controller/user/user_controller.go index 89560fe..b4031c5 100644 --- a/internal/app/controller/user/user_controller.go +++ b/internal/app/controller/user/user_controller.go @@ -60,10 +60,8 @@ type LoginResponse struct { // @Accept json // @Produce json // @Param user body CreateUserRequest true "用户信息" -// @Success 200 {object} controller.Response{data=CreateUserResponse} "用户创建成功" -// @Failure 400 {object} controller.Response "请求参数错误" -// @Failure 409 {object} controller.Response "用户名已存在" -// @Failure 500 {object} controller.Response "服务器内部错误" +// @Success 200 {object} controller.Response{data=CreateUserResponse} "业务码为201代表创建成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 409, 500)" // @Router /users [post] func (c *Controller) CreateUser(ctx *gin.Context) { var req CreateUserRequest @@ -93,7 +91,7 @@ func (c *Controller) CreateUser(ctx *gin.Context) { return } - controller.SendResponse(ctx, http.StatusOK, "用户创建成功", CreateUserResponse{ + controller.SendResponse(ctx, http.StatusCreated, "用户创建成功", CreateUserResponse{ Username: user.Username, ID: user.ID, }) @@ -106,10 +104,8 @@ func (c *Controller) CreateUser(ctx *gin.Context) { // @Accept json // @Produce json // @Param credentials body LoginRequest true "登录凭证" -// @Success 200 {object} controller.Response{data=LoginResponse} "登录成功" -// @Failure 400 {object} controller.Response "请求参数错误" -// @Failure 401 {object} controller.Response "用户名或密码不正确" -// @Failure 500 {object} controller.Response "服务器内部错误" +// @Success 200 {object} controller.Response{data=LoginResponse} "业务码为200代表登录成功" +// @Failure 200 {object} controller.Response "业务失败,具体错误码和信息见响应体(例如400, 401, 500)" // @Router /users/login [post] func (c *Controller) Login(ctx *gin.Context) { var req LoginRequest diff --git a/internal/core/application.go b/internal/core/application.go index 8ec5c73..7ce4bce 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -46,16 +46,19 @@ func NewApplication(configPath string) (*Application, error) { // 4. 初始化任务执行器 executor := task.NewExecutor(cfg.Heartbeat.Concurrency, logger) - // 5. 初始化用户仓库 - userRepo := repository.NewGormUserRepository(storage.GetDB()) - - // 6. 初始化 Token 服务 + // 5. 初始化 Token 服务 tokenService := token.NewTokenService([]byte(cfg.App.JWTSecret)) - // 7. 初始化 API 服务器 - apiServer := api.NewAPI(cfg.Server, logger, userRepo, tokenService) + // 6. 初始化用户仓库 + userRepo := repository.NewGormUserRepository(storage.GetDB()) - // 8. 组装 Application 对象 + // 7. 初始化设备仓库 + deviceRepo := repository.NewGormDeviceRepository(storage.GetDB()) + + // 8. 初始化 API 服务器 + apiServer := api.NewAPI(cfg.Server, logger, userRepo, deviceRepo, tokenService) + + // 9. 组装 Application 对象 app := &Application{ Config: cfg, Logger: logger, diff --git a/internal/infra/repository/device_repository.go b/internal/infra/repository/device_repository.go index 50ab009..b5554b3 100644 --- a/internal/infra/repository/device_repository.go +++ b/internal/infra/repository/device_repository.go @@ -20,6 +20,9 @@ type DeviceRepository interface { // FindByIDString 根据字符串形式的主键 ID 查找设备,方便控制器调用 FindByIDString(id string) (*models.Device, error) + // ListAll 获取所有设备的列表 + ListAll() ([]*models.Device, error) + // ListByParentID 根据父级 ID 列出所有子设备。 // 如果 parentID 为 nil,则列出所有顶层设备(如区域主控)。 ListByParentID(parentID *uint) ([]*models.Device, error) @@ -67,6 +70,15 @@ func (r *gormDeviceRepository) FindByIDString(id string) (*models.Device, error) return r.FindByID(uint(idInt)) } +// ListAll 获取所有设备的列表 +func (r *gormDeviceRepository) ListAll() ([]*models.Device, error) { + var devices []*models.Device + if err := r.db.Find(&devices).Error; err != nil { + return nil, err + } + return devices, nil +} + // ListByParentID 根据父级 ID 列出所有子设备 func (r *gormDeviceRepository) ListByParentID(parentID *uint) ([]*models.Device, error) { var devices []*models.Device