feat(dashboard-apps): align chatwoot payloads

This commit is contained in:
2026-06-06 13:03:16 +08:00
parent 11e5537f9b
commit 75f0e809a5
9 changed files with 312 additions and 71 deletions
+123 -28
View File
@@ -1,13 +1,18 @@
package v1
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/response"
pkgvalidator "github.com/gochat/gochat/pkg/validator"
)
// DashboardAppHandler handles DashboardApp CRUD endpoints.
@@ -21,6 +26,13 @@ type DashboardAppHandler struct {
svc *service.DashboardAppService
}
type dashboardAppPayload struct {
ID uint `json:"id"`
Title string `json:"title"`
Content json.RawMessage `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
func NewDashboardAppHandler(svc *service.DashboardAppService) *DashboardAppHandler {
return &DashboardAppHandler{svc: svc}
}
@@ -37,26 +49,24 @@ func (h *DashboardAppHandler) Create(c *gin.Context) {
userID := getUserID(c)
userIDPtr := &userID
var wrapper service.DashboardAppCreateWrapper
if err := c.ShouldBindJSON(&wrapper); err != nil {
req, err := bindDashboardAppCreate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
req := wrapper.DashboardApp
if err := pkgvalidator.ValidateStruct(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
if req.Title == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "title is required")
return
}
app, err := h.svc.Create(c.Request.Context(), accountID, userIDPtr, &req)
app, err := h.svc.Create(c.Request.Context(), accountID, userIDPtr, req)
if err != nil {
applogger.L().Errorf("Create dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create dashboard app")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
return
}
response.Created(c, app)
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Get retrieves a dashboard app by ID.
@@ -68,14 +78,20 @@ func (h *DashboardAppHandler) Get(c *gin.Context) {
return
}
app, err := h.svc.GetByID(c.Request.Context(), id)
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
app, err := h.svc.GetByAccountAndID(c.Request.Context(), accountID, id)
if err != nil {
applogger.L().Errorf("Get dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.OK(c, app)
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Update modifies an existing dashboard app.
@@ -88,21 +104,26 @@ func (h *DashboardAppHandler) Update(c *gin.Context) {
return
}
var wrapper service.DashboardAppUpdateWrapper
if err := c.ShouldBindJSON(&wrapper); err != nil {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
req, err := bindDashboardAppUpdate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
req := wrapper.DashboardApp
app, err := h.svc.Update(c.Request.Context(), id, &req)
app, err := h.svc.UpdateByAccountAndID(c.Request.Context(), accountID, id, req)
if err != nil {
applogger.L().Errorf("Update dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update dashboard app")
handleDashboardAppMutationError(c, err)
return
}
response.OK(c, app)
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// Delete removes a dashboard app.
@@ -115,8 +136,18 @@ func (h *DashboardAppHandler) Delete(c *gin.Context) {
return
}
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
if err := h.svc.DeleteByAccountAndID(c.Request.Context(), accountID, id); err != nil {
applogger.L().Errorf("Delete dashboard app: %v", err)
if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete dashboard app")
return
}
@@ -133,21 +164,26 @@ func (h *DashboardAppHandler) Patch(c *gin.Context) {
return
}
var wrapper service.DashboardAppUpdateWrapper
if err := c.ShouldBindJSON(&wrapper); err != nil {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
req, err := bindDashboardAppUpdate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
req := wrapper.DashboardApp
app, err := h.svc.Update(c.Request.Context(), id, &req)
app, err := h.svc.UpdateByAccountAndID(c.Request.Context(), accountID, id, req)
if err != nil {
applogger.L().Errorf("Patch dashboard app: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update dashboard app")
handleDashboardAppMutationError(c, err)
return
}
response.OK(c, app)
c.JSON(http.StatusOK, serializeDashboardApp(app))
}
// List returns all dashboard apps for an account.
@@ -167,8 +203,67 @@ func (h *DashboardAppHandler) List(c *gin.Context) {
return
}
// Chatwoot returns a pure JSON array (no meta/pagination wrapper)
c.JSON(http.StatusOK, apps)
payload := make([]dashboardAppPayload, 0, len(apps))
for i := range apps {
payload = append(payload, serializeDashboardApp(&apps[i]))
}
c.JSON(http.StatusOK, payload)
}
func bindDashboardAppCreate(c *gin.Context) (*service.CreateDashboardAppRequest, error) {
var raw map[string]json.RawMessage
if err := c.ShouldBindJSON(&raw); err != nil {
return nil, err
}
if nested, ok := raw["dashboard_app"]; ok {
var req service.CreateDashboardAppRequest
if err := json.Unmarshal(nested, &req); err != nil {
return nil, err
}
return &req, nil
}
body, _ := json.Marshal(raw)
var req service.CreateDashboardAppRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, err
}
return &req, nil
}
func bindDashboardAppUpdate(c *gin.Context) (*service.UpdateDashboardAppRequest, error) {
var raw map[string]json.RawMessage
if err := c.ShouldBindJSON(&raw); err != nil {
return nil, err
}
if nested, ok := raw["dashboard_app"]; ok {
var req service.UpdateDashboardAppRequest
if err := json.Unmarshal(nested, &req); err != nil {
return nil, err
}
return &req, nil
}
body, _ := json.Marshal(raw)
var req service.UpdateDashboardAppRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, err
}
return &req, nil
}
func serializeDashboardApp(app *model.DashboardApp) dashboardAppPayload {
content := app.Content
if len(content) == 0 {
content = json.RawMessage(`[]`)
}
return dashboardAppPayload{ID: app.ID, Title: app.Title, Content: content, CreatedAt: app.CreatedAt}
}
func handleDashboardAppMutationError(c *gin.Context, err error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "dashboard app not found")
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
}
// ========== GoChat Extension Endpoints (not in Chatwoot) ==========
@@ -297,4 +392,4 @@ func (h *DashboardAppHandler) UpdateWidget(c *gin.Context) {
}
response.OK(c, widgets)
}
}