feat: 方案100%匹配 — RBAC动态权限+Grafana监控+目录库独立微服务+全国目录中心+标识同步

This commit is contained in:
selfrelease
2026-06-30 13:42:11 +08:00
parent 96e2393c50
commit 29a6b57533
40 changed files with 4159 additions and 7 deletions
+160
View File
@@ -0,0 +1,160 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/tcs-iptv/tcs/internal/httpx"
"github.com/tcs-iptv/tcs/internal/model"
"github.com/tcs-iptv/tcs/internal/service"
)
// AdminHandler 用户管理/组织管理 API 处理器(系统管理模块补足)。
type AdminHandler struct {
admin *service.AdminService
}
// NewAdminHandler 创建用户/组织管理 API 处理器。
func NewAdminHandler(admin *service.AdminService) *AdminHandler {
return &AdminHandler{admin: admin}
}
// Register 注册管理路由(应挂载在监管主体权限组下)。
func (h *AdminHandler) Register(rg *gin.RouterGroup) {
// 组织管理
rg.POST("/admin/orgs", h.createOrg)
rg.PUT("/admin/orgs/:id", h.updateOrg)
rg.GET("/admin/orgs", h.listOrgs)
rg.GET("/admin/orgs/:id", h.getOrg)
rg.POST("/admin/orgs/:id/disable", h.disableOrg)
// 用户管理
rg.POST("/admin/users", h.createUser)
rg.PUT("/admin/users/:id", h.updateUser)
rg.GET("/admin/users", h.listUsers)
rg.GET("/admin/users/:id", h.getUser)
rg.POST("/admin/users/:id/disable", h.disableUser)
rg.POST("/admin/users/:id/reset-key", h.resetAPIKey)
}
// ---- 组织管理 handlers ----
func (h *AdminHandler) createOrg(c *gin.Context) {
var org model.Organization
if err := c.ShouldBindJSON(&org); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
result, err := h.admin.CreateOrg(org)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "CREATE_ORG_FAILED", err.Error())
return
}
httpx.Created(c, result)
}
func (h *AdminHandler) updateOrg(c *gin.Context) {
orgID := c.Param("id")
var updates model.Organization
if err := c.ShouldBindJSON(&updates); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
result, err := h.admin.UpdateOrg(orgID, updates)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "UPDATE_ORG_FAILED", err.Error())
return
}
httpx.OK(c, result)
}
func (h *AdminHandler) getOrg(c *gin.Context) {
orgID := c.Param("id")
org, err := h.admin.GetOrg(orgID)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, org)
}
func (h *AdminHandler) listOrgs(c *gin.Context) {
orgType := c.Query("type")
orgs := h.admin.ListOrgs(orgType)
httpx.OK(c, orgs)
}
func (h *AdminHandler) disableOrg(c *gin.Context) {
orgID := c.Param("id")
if err := h.admin.DisableOrg(orgID); err != nil {
httpx.Error(c, http.StatusBadRequest, "DISABLE_ORG_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"status": "disabled"})
}
// ---- 用户管理 handlers ----
func (h *AdminHandler) createUser(c *gin.Context) {
var user model.User
if err := c.ShouldBindJSON(&user); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
result, err := h.admin.CreateUser(user)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "CREATE_USER_FAILED", err.Error())
return
}
httpx.Created(c, result)
}
func (h *AdminHandler) updateUser(c *gin.Context) {
userID := c.Param("id")
var updates model.User
if err := c.ShouldBindJSON(&updates); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
result, err := h.admin.UpdateUser(userID, updates)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "UPDATE_USER_FAILED", err.Error())
return
}
httpx.OK(c, result)
}
func (h *AdminHandler) getUser(c *gin.Context) {
userID := c.Param("id")
user, err := h.admin.GetUser(userID)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, user)
}
func (h *AdminHandler) listUsers(c *gin.Context) {
orgID := c.Query("org_id")
role := c.Query("role")
users := h.admin.ListUsers(orgID, role)
httpx.OK(c, users)
}
func (h *AdminHandler) disableUser(c *gin.Context) {
userID := c.Param("id")
if err := h.admin.DisableUser(userID); err != nil {
httpx.Error(c, http.StatusBadRequest, "DISABLE_USER_FAILED", err.Error())
return
}
httpx.OK(c, gin.H{"status": "disabled"})
}
func (h *AdminHandler) resetAPIKey(c *gin.Context) {
userID := c.Param("id")
user, err := h.admin.ResetAPIKey(userID)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "RESET_KEY_FAILED", err.Error())
return
}
httpx.OK(c, user)
}
+99
View File
@@ -0,0 +1,99 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/tcs-iptv/tcs/internal/catalog"
"github.com/tcs-iptv/tcs/internal/httpx"
)
// CatalogHandler 目录库独立微服务 API 处理器。
// 提供四维度标识查询接口,可作为独立服务部署。
type CatalogHandler struct {
cat *catalog.Catalog
}
// NewCatalogHandler 创建目录库 API 处理器。
func NewCatalogHandler(cat *catalog.Catalog) *CatalogHandler {
return &CatalogHandler{cat: cat}
}
// Register 注册目录库查询路由。
func (h *CatalogHandler) Register(rg *gin.RouterGroup) {
rg.GET("/catalog/query-by-ma", h.queryByMA)
rg.GET("/catalog/query-by-hash", h.queryByHash)
rg.GET("/catalog/query-by-provincial-code", h.queryByProvincialCode)
rg.GET("/catalog/query-by-library-id", h.queryByLibraryID)
rg.GET("/catalog/query-all", h.queryAll)
}
func (h *CatalogHandler) queryByMA(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code 参数")
return
}
result, err := h.cat.QueryByMA(maCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, result)
}
func (h *CatalogHandler) queryByHash(c *gin.Context) {
fileHash := c.Query("file_hash")
if fileHash == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 file_hash 参数")
return
}
result, err := h.cat.QueryByHash(fileHash)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, result)
}
func (h *CatalogHandler) queryByProvincialCode(c *gin.Context) {
code := c.Query("provincial_code")
if code == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 provincial_code 参数")
return
}
result, err := h.cat.QueryByProvincialCode(code)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, result)
}
func (h *CatalogHandler) queryByLibraryID(c *gin.Context) {
libraryID := c.Query("library_id")
if libraryID == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 library_id 参数")
return
}
result, err := h.cat.QueryByLibraryFileID(libraryID)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, result)
}
func (h *CatalogHandler) queryAll(c *gin.Context) {
maCode := c.Query("ma_code")
if maCode == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 ma_code 参数")
return
}
result, err := h.cat.QueryAll(maCode)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, result)
}
@@ -0,0 +1,108 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/catalog"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/model"
)
func TestCatalogHandler_QueryByMA(t *testing.T) {
gin.SetMode(gin.TestMode)
c := chain.NewMemoryChain()
// 发码
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000001", ContentTwinID: "ctid-cat-svc-001",
FileHash: "fh-cat-svc-001", MerkleRoot: "mr-cat-svc-001",
Content: model.Content{Title: "目录库服务测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
cat := catalog.New(c)
h := NewCatalogHandler(cat)
r := gin.New()
rg := r.Group("/api/v1")
h.Register(rg)
// 按 MA 码查询
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/catalog/query-by-ma?ma_code=MA.156.8531.6101/WD/20260000001", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp struct {
Code string `json:"code"`
Data model.ContentQueryResult `json:"data"`
}
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, "SUCCESS", resp.Code)
assert.True(t, resp.Data.Found)
assert.Equal(t, "目录库服务测试", resp.Data.Content.Title)
}
func TestCatalogHandler_QueryByHash(t *testing.T) {
gin.SetMode(gin.TestMode)
c := chain.NewMemoryChain()
_, err := c.IssueMA(chain.RoleRegulator, chain.IssueRequest{
MACode: "MA.156.8531.6101/WD/20260000002", ContentTwinID: "ctid-cat-svc-002",
FileHash: "fh-cat-svc-002", MerkleRoot: "mr-cat-svc-002",
Content: model.Content{Title: "Hash查询服务测试", MAType: "WD", Issuer: "测试局"},
})
require.NoError(t, err)
cat := catalog.New(c)
h := NewCatalogHandler(cat)
r := gin.New()
rg := r.Group("/api/v1")
h.Register(rg)
// 按 Hash 查询
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/catalog/query-by-hash?file_hash=fh-cat-svc-002", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp struct {
Code string `json:"code"`
Data model.ContentQueryResult `json:"data"`
}
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Data.Found)
assert.Equal(t, "MA.156.8531.6101/WD/20260000002", resp.Data.Content.MACode)
// 缺少参数
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest("GET", "/api/v1/catalog/query-by-hash", nil)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusBadRequest, w2.Code)
}
func TestCatalogHandler_NotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
c := chain.NewMemoryChain()
cat := catalog.New(c)
h := NewCatalogHandler(cat)
r := gin.New()
rg := r.Group("/api/v1")
h.Register(rg)
// 不存在的 MA 码
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/catalog/query-by-ma?ma_code=nonexistent", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
+86
View File
@@ -60,6 +60,13 @@ func (h *Handler) Register(rg *gin.RouterGroup) {
rg.GET("/regulatory/national-stats", h.nationalStats) // 全国监管统计(三期F.2
rg.GET("/regulatory/daily-report", h.dailyReport) // 监管数据上报日报(三期A.2
rg.GET("/admin/segments", h.listSegments) // 号段管理(三期B.1
// ---- 多维度标识查询(方案第一阶段标识查询接口补足)----
rg.GET("/content/query-by-hash", h.queryByHash) // 按 Hash 反查标识信息
rg.GET("/content/query-by-provincial-code", h.queryByProvincialCode) // 按省级内容编码反查
rg.GET("/content/query-by-library-id", h.queryByLibraryID) // 按片库文件ID反查
// ---- MA 合并/拆分(方案 MA 管理模块补足)----
rg.POST("/content/merge", h.mergeMA) // MA 码合并(仅监管)
rg.POST("/content/split", h.splitMA) // MA 码拆分(仅监管)
// ---- 四期:大小屏融合(跨域解析/扫码验真/跨屏权益)----
rg.GET("/content/resolve", h.resolve) // MA 跨域解析网关(C.1/C.2
rg.POST("/content/scan-verify", h.scanVerify) // 用户扫码验真(B.2
@@ -744,3 +751,82 @@ func (h *Handler) verifyRights(c *gin.Context) {
}
httpx.OK(c, h.svc.VerifyCrossScreenRights(req.MACode, req.UserHash, model.ScreenType(req.Screen)))
}
// ---- 多维度标识查询 handlers ----
// queryByHash 按内容哈希反查标识信息。
func (h *Handler) queryByHash(c *gin.Context) {
fileHash := c.Query("file_hash")
if fileHash == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 file_hash 参数")
return
}
res, err := h.svc.QueryByHash(fileHash)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// queryByProvincialCode 按省级内容编码反查标识信息。
func (h *Handler) queryByProvincialCode(c *gin.Context) {
code := c.Query("provincial_code")
if code == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 provincial_code 参数")
return
}
res, err := h.svc.QueryByProvincialCode(code)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// queryByLibraryID 按片库文件 ID 反查标识信息。
func (h *Handler) queryByLibraryID(c *gin.Context) {
libraryID := c.Query("library_file_id")
if libraryID == "" {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "缺少 library_file_id 参数")
return
}
res, err := h.svc.QueryByLibraryFileID(libraryID)
if err != nil {
httpx.Error(c, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
httpx.OK(c, res)
}
// ---- MA 合并/拆分 handlers ----
// mergeMA MA 码合并(仅监管主体)。
func (h *Handler) mergeMA(c *gin.Context) {
var req model.MergeRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.MergeMACodes(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "MERGE_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
// splitMA MA 码拆分(仅监管主体)。
func (h *Handler) splitMA(c *gin.Context) {
var req model.SplitRequest
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
res, err := h.svc.SplitMACode(roleOf(c), req)
if err != nil {
httpx.Error(c, http.StatusBadRequest, "SPLIT_FAILED", err.Error())
return
}
httpx.OK(c, res)
}
+82
View File
@@ -0,0 +1,82 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/tcs-iptv/tcs/internal/httpx"
)
// RBACHandler RBAC 权限管理 API 处理器。
type RBACHandler struct {
rbac *httpx.RBACManager
}
// NewRBACHandler 创建 RBAC 管理 API 处理器。
func NewRBACHandler(rbac *httpx.RBACManager) *RBACHandler {
return &RBACHandler{rbac: rbac}
}
// Register 注册 RBAC 管理路由(应挂载在监管主体权限组下)。
func (h *RBACHandler) Register(rg *gin.RouterGroup) {
rg.GET("/admin/rbac/roles", h.listRoles)
rg.GET("/admin/rbac/permissions/:role", h.listPermissions)
rg.PUT("/admin/rbac/roles/:role", h.setRolePermissions)
rg.POST("/admin/rbac/grant", h.grantPermission)
rg.POST("/admin/rbac/revoke", h.revokePermission)
}
func (h *RBACHandler) listRoles(c *gin.Context) {
roles := h.rbac.ListRoles()
httpx.OK(c, roles)
}
func (h *RBACHandler) listPermissions(c *gin.Context) {
role := c.Param("role")
perms := h.rbac.ListPermissions(role)
httpx.OK(c, perms)
}
type setPermissionsReq struct {
Permissions []string `json:"permissions"`
}
func (h *RBACHandler) setRolePermissions(c *gin.Context) {
role := c.Param("role")
var req setPermissionsReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
perms := make([]httpx.Permission, len(req.Permissions))
for i, p := range req.Permissions {
perms[i] = httpx.Permission(p)
}
h.rbac.SetRolePermissions(role, perms)
httpx.OK(c, gin.H{"role": role, "permissions": req.Permissions})
}
type grantRevokeReq struct {
Role string `json:"role"`
Permission string `json:"permission"`
}
func (h *RBACHandler) grantPermission(c *gin.Context) {
var req grantRevokeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
h.rbac.GrantPermission(req.Role, httpx.Permission(req.Permission))
httpx.OK(c, gin.H{"role": req.Role, "permission": req.Permission, "granted": true})
}
func (h *RBACHandler) revokePermission(c *gin.Context) {
var req grantRevokeReq
if err := c.ShouldBindJSON(&req); err != nil {
httpx.Error(c, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
return
}
h.rbac.RevokePermission(req.Role, httpx.Permission(req.Permission))
httpx.OK(c, gin.H{"role": req.Role, "permission": req.Permission, "revoked": true})
}