WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
package workbench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Handler struct{ service *Service }
|
||||
|
||||
func NewHandler(s *Service) *Handler { return &Handler{s} }
|
||||
func respondError(c *gin.Context, e error) {
|
||||
_ = c.Error(e)
|
||||
code := http.StatusInternalServerError
|
||||
message := "服务处理失败,请稍后重试"
|
||||
switch {
|
||||
case errors.Is(e, gorm.ErrRecordNotFound):
|
||||
code = 404
|
||||
message = "资源不存在或无权访问"
|
||||
case errors.Is(e, ErrForbidden):
|
||||
code = 403
|
||||
message = e.Error()
|
||||
case errors.Is(e, ErrConflict):
|
||||
code = 409
|
||||
message = e.Error()
|
||||
case errors.Is(e, ErrInvalid):
|
||||
code = 400
|
||||
message = e.Error()
|
||||
}
|
||||
c.JSON(code, gin.H{"success": false, "message": message})
|
||||
}
|
||||
func result(c *gin.Context, v any) { c.JSON(200, gin.H{"success": true, "data": v}) }
|
||||
func (h *Handler) Modules(c *gin.Context) {
|
||||
result(c, []gin.H{{"id": "training-maps", "name": "训练营导图"}, {"id": "content-tasks", "name": "内容任务"}, {"id": "flows", "name": "画布编排"}})
|
||||
}
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
k := c.Param("kind")
|
||||
if !validKind(k) {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
q, e := scope(c.Request.Context(), h.service.db.WithContext(c.Request.Context()).Model(&Resource{}))
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
q = q.Where("kind = ?", k)
|
||||
if search := strings.TrimSpace(c.Query("q")); search != "" {
|
||||
q = q.Where("name LIKE ?", "%"+search+"%")
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 50
|
||||
}
|
||||
var total int64
|
||||
if e = q.Count(&total).Error; e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
rows := []Resource{}
|
||||
if e = q.Order("updated_at DESC, id ASC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if c.Query("view") == "summary" {
|
||||
summaries := make([]resourceListSummary, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
summaries = append(summaries, summarizeResource(row))
|
||||
}
|
||||
response := gin.H{"success": true, "data": summaries, "total": total}
|
||||
// The initial detail is opt-in and still goes through the same tenant/owner
|
||||
// scope query. It removes one client round trip without putting full
|
||||
// resource bodies into ordinary summary responses.
|
||||
if c.Query("include") == "initial_detail" {
|
||||
selectedID := strings.TrimSpace(c.Query("selected_id"))
|
||||
if selectedID != "" {
|
||||
detailScope, scopeErr := scope(c.Request.Context(), h.service.db.WithContext(c.Request.Context()))
|
||||
if scopeErr != nil {
|
||||
respondError(c, scopeErr)
|
||||
return
|
||||
}
|
||||
detailScope = detailScope.Where("kind = ?", k)
|
||||
if search := strings.TrimSpace(c.Query("q")); search != "" {
|
||||
detailScope = detailScope.Where("name LIKE ?", "%"+search+"%")
|
||||
}
|
||||
selectedScope := detailScope.Session(&gorm.Session{})
|
||||
fallbackScope := detailScope.Session(&gorm.Session{})
|
||||
var selected Resource
|
||||
if err := selectedScope.Where("id = ?", selectedID).First(&selected).Error; err == nil {
|
||||
response["initial_detail"] = selected
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// A stale/deleted selection falls back to the first authorized
|
||||
// resource in the same ordering used by the summary list.
|
||||
fallback := fallbackScope.Order("updated_at DESC, id ASC").Limit(1).First(&selected).Error
|
||||
if fallback == nil {
|
||||
response["initial_detail"] = selected
|
||||
} else if errors.Is(fallback, gorm.ErrRecordNotFound) {
|
||||
response["initial_detail"] = nil
|
||||
} else {
|
||||
respondError(c, fallback)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(200, response)
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "data": rows, "total": total})
|
||||
}
|
||||
func (h *Handler) Get(c *gin.Context) {
|
||||
r, e := h.service.get(c.Request.Context(), c.Param("kind"), c.Param("id"))
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, r)
|
||||
}
|
||||
|
||||
type resourceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Data JSON `json:"data"`
|
||||
Version int `json:"version"`
|
||||
Configure bool `json:"configure,omitempty"`
|
||||
Summary bool `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
tenant, user, _, e := identity(ctx)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
var request resourceRequest
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 15<<20)
|
||||
if c.ShouldBindJSON(&request) != nil {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
if request.Summary {
|
||||
respondError(c, fmt.Errorf("%w: 请先加载完整详情后再保存", ErrInvalid))
|
||||
return
|
||||
}
|
||||
r := &Resource{ID: uuid.NewString(), TenantID: tenant, OwnerID: user, Kind: c.Param("kind"), Name: strings.TrimSpace(request.Name), Status: "draft", Version: 1, Data: request.Data, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
||||
if r.Data == nil {
|
||||
r.Data = JSON{}
|
||||
}
|
||||
if e = validateLegacyMetadataUpdate(nil, r.Data); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
var u types.User
|
||||
if e = h.service.db.WithContext(ctx).Select("id", "username").Where("id = ?", user).First(&u).Error; e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
r.OwnerName = u.Username
|
||||
// Generated state cannot be supplied while creating an unreviewed draft.
|
||||
delete(r.Data, "reading")
|
||||
if r.Kind == "training-maps" {
|
||||
r.Data["days"] = []Day{}
|
||||
}
|
||||
if r.Kind == "content-tasks" {
|
||||
r.Data["segments"] = []Segment{}
|
||||
r.Data["review"] = ""
|
||||
r.Data["composed"] = ""
|
||||
delete(r.Data, "quality")
|
||||
delete(r.Data, "flow_snapshot")
|
||||
delete(r.Data, "agent_snapshots")
|
||||
delete(r.Data, "continuity_anchors")
|
||||
delete(r.Data, "category_options")
|
||||
}
|
||||
if e = h.service.validate(ctx, r); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if e = h.service.create(ctx, r); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
_ = h.service.event(ctx, r.ID, "create", "创建"+r.Name, "info")
|
||||
c.JSON(201, gin.H{"success": true, "data": r})
|
||||
}
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
r, e := h.service.get(ctx, c.Param("kind"), c.Param("id"))
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
var request resourceRequest
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 15<<20)
|
||||
if c.ShouldBindJSON(&request) != nil {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
if request.Version != r.Version {
|
||||
respondError(c, ErrConflict)
|
||||
return
|
||||
}
|
||||
if request.Summary {
|
||||
respondError(c, fmt.Errorf("%w: 请先加载完整详情后再保存", ErrInvalid))
|
||||
return
|
||||
}
|
||||
beforeData := compact(r.Data)
|
||||
if e = applyManualUpdate(r, request); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if request.Configure || beforeData != compact(r.Data) {
|
||||
ctx = context.WithValue(ctx, configurationSaveKey{}, true)
|
||||
if e = h.service.validate(ctx, r); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
if r.Status == "needs_configuration" {
|
||||
r.Status = "draft"
|
||||
}
|
||||
} else if strings.TrimSpace(r.Name) == "" || len([]rune(r.Name)) > 200 {
|
||||
respondError(c, fmt.Errorf("%w: 名称必填且最多 200 字", ErrInvalid))
|
||||
return
|
||||
}
|
||||
if e = h.service.save(ctx, r, request.Version); e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
_ = h.service.event(ctx, r.ID, "update", "修改"+r.Name, "info")
|
||||
result(c, r)
|
||||
}
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var r *Resource
|
||||
e := h.service.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
r, err = lockResourceForDeletion(ctx, tx, c.Param("kind"), c.Param("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = requireIdle(tx, r.TenantID, r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = archiveResource(ctx, tx, r); err != nil {
|
||||
return err
|
||||
}
|
||||
// Content tasks cannot be referenced by other workbench resources.
|
||||
if r.Kind == "content-tasks" {
|
||||
return deleteResource(ctx, tx, r)
|
||||
}
|
||||
var related []Resource
|
||||
if err := tx.Where("tenant_id = ? AND kind = ?", r.TenantID, "content-tasks").Order("id ASC").Find(&related).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, other := range related {
|
||||
d, _ := decode[TaskData](other.Data)
|
||||
if d.MapID != r.ID && d.FlowID != r.ID {
|
||||
continue
|
||||
}
|
||||
if c.Query("detach") != "true" {
|
||||
return fmt.Errorf("%w: 此资源已绑定内容任务,请选择一键解绑后删除", ErrConflict)
|
||||
}
|
||||
_, _, admin, _ := identity(ctx)
|
||||
if other.OwnerID != r.OwnerID && !admin {
|
||||
return fmt.Errorf("%w: 其他用户的内容任务仍在使用此资源,请先由任务所有者更换画布或解除绑定", ErrConflict)
|
||||
}
|
||||
// Re-read after locking: another edit may have changed the binding.
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", other.ID, r.TenantID).First(&other).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
d, err = decode[TaskData](other.Data)
|
||||
if err != nil {
|
||||
return ErrInvalid
|
||||
}
|
||||
if d.MapID != r.ID && d.FlowID != r.ID {
|
||||
continue
|
||||
}
|
||||
if err := requireIdle(tx, r.TenantID, other.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := detachResourceForDeletion(ctx, tx, r, &other); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deleteResource(ctx, tx, r)
|
||||
})
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, gin.H{"deleted": true})
|
||||
}
|
||||
func (h *Handler) Action(c *gin.Context) {
|
||||
var request ActionRequest
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 2<<20)
|
||||
if c.ShouldBindJSON(&request) != nil {
|
||||
respondError(c, ErrInvalid)
|
||||
return
|
||||
}
|
||||
j, e := h.service.submit(c.Request.Context(), c.Param("kind"), c.Param("id"), c.Param("action"), request)
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
c.JSON(202, gin.H{"success": true, "data": j})
|
||||
}
|
||||
|
||||
func deleteResource(ctx context.Context, tx *gorm.DB, r *Resource) error {
|
||||
if err := invalidateDeletedContentFeedback(tx, r); err != nil {
|
||||
return err
|
||||
}
|
||||
q := tx.Where("id = ? AND tenant_id = ? AND version = ?", r.ID, r.TenantID, r.Version).Delete(&Resource{})
|
||||
if q.Error != nil {
|
||||
return q.Error
|
||||
}
|
||||
if q.RowsAffected != 1 {
|
||||
return ErrConflict
|
||||
}
|
||||
return (&Service{db: tx}).event(ctx, r.ID, "delete", "删除"+r.Name, "info")
|
||||
}
|
||||
|
||||
// PUT changes configuration; generated/accepted content must go through actions.
|
||||
func applyManualUpdate(r *Resource, request resourceRequest) error {
|
||||
if request.Data == nil {
|
||||
return ErrInvalid
|
||||
}
|
||||
if err := validateLegacyMetadataUpdate(r.Data, request.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if compact(r.Data) == compact(request.Data) {
|
||||
r.Name = strings.TrimSpace(request.Name)
|
||||
return nil
|
||||
}
|
||||
switch r.Kind {
|
||||
case "training-maps":
|
||||
before, err := decode[MapData](r.Data)
|
||||
if err != nil {
|
||||
return ErrInvalid
|
||||
}
|
||||
after, err := decode[MapData](request.Data)
|
||||
if err != nil || !reflect.DeepEqual(before.Days, after.Days) || !reflect.DeepEqual(before.Reading, after.Reading) {
|
||||
return fmt.Errorf("%w: 导图内容及确认状态请通过生成或确认操作修改", ErrInvalid)
|
||||
}
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
for i := range after.Days {
|
||||
after.Days[i].Confirmed = false
|
||||
}
|
||||
r.Status = "draft"
|
||||
if len(after.Days) > 0 {
|
||||
r.Status = "needs_review"
|
||||
}
|
||||
}
|
||||
r.Data = resourceData(r, after)
|
||||
case "content-tasks":
|
||||
before, err := decode[TaskData](r.Data)
|
||||
if err != nil {
|
||||
return ErrInvalid
|
||||
}
|
||||
after, err := decode[TaskData](request.Data)
|
||||
if before.QualitySettings != after.QualitySettings {
|
||||
return fmt.Errorf("%w: 质检规则与风格请通过质检设置保存", ErrInvalid)
|
||||
}
|
||||
if !reflect.DeepEqual(before.CategoryOptions, after.CategoryOptions) {
|
||||
return ErrInvalid
|
||||
}
|
||||
if !reflect.DeepEqual(before.AgentCategoryOptions, after.AgentCategoryOptions) {
|
||||
return ErrInvalid
|
||||
}
|
||||
if !reflect.DeepEqual(before.ContinuityAnchors, after.ContinuityAnchors) {
|
||||
return ErrInvalid
|
||||
}
|
||||
if !reflect.DeepEqual(before.Reading, after.Reading) {
|
||||
return ErrInvalid
|
||||
}
|
||||
if err != nil || len(before.Segments) != len(after.Segments) || before.Review != after.Review || before.Composed != after.Composed || !reflect.DeepEqual(before.Quality, after.Quality) || !reflect.DeepEqual(before.FlowSnapshot, after.FlowSnapshot) || !reflect.DeepEqual(before.AgentSnapshots, after.AgentSnapshots) {
|
||||
return fmt.Errorf("%w: 内容段和质检结果请通过任务操作修改", ErrInvalid)
|
||||
}
|
||||
firstChanged := -1
|
||||
for i := range before.Segments {
|
||||
original, proposed := before.Segments[i], after.Segments[i]
|
||||
if (original.AgentID != proposed.AgentID || original.CategoryID != proposed.CategoryID) && firstChanged < 0 {
|
||||
firstChanged = i
|
||||
}
|
||||
options := before.CategoryOptions
|
||||
if before.AgentCategoryOptions != nil {
|
||||
options = before.AgentCategoryOptions[proposed.AgentID]
|
||||
}
|
||||
if proposed.CategoryID != "" && !slices.ContainsFunc(options, func(option SegmentCategory) bool { return option.ID == proposed.CategoryID }) {
|
||||
return fmt.Errorf("%w: 所选分类不属于本段 Agent 的资料范围", ErrInvalid)
|
||||
}
|
||||
proposed.AgentID = original.AgentID
|
||||
proposed.CategoryID = original.CategoryID
|
||||
if !reflect.DeepEqual(original, proposed) {
|
||||
return fmt.Errorf("%w: 内容段只允许更换负责 Agent 和知识分类;生成及确认请使用任务操作", ErrInvalid)
|
||||
}
|
||||
}
|
||||
beforeConfig, afterConfig := before, after
|
||||
beforeConfig.Segments, afterConfig.Segments = nil, nil
|
||||
if !reflect.DeepEqual(beforeConfig, afterConfig) {
|
||||
durationChanged := before.DurationMinutes != after.DurationMinutes
|
||||
if durationChanged {
|
||||
after.Review, after.Composed = "", ""
|
||||
}
|
||||
after.Quality = nil
|
||||
after.FlowSnapshot = nil
|
||||
after.AgentSnapshots = nil
|
||||
for i := range after.Segments {
|
||||
if durationChanged {
|
||||
after.Segments[i].Review = ""
|
||||
}
|
||||
if after.Segments[i].Status != "skipped" && after.Segments[i].Content != "" {
|
||||
after.Segments[i].Status = "needs_review"
|
||||
}
|
||||
}
|
||||
r.Status = "draft"
|
||||
if len(after.Segments) > 0 {
|
||||
r.Status = "needs_review"
|
||||
}
|
||||
} else if firstChanged >= 0 {
|
||||
invalidateTask(&after, r, firstChanged)
|
||||
}
|
||||
r.Data = resourceData(r, after)
|
||||
default:
|
||||
r.Data = resourceData(r, request.Data)
|
||||
}
|
||||
r.Name = strings.TrimSpace(request.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) Jobs(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
r, err := h.service.get(ctx, c.Param("kind"), c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
q, err := scope(ctx, h.service.db.WithContext(ctx).Model(&Job{}))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
q = q.Where("resource_id = ? AND kind = ?", r.ID, r.Kind)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 50
|
||||
}
|
||||
var total int64
|
||||
if err = q.Count(&total).Error; err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
rows := []Job{}
|
||||
if err = q.Order("created_at DESC, id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true, "data": rows, "total": total})
|
||||
}
|
||||
func (h *Handler) Job(c *gin.Context) {
|
||||
q, e := scope(c.Request.Context(), h.service.db.WithContext(c.Request.Context()))
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
var j Job
|
||||
if e = q.Where("id = ?", c.Param("id")).First(&j).Error; e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, j)
|
||||
}
|
||||
func (h *Handler) Notifications(c *gin.Context) {
|
||||
t, u, _, e := identity(c.Request.Context())
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
rows := []Event{}
|
||||
if e = h.service.db.WithContext(c.Request.Context()).Where("tenant_id = ? AND owner_id = ?", t, u).Order("created_at DESC").Limit(100).Find(&rows).Error; e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
result(c, rows)
|
||||
}
|
||||
func (h *Handler) ReadAll(c *gin.Context) {
|
||||
t, u, _, e := identity(c.Request.Context())
|
||||
if e != nil {
|
||||
respondError(c, e)
|
||||
return
|
||||
}
|
||||
q := h.service.db.WithContext(c.Request.Context()).Model(&Event{}).Where("tenant_id = ? AND owner_id = ? AND read = ?", t, u, false).Update("read", true)
|
||||
if q.Error != nil {
|
||||
respondError(c, q.Error)
|
||||
return
|
||||
}
|
||||
result(c, gin.H{"updated": q.RowsAffected})
|
||||
}
|
||||
Reference in New Issue
Block a user