merge: 合入本地 Ops 扩展与 origin/main(ECR-009–016)

保留远程用户侧 ECR-009–016 与本地 Ops 目录/RBAC/CMS/危机等能力;文档标注分叉期间 ECR 编号冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-13 01:45:53 +08:00
co-authored by Cursor
593 changed files with 21918 additions and 328 deletions
+39 -12
View File
@@ -30,22 +30,49 @@ func (h *AdminHandler) Register(api *gin.RouterGroup) {
authed.Use(middleware.AdminAuth(h.Svc))
authed.POST("/auth/logout", h.Logout)
authed.GET("/me", h.Me)
authed.GET("/stats", h.Stats)
authed.GET("/users", h.ListUsers)
authed.GET("/users/:id", h.GetUser)
authed.POST("/users/:id/membership/grant", h.GrantMembership)
authed.POST("/users/:id/ask-quota/grant", h.GrantAskQuota)
authed.GET("/orders", h.ListOrders)
authed.GET("/stats", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.Stats)
authed.GET("/users", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.ListUsers)
authed.GET("/users/:id", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.GetUser)
authed.POST("/users/:id/membership/grant", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipGrant), h.GrantMembership)
authed.POST("/users/:id/ask-quota/grant", middleware.RequireAdminPermission(h.Svc, admin.PermAskQuotaGrant), h.GrantAskQuota)
authed.GET("/orders", middleware.RequireAdminPermission(h.Svc, admin.PermOrdersRead), h.ListOrders)
authed.GET("/membership/plan-prices", h.ListPlanPrices)
authed.PUT("/membership/plan-prices", h.PutPlanPrices)
authed.GET("/audit-logs", h.ListAudit)
authed.GET("/analytics/overview", h.AnalyticsOverview)
authed.GET("/analytics/pages", h.AnalyticsPages)
authed.GET("/analytics/exits", h.AnalyticsExits)
authed.GET("/analytics/clicks", h.AnalyticsClicks)
authed.GET("/analytics/funnel", h.AnalyticsFunnel)
authed.GET("/audit-logs", middleware.RequireAdminPermission(h.Svc, admin.PermAuditRead), h.ListAudit)
authed.GET("/analytics/overview", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.AnalyticsOverview)
authed.GET("/analytics/pages", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.AnalyticsPages)
authed.GET("/analytics/exits", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.AnalyticsExits)
authed.GET("/analytics/clicks", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.AnalyticsClicks)
authed.GET("/analytics/funnel", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.AnalyticsFunnel)
h.registerContent(authed)
h.registerSystem(authed)
h.registerRBAC(authed)
h.registerLifecycle(authed)
h.registerMembershipPlans(authed)
h.registerRedemption(authed)
h.registerInsight(authed)
h.registerAskOps(authed)
h.registerQualityFeedback(authed)
h.registerEntitlement(authed)
h.registerContentSafety(authed)
h.registerAIConfig(authed)
h.registerCrisis(authed)
h.registerCMS(authed)
h.registerCMSPublications(authed)
h.registerKnowledgeChunks(authed)
h.registerToolDefinitions(authed)
h.registerBlockPolicies(authed)
h.registerModerationCases(authed)
h.registerCrisisEvents(authed)
h.registerInterventionOutcomes(authed)
h.registerHandoffCases(authed)
h.registerPrivacyRequests(authed)
h.registerStarConfigs(authed)
h.registerRhythmConfigs(authed)
h.registerImageCardDecks(authed)
h.registerReportTemplates(authed)
h.registerFunnelDefinitions(authed)
h.registerExploreScales(authed)
}
func (h *AdminHandler) Login(c *gin.Context) {
@@ -0,0 +1,75 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerAIConfig(authed *gin.RouterGroup) {
g := authed.Group("/ai")
g.GET("/system-prompts", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.ListSystemPrompts)
g.GET("/system-prompts/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.GetSystemPrompt)
g.GET("/knowledge-sources", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.ListKnowledgeSources)
g.GET("/knowledge-sources/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.GetKnowledgeSource)
}
func (h *AdminHandler) ListSystemPrompts(c *gin.Context) {
items, err := h.Svc.ListSystemPrompts(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50027, "list system prompts failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetSystemPrompt(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetSystemPrompt(c.Request.Context(), id)
if errors.Is(err, admin.ErrSystemPromptNotFound) {
response.Fail(c, http.StatusNotFound, 40404, "system prompt not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50028, "get system prompt failed")
return
}
response.OK(c, row)
}
func (h *AdminHandler) ListKnowledgeSources(c *gin.Context) {
items, err := h.Svc.ListKnowledgeSources(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50029, "list knowledge sources failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetKnowledgeSource(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetKnowledgeSource(c.Request.Context(), id)
if errors.Is(err, admin.ErrKnowledgeSourceNotFound) {
response.Fail(c, http.StatusNotFound, 40405, "knowledge source not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50030, "get knowledge source failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,57 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerAskOps(authed *gin.RouterGroup) {
authed.GET("/ask/threads", middleware.RequireAdminPermission(h.Svc, admin.PermAskRead), h.ListAskThreads)
authed.GET("/ask/threads/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAskRead), h.GetAskThread)
}
func (h *AdminHandler) ListAskThreads(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
var userID *uuid.UUID
if q := c.Query("user_id"); q != "" {
id, err := uuid.Parse(q)
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid user_id")
return
}
userID = &id
}
items, err := h.Svc.ListAskSessions(c.Request.Context(), userID, limit, offset)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50019, "list ask threads failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetAskThread(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid thread id")
return
}
detail, err := h.Svc.GetAskSessionDetail(c.Request.Context(), id)
if errors.Is(err, admin.ErrAskThreadNotFound) {
response.Fail(c, http.StatusNotFound, 40402, "ask thread not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50020, "get ask thread failed")
return
}
response.OK(c, detail)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerBlockPolicies(authed *gin.RouterGroup) {
g := authed.Group("/content-safety")
g.GET("/block-policies", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.ListBlockPolicies)
g.GET("/block-policies/:id", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.GetBlockPolicy)
}
func (h *AdminHandler) ListBlockPolicies(c *gin.Context) {
items, err := h.Svc.ListBlockPolicies(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list block-policy failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetBlockPolicy(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetBlockPolicy(c.Request.Context(), id)
if errors.Is(err, admin.ErrBlockPolicyNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "block-policy not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get block-policy failed")
return
}
response.OK(c, row)
}
+75
View File
@@ -0,0 +1,75 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerCMS(authed *gin.RouterGroup) {
g := authed.Group("/cms")
g.GET("/banners", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.ListBanners)
g.GET("/banners/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.GetBanner)
g.GET("/feed-slots", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.ListFeedSlots)
g.GET("/feed-slots/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.GetFeedSlot)
}
func (h *AdminHandler) ListBanners(c *gin.Context) {
items, err := h.Svc.ListBanners(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50040, "list banners failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetBanner(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetBanner(c.Request.Context(), id)
if errors.Is(err, admin.ErrBannerNotFound) {
response.Fail(c, http.StatusNotFound, 40410, "banner not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50041, "get banner failed")
return
}
response.OK(c, row)
}
func (h *AdminHandler) ListFeedSlots(c *gin.Context) {
items, err := h.Svc.ListFeedSlots(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50042, "list feed slots failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetFeedSlot(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetFeedSlot(c.Request.Context(), id)
if errors.Is(err, admin.ErrFeedSlotNotFound) {
response.Fail(c, http.StatusNotFound, 40411, "feed slot not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50043, "get feed slot failed")
return
}
response.OK(c, row)
}
+4 -4
View File
@@ -14,10 +14,10 @@ import (
)
func (h *AdminHandler) registerContent(authed *gin.RouterGroup) {
authed.GET("/home/tools", h.ListHomeTools)
authed.PUT("/home/tools", h.ReplaceHomeTools)
authed.GET("/scales", h.ListScales)
authed.PATCH("/scales/:id", h.PatchScale)
authed.GET("/home/tools", middleware.RequireAdminPermission(h.Svc, admin.PermContentWrite), h.ListHomeTools)
authed.PUT("/home/tools", middleware.RequireAdminPermission(h.Svc, admin.PermContentWrite), h.ReplaceHomeTools)
authed.GET("/scales", middleware.RequireAdminPermission(h.Svc, admin.PermContentWrite), h.ListScales)
authed.PATCH("/scales/:id", middleware.RequireAdminPermission(h.Svc, admin.PermContentWrite), h.PatchScale)
}
func (h *AdminHandler) ListHomeTools(c *gin.Context) {
@@ -0,0 +1,63 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerContentSafety(authed *gin.RouterGroup) {
g := authed.Group("/content-safety")
g.GET("/filter-rules", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.ListFilterRules)
g.GET("/filter-rules/:id", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.GetFilterRule)
g.POST("/evaluate", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.EvaluateContent)
}
func (h *AdminHandler) ListFilterRules(c *gin.Context) {
items, err := h.Svc.ListFilterRules(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50022, "list filter rules failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetFilterRule(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetFilterRule(c.Request.Context(), id)
if errors.Is(err, admin.ErrFilterRuleNotFound) {
response.Fail(c, http.StatusNotFound, 40403, "filter rule not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50023, "get filter rule failed")
return
}
response.OK(c, row)
}
func (h *AdminHandler) EvaluateContent(c *gin.Context) {
var body struct {
Text string `json:"text"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
matches, err := h.Svc.EvaluateContent(c.Request.Context(), body.Text)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50024, "evaluate failed")
return
}
response.OK(c, gin.H{"matches": matches})
}
+63
View File
@@ -0,0 +1,63 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerCrisis(authed *gin.RouterGroup) {
g := authed.Group("/crisis")
g.GET("/policies", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.ListCrisisPolicies)
g.GET("/policies/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.GetCrisisPolicy)
g.POST("/evaluate", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.EvaluateCrisis)
}
func (h *AdminHandler) ListCrisisPolicies(c *gin.Context) {
items, err := h.Svc.ListCrisisPolicies(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50029, "list crisis policies failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetCrisisPolicy(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetCrisisPolicy(c.Request.Context(), id)
if errors.Is(err, admin.ErrCrisisPolicyNotFound) {
response.Fail(c, http.StatusNotFound, 40405, "crisis policy not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50030, "get crisis policy failed")
return
}
response.OK(c, row)
}
func (h *AdminHandler) EvaluateCrisis(c *gin.Context) {
var body struct {
Text string `json:"text"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
matches, err := h.Svc.EvaluateCrisis(c.Request.Context(), body.Text)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50031, "evaluate failed")
return
}
response.OK(c, gin.H{"matches": matches})
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerCrisisEvents(authed *gin.RouterGroup) {
g := authed.Group("/crisis")
g.GET("/events", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.ListCrisisEvents)
g.GET("/events/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.GetCrisisEvent)
}
func (h *AdminHandler) ListCrisisEvents(c *gin.Context) {
items, err := h.Svc.ListCrisisEvents(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list crisis-event failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetCrisisEvent(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetCrisisEvent(c.Request.Context(), id)
if errors.Is(err, admin.ErrCrisisEventNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "crisis-event not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get crisis-event failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,35 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerEntitlement(authed *gin.RouterGroup) {
authed.GET("/users/:id/entitlements", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.GetUserEntitlements)
}
func (h *AdminHandler) GetUserEntitlements(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid user id")
return
}
ent, err := h.Svc.GetUserEntitlement(c.Request.Context(), id)
if errors.Is(err, admin.ErrUserNotFound) {
response.Fail(c, http.StatusNotFound, 40401, "user not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50021, "get entitlements failed")
return
}
response.OK(c, ent)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerExploreScales(authed *gin.RouterGroup) {
g := authed.Group("/explore")
g.GET("/scales", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.ListExploreScales)
g.GET("/scales/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.GetExploreScale)
}
func (h *AdminHandler) ListExploreScales(c *gin.Context) {
items, err := h.Svc.ListScalesAdmin(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50060, "list explore scales failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetExploreScale(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetScaleAdmin(c.Request.Context(), id)
if errors.Is(err, admin.ErrScaleNotFound) {
response.Fail(c, http.StatusNotFound, 40430, "scale not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50061, "get explore scale failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerFunnelDefinitions(authed *gin.RouterGroup) {
g := authed.Group("/analytics")
g.GET("/funnel-definitions", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.ListFunnelDefinitions)
g.GET("/funnel-definitions/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAnalyticsRead), h.GetFunnelDefinition)
}
func (h *AdminHandler) ListFunnelDefinitions(c *gin.Context) {
items, err := h.Svc.ListFunnelDefinitions(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list funnel-definition failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetFunnelDefinition(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetFunnelDefinition(c.Request.Context(), id)
if errors.Is(err, admin.ErrFunnelDefinitionNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "funnel-definition not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get funnel-definition failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerHandoffCases(authed *gin.RouterGroup) {
g := authed.Group("/ask")
g.GET("/handoffs", middleware.RequireAdminPermission(h.Svc, admin.PermAskRead), h.ListHandoffCases)
g.GET("/handoffs/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAskRead), h.GetHandoffCase)
}
func (h *AdminHandler) ListHandoffCases(c *gin.Context) {
items, err := h.Svc.ListHandoffCases(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list handoff-case failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetHandoffCase(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetHandoffCase(c.Request.Context(), id)
if errors.Is(err, admin.ErrHandoffCaseNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "handoff-case not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get handoff-case failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerImageCardDecks(authed *gin.RouterGroup) {
g := authed.Group("/explore")
g.GET("/image-card-decks", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.ListImageCardDecks)
g.GET("/image-card-decks/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.GetImageCardDeck)
}
func (h *AdminHandler) ListImageCardDecks(c *gin.Context) {
items, err := h.Svc.ListImageCardDecks(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list image-card-deck failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetImageCardDeck(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetImageCardDeck(c.Request.Context(), id)
if errors.Is(err, admin.ErrImageCardDeckNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "image-card-deck not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get image-card-deck failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,35 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerInsight(authed *gin.RouterGroup) {
authed.GET("/users/:id/insight", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.GetUserInsight)
}
func (h *AdminHandler) GetUserInsight(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid user id")
return
}
insight, err := h.Svc.GetUserInsight(c.Request.Context(), id)
if errors.Is(err, admin.ErrUserNotFound) {
response.Fail(c, http.StatusNotFound, 40401, "user not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50018, "get insight failed")
return
}
response.OK(c, insight)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerInterventionOutcomes(authed *gin.RouterGroup) {
g := authed.Group("/crisis")
g.GET("/interventions", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.ListInterventionOutcomes)
g.GET("/interventions/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCrisisRead), h.GetInterventionOutcome)
}
func (h *AdminHandler) ListInterventionOutcomes(c *gin.Context) {
items, err := h.Svc.ListInterventionOutcomes(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list intervention-outcome failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetInterventionOutcome(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetInterventionOutcome(c.Request.Context(), id)
if errors.Is(err, admin.ErrInterventionOutcomeNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "intervention-outcome not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get intervention-outcome failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerKnowledgeChunks(authed *gin.RouterGroup) {
g := authed.Group("/ai")
g.GET("/knowledge-chunks", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.ListKnowledgeChunks)
g.GET("/knowledge-chunks/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.GetKnowledgeChunk)
}
func (h *AdminHandler) ListKnowledgeChunks(c *gin.Context) {
items, err := h.Svc.ListKnowledgeChunks(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list knowledge-chunk failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetKnowledgeChunk(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetKnowledgeChunk(c.Request.Context(), id)
if errors.Is(err, admin.ErrKnowledgeChunkNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "knowledge-chunk not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get knowledge-chunk failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,78 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerLifecycle(authed *gin.RouterGroup) {
authed.POST("/users/:id/status", middleware.RequireAdminPermission(h.Svc, admin.PermUsersStatusWrite), h.PostUserStatus)
authed.GET("/users/:id/status-transitions", middleware.RequireAdminPermission(h.Svc, admin.PermUsersRead), h.ListUserStatusTransitions)
}
func (h *AdminHandler) PostUserStatus(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
var body struct {
Status string `json:"status"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Status == "" {
response.Fail(c, http.StatusBadRequest, 40000, "status required")
return
}
err = h.Svc.TransitionUserStatus(c.Request.Context(), adminID, userID, body.Status, body.Reason)
if errors.Is(err, admin.ErrUserNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "user not found")
return
}
if errors.Is(err, admin.ErrReasonRequired) || errors.Is(err, admin.ErrInvalidStatusEdge) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
detail, err := h.Svc.GetUser(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, detail)
}
func (h *AdminHandler) ListUserStatusTransitions(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
items, err := h.Svc.ListStatusTransitions(c.Request.Context(), userID, limit)
if errors.Is(err, admin.ErrUserNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "user not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
@@ -0,0 +1,78 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerMembershipPlans(authed *gin.RouterGroup) {
authed.GET("/membership-plans", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipPlansRead), h.ListMembershipPlans)
authed.GET("/membership-plans/:code", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipPlansRead), h.GetMembershipPlan)
authed.PUT("/membership-plans/:code", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipPlansWrite), h.PutMembershipPlan)
}
func (h *AdminHandler) ListMembershipPlans(c *gin.Context) {
items, err := h.Svc.ListMembershipPlans(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetMembershipPlan(c *gin.Context) {
plan, err := h.Svc.GetMembershipPlan(c.Request.Context(), c.Param("code"))
if errors.Is(err, admin.ErrPlanNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "plan not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, plan)
}
func (h *AdminHandler) PutMembershipPlan(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
var body struct {
Title string `json:"title"`
DurationDays int `json:"duration_days"`
AmountCents int `json:"amount_cents"`
Active *bool `json:"active"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
active := true
if body.Active != nil {
active = *body.Active
}
plan, err := h.Svc.UpdateMembershipPlan(
c.Request.Context(), adminID, c.Param("code"), body.Title, body.DurationDays, body.AmountCents, active,
)
if errors.Is(err, admin.ErrPlanNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "plan not found")
return
}
if errors.Is(err, admin.ErrInvalidPlanU) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, plan)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerModerationCases(authed *gin.RouterGroup) {
g := authed.Group("/content-safety")
g.GET("/cases", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.ListModerationCases)
g.GET("/cases/:id", middleware.RequireAdminPermission(h.Svc, admin.PermContentSafetyRead), h.GetModerationCase)
}
func (h *AdminHandler) ListModerationCases(c *gin.Context) {
items, err := h.Svc.ListModerationCases(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list moderation-case failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetModerationCase(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetModerationCase(c.Request.Context(), id)
if errors.Is(err, admin.ErrModerationCaseNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "moderation-case not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get moderation-case failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerPrivacyRequests(authed *gin.RouterGroup) {
g := authed.Group("/privacy")
g.GET("/requests", middleware.RequireAdminPermission(h.Svc, admin.PermPrivacyRead), h.ListPrivacyRequests)
g.GET("/requests/:id", middleware.RequireAdminPermission(h.Svc, admin.PermPrivacyRead), h.GetPrivacyRequest)
}
func (h *AdminHandler) ListPrivacyRequests(c *gin.Context) {
items, err := h.Svc.ListPrivacyRequests(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list privacy-request failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetPrivacyRequest(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetPrivacyRequest(c.Request.Context(), id)
if errors.Is(err, admin.ErrPrivacyRequestNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "privacy-request not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get privacy-request failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,76 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerQualityFeedback(authed *gin.RouterGroup) {
authed.GET("/ask/feedback", middleware.RequireAdminPermission(h.Svc, admin.PermAskRead), h.ListAskFeedback)
authed.POST("/ask/threads/:id/feedback", middleware.RequireAdminPermission(h.Svc, admin.PermAskFeedbackWrite), h.CreateAskFeedback)
}
func (h *AdminHandler) ListAskFeedback(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
items, err := h.Svc.ListQualityFeedback(c.Request.Context(), limit, offset)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50025, "list feedback failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) CreateAskFeedback(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
threadID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid thread id")
return
}
var body struct {
Rating int `json:"rating"`
Tag string `json:"tag"`
Note string `json:"note"`
MessageID *string `json:"message_id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
var msgID *uuid.UUID
if body.MessageID != nil && *body.MessageID != "" {
id, err := uuid.Parse(*body.MessageID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid message_id")
return
}
msgID = &id
}
row, err := h.Svc.CreateQualityFeedback(c.Request.Context(), adminID, threadID, msgID, body.Rating, body.Tag, body.Note)
if errors.Is(err, admin.ErrBadFeedbackRating) || errors.Is(err, admin.ErrBadFeedbackTag) || errors.Is(err, admin.ErrFeedbackNoteLong) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if errors.Is(err, admin.ErrAskThreadNotFound) {
response.Fail(c, http.StatusNotFound, 40402, "ask thread not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50026, "create feedback failed")
return
}
response.OK(c, row)
}
+88
View File
@@ -0,0 +1,88 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerRBAC(authed *gin.RouterGroup) {
authed.GET("/roles", middleware.RequireAdminPermission(h.Svc, admin.PermRolesRead), h.ListRoles)
authed.GET("/roles/:id", middleware.RequireAdminPermission(h.Svc, admin.PermRolesRead), h.GetRole)
authed.PUT("/roles/:id/permissions", middleware.RequireAdminPermission(h.Svc, admin.PermRolesWrite), h.PutRolePermissions)
}
func (h *AdminHandler) ListRoles(c *gin.Context) {
items, err := h.Svc.ListRoles(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetRole(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
role, err := h.Svc.GetRole(c.Request.Context(), id)
if errors.Is(err, admin.ErrRoleNotFound) || role == nil {
response.Fail(c, http.StatusNotFound, 40400, "role not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, role)
}
func (h *AdminHandler) PutRolePermissions(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
roleID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
var body struct {
Permissions []string `json:"permissions"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
if body.Permissions == nil {
body.Permissions = []string{}
}
err = h.Svc.ReplaceRolePermissions(c.Request.Context(), adminID, roleID, body.Permissions)
if errors.Is(err, admin.ErrRoleNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "role not found")
return
}
if errors.Is(err, admin.ErrInvalidPerm) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
role, err := h.Svc.GetRole(c.Request.Context(), roleID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, role)
}
@@ -0,0 +1,99 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerRedemption(authed *gin.RouterGroup) {
authed.POST("/redemption-batches", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipCodesWrite), h.CreateRedemptionBatch)
authed.GET("/redemption-batches", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipCodesRead), h.ListRedemptionBatches)
authed.GET("/redemption-batches/:id/codes", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipCodesRead), h.ListRedemptionCodes)
authed.POST("/redemption-codes/:id/disable", middleware.RequireAdminPermission(h.Svc, admin.PermMembershipCodesWrite), h.DisableRedemptionCode)
}
func (h *AdminHandler) CreateRedemptionBatch(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
var body struct {
Label string `json:"label"`
PlanCode string `json:"plan_code"`
Quantity int `json:"quantity"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid body")
return
}
batch, codes, err := h.Svc.CreateRedemptionBatch(c.Request.Context(), adminID, body.Label, body.PlanCode, body.Quantity)
if errors.Is(err, admin.ErrBadBatchQty) || errors.Is(err, admin.ErrPlanNotFound) || errors.Is(err, admin.ErrInvalidPlanU) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"batch": batch, "codes": codes})
}
func (h *AdminHandler) ListRedemptionBatches(c *gin.Context) {
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
items, err := h.Svc.ListRedemptionBatches(c.Request.Context(), limit)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) ListRedemptionCodes(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
items, err := h.Svc.ListRedemptionCodes(c.Request.Context(), id)
if errors.Is(err, admin.ErrBatchNotFound) {
response.Fail(c, http.StatusNotFound, 40400, "batch not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) DisableRedemptionCode(c *gin.Context) {
adminID, ok := middleware.AdminIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40102, "admin session invalid")
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, "invalid id")
return
}
err = h.Svc.DisableRedemptionCode(c.Request.Context(), adminID, id)
if errors.Is(err, admin.ErrCodeDisable) {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"ok": true})
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerReportTemplates(authed *gin.RouterGroup) {
g := authed.Group("/growth")
g.GET("/report-templates", middleware.RequireAdminPermission(h.Svc, admin.PermGrowthRead), h.ListReportTemplates)
g.GET("/report-templates/:id", middleware.RequireAdminPermission(h.Svc, admin.PermGrowthRead), h.GetReportTemplate)
}
func (h *AdminHandler) ListReportTemplates(c *gin.Context) {
items, err := h.Svc.ListReportTemplates(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list report-template failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetReportTemplate(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetReportTemplate(c.Request.Context(), id)
if errors.Is(err, admin.ErrReportTemplateNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "report-template not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get report-template failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerRhythmConfigs(authed *gin.RouterGroup) {
g := authed.Group("/explore")
g.GET("/rhythm-configs", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.ListRhythmConfigs)
g.GET("/rhythm-configs/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.GetRhythmConfig)
}
func (h *AdminHandler) ListRhythmConfigs(c *gin.Context) {
items, err := h.Svc.ListRhythmConfigs(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list rhythm-config failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetRhythmConfig(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetRhythmConfig(c.Request.Context(), id)
if errors.Is(err, admin.ErrRhythmConfigNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "rhythm-config not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get rhythm-config failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerCMSPublications(authed *gin.RouterGroup) {
g := authed.Group("/cms")
g.GET("/publications", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.ListScheduledPublications)
g.GET("/publications/:id", middleware.RequireAdminPermission(h.Svc, admin.PermCMSRead), h.GetScheduledPublication)
}
func (h *AdminHandler) ListScheduledPublications(c *gin.Context) {
items, err := h.Svc.ListScheduledPublications(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list scheduled-publication failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetScheduledPublication(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetScheduledPublication(c.Request.Context(), id)
if errors.Is(err, admin.ErrScheduledPublicationNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "scheduled-publication not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get scheduled-publication failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerStarConfigs(authed *gin.RouterGroup) {
g := authed.Group("/explore")
g.GET("/star-configs", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.ListStarConfigs)
g.GET("/star-configs/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.GetStarConfig)
}
func (h *AdminHandler) ListStarConfigs(c *gin.Context) {
items, err := h.Svc.ListStarConfigs(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list star-config failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetStarConfig(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetStarConfig(c.Request.Context(), id)
if errors.Is(err, admin.ErrStarConfigNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "star-config not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get star-config failed")
return
}
response.OK(c, row)
}
@@ -0,0 +1,46 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AdminHandler) registerToolDefinitions(authed *gin.RouterGroup) {
g := authed.Group("/ai")
g.GET("/tools", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.ListToolDefinitions)
g.GET("/tools/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.GetToolDefinition)
}
func (h *AdminHandler) ListToolDefinitions(c *gin.Context) {
items, err := h.Svc.ListToolDefinitions(c.Request.Context())
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50050, "list tool-definition failed")
return
}
response.OK(c, gin.H{"items": items})
}
func (h *AdminHandler) GetToolDefinition(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 40002, "invalid id")
return
}
row, err := h.Svc.GetToolDefinition(c.Request.Context(), id)
if errors.Is(err, admin.ErrToolDefinitionNotFound) {
response.Fail(c, http.StatusNotFound, 40420, "tool-definition not found")
return
}
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50051, "get tool-definition failed")
return
}
response.OK(c, row)
}
+1
View File
@@ -28,6 +28,7 @@ func (h *AskHandler) Register(rg *gin.RouterGroup) {
rg.DELETE("/ask/threads/:id", h.ClearThread)
rg.GET("/ask/threads/:id/messages", h.ListMessages)
rg.POST("/ask/threads/:id/messages", h.SendMessage)
h.registerFeedback(rg)
}
// GetQuota handles GET /ask/quota.
+67
View File
@@ -0,0 +1,67 @@
package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
asksvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/ask"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
func (h *AskHandler) registerFeedback(rg *gin.RouterGroup) {
rg.POST("/ask/threads/:id/feedback", h.SubmitFeedback)
}
// SubmitFeedback handles POST /ask/threads/:id/feedback.
func (h *AskHandler) SubmitFeedback(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
threadID, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid thread id")
return
}
var req struct {
Rating int `json:"rating"`
Tag string `json:"tag"`
Note string `json:"note"`
MessageID *string `json:"message_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
var msgID *uuid.UUID
if req.MessageID != nil && *req.MessageID != "" {
id, err := uuid.Parse(*req.MessageID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid message_id")
return
}
msgID = &id
}
row, err := h.Svc.SubmitFeedback(c.Request.Context(), userID, threadID, asksvc.SubmitFeedbackInput{
MessageID: msgID, Rating: req.Rating, Tag: req.Tag, Note: req.Note,
})
if err != nil {
msg := err.Error()
if strings.Contains(msg, "rating") || strings.Contains(msg, "tag") || strings.Contains(msg, "note") {
response.Fail(c, http.StatusBadRequest, 40000, msg)
return
}
if strings.Contains(msg, "thread not found") {
response.Fail(c, http.StatusNotFound, 40410, msg)
return
}
response.Fail(c, http.StatusInternalServerError, 50000, msg)
return
}
response.OK(c, row)
}
+9
View File
@@ -1,6 +1,7 @@
package handler
import (
"errors"
"net/http"
"strings"
@@ -51,6 +52,10 @@ func (h *AuthHandler) RegisterAccount(c *gin.Context) {
if failTextCompliance(c, err) {
return
}
if errors.Is(err, auth.ErrAccountRestricted) {
response.Fail(c, http.StatusUnauthorized, 40113, err.Error())
return
}
response.Fail(c, http.StatusBadRequest, 40110, err.Error())
return
}
@@ -72,6 +77,10 @@ func (h *AuthHandler) Login(c *gin.Context) {
deviceKey := c.GetHeader(middleware.DeviceKeyHeader)
res, err := h.Svc.Login(c.Request.Context(), userID, deviceKey, body.Phone, body.Password)
if err != nil {
if errors.Is(err, auth.ErrAccountRestricted) {
response.Fail(c, http.StatusUnauthorized, 40113, err.Error())
return
}
response.Fail(c, http.StatusBadRequest, 40111, err.Error())
return
}
+32
View File
@@ -37,6 +37,7 @@ func (h *ReportHandler) Register(rg *gin.RouterGroup) {
rg.GET("/reports/latest", h.GetLatest)
rg.GET("/reports/:id", h.Get)
rg.GET("/membership/me", h.GetMembership)
rg.POST("/membership/redeem", h.RedeemCode)
rg.POST("/orders", h.CreateOrder)
rg.POST("/orders/:id/pay-mock", h.PayMock)
}
@@ -257,6 +258,37 @@ func (h *ReportHandler) GetMembership(c *gin.Context) {
response.OK(c, me)
}
// RedeemCode handles POST /membership/redeem.
func (h *ReportHandler) RedeemCode(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
msvc, ok := h.requireMembership(c)
if !ok {
return
}
var body struct {
Code string `json:"code"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Code == "" {
response.Fail(c, http.StatusBadRequest, 40000, "code required")
return
}
plan, err := msvc.Redeem(c.Request.Context(), userID, body.Code)
if err != nil {
response.Fail(c, http.StatusBadRequest, 40000, err.Error())
return
}
me, err := msvc.Get(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"plan": plan, "membership": me})
}
// CreateOrder handles POST /orders.
func (h *ReportHandler) CreateOrder(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)