0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆) - 新增包: license/memory/modelquota/assistant,扫描引擎 - 全部功能后端+前端+端到端验证通过(25 包单测)
This commit is contained in:
@@ -24,6 +24,37 @@ var (
|
||||
type ManagementHTTPHandler struct {
|
||||
service *Service
|
||||
mux *http.ServeMux
|
||||
// license 提供账号数上限管控(nil 时不限制)。
|
||||
license interface {
|
||||
AccountLimit() int
|
||||
}
|
||||
}
|
||||
|
||||
// SetLicenseManager 注入 License 管理器用于账号数管控。
|
||||
func (h *ManagementHTTPHandler) SetLicenseManager(manager interface{ AccountLimit() int }) {
|
||||
h.license = manager
|
||||
}
|
||||
|
||||
// checkAccountLimit 在创建账号前校验 License 账号数上限。
|
||||
func (h *ManagementHTTPHandler) checkAccountLimit(writer http.ResponseWriter, request *http.Request) bool {
|
||||
if h.license == nil {
|
||||
return true
|
||||
}
|
||||
limit := h.license.AccountLimit()
|
||||
if limit <= 0 {
|
||||
return true // 不限
|
||||
}
|
||||
var total int
|
||||
err := h.service.repository.CountIdentities(request.Context(), &total)
|
||||
if err != nil {
|
||||
apiresponse.Error(writer, http.StatusServiceUnavailable, "身份服务暂不可用")
|
||||
return false
|
||||
}
|
||||
if total >= limit {
|
||||
apiresponse.Error(writer, http.StatusForbidden, fmt.Sprintf("账号数已达 License 上限(%d 个),请联系管理员升级", limit))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type identityInput struct {
|
||||
@@ -42,6 +73,10 @@ func NewManagementHTTPHandler(service *Service) *ManagementHTTPHandler {
|
||||
h.mux.HandleFunc("POST /api/v1/admin/identities/admins", h.create(KindAdmin))
|
||||
h.mux.HandleFunc("PUT /api/v1/admin/identities/admins/{identity_id}", h.update(KindAdmin))
|
||||
h.mux.HandleFunc("GET /api/v1/admin/identities/portal-users", h.list(KindPortal))
|
||||
h.mux.HandleFunc("GET /api/v1/admin/roles", h.listRoles)
|
||||
h.mux.HandleFunc("POST /api/v1/admin/roles", h.createRole)
|
||||
h.mux.HandleFunc("PUT /api/v1/admin/roles/{role_id}", h.updateRole)
|
||||
h.mux.HandleFunc("DELETE /api/v1/admin/roles/{role_id}", h.deleteRole)
|
||||
h.mux.HandleFunc("POST /api/v1/admin/identities/portal-users", h.create(KindPortal))
|
||||
h.mux.HandleFunc("PUT /api/v1/admin/identities/portal-users/{identity_id}", h.update(KindPortal))
|
||||
h.mux.HandleFunc("GET /api/v1/admin/departments", h.listDepartments)
|
||||
@@ -79,6 +114,9 @@ func (h *ManagementHTTPHandler) create(kind Kind) http.HandlerFunc {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.checkAccountLimit(writer, request) {
|
||||
return
|
||||
}
|
||||
input, account, password, ok := h.decode(writer, request, kind, true)
|
||||
if !ok {
|
||||
return
|
||||
@@ -150,6 +188,98 @@ func (h *ManagementHTTPHandler) update(kind Kind) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) listRoles(writer http.ResponseWriter, request *http.Request) {
|
||||
actor, ok := h.requirePermission(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = actor
|
||||
roles, err := h.service.repository.ListRoles(request.Context())
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, roles)
|
||||
}
|
||||
|
||||
type roleInput struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) decodeRole(writer http.ResponseWriter, request *http.Request) (roleInput, bool) {
|
||||
var input roleInput
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&input); err != nil {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "请求格式无效")
|
||||
return input, false
|
||||
}
|
||||
input.Code = strings.ToLower(strings.TrimSpace(input.Code))
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.Description = strings.TrimSpace(input.Description)
|
||||
if !roleCodePattern.MatchString(input.Code) || input.Name == "" || len(input.Name) > 64 || len(input.Description) > 512 {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "角色代码或名称格式无效")
|
||||
return input, false
|
||||
}
|
||||
permissions, err := normalizePermissions(input.Permissions)
|
||||
if err != nil {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, err.Error())
|
||||
return input, false
|
||||
}
|
||||
input.Permissions = permissions
|
||||
return input, true
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) createRole(writer http.ResponseWriter, request *http.Request) {
|
||||
actor, ok := h.requirePermission(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
input, ok := h.decodeRole(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
role, err := h.service.repository.SaveRole(request.Context(), "", input.Code, input.Name, input.Description, input.Permissions, actor.ID, true)
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, role)
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) updateRole(writer http.ResponseWriter, request *http.Request) {
|
||||
actor, ok := h.requirePermission(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
input, ok := h.decodeRole(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
role, err := h.service.repository.SaveRole(request.Context(), request.PathValue("role_id"), input.Code, input.Name, input.Description, input.Permissions, actor.ID, false)
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, role)
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) deleteRole(writer http.ResponseWriter, request *http.Request) {
|
||||
actor, ok := h.requirePermission(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = actor
|
||||
if err := h.service.repository.DeleteRole(request.Context(), request.PathValue("role_id")); err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *ManagementHTTPHandler) decode(writer http.ResponseWriter, request *http.Request, kind Kind, creating bool) (identityInput, Account, string, bool) {
|
||||
var input identityInput
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
||||
@@ -176,9 +306,19 @@ func (h *ManagementHTTPHandler) decode(writer http.ResponseWriter, request *http
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "账号或显示名称格式无效")
|
||||
return input, Account{}, "", false
|
||||
}
|
||||
if kind == KindAdmin && input.Role != "" && input.Role != "superadmin" && input.Role != "operator" && input.Role != "auditor" {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "管理员角色无效")
|
||||
return input, Account{}, "", false
|
||||
if kind == KindAdmin && input.Role != "" {
|
||||
switch input.Role {
|
||||
case "superadmin", "operator", "auditor":
|
||||
default:
|
||||
// 自定义角色:必须存在于角色表,并把其权限展开到账号
|
||||
// permissions(角色权限变更后由管理员重新分配或手动同步)。
|
||||
role, roleErr := h.service.repository.FindRole(request.Context(), input.Role)
|
||||
if roleErr != nil {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "角色不存在")
|
||||
return input, Account{}, "", false
|
||||
}
|
||||
input.Permissions = append(input.Permissions, role.Permissions...)
|
||||
}
|
||||
}
|
||||
if kind == KindPortal && input.Role != "" && input.Role != "member" {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "门户角色无效")
|
||||
@@ -230,6 +370,8 @@ func (h *ManagementHTTPHandler) requirePermission(writer http.ResponseWriter, re
|
||||
return account, true
|
||||
}
|
||||
|
||||
var roleCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{1,63}$`)
|
||||
|
||||
func (h *ManagementHTTPHandler) writeError(writer http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, ErrNotFound):
|
||||
|
||||
Reference in New Issue
Block a user