0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)

- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆)
- 新增包: license/memory/modelquota/assistant,扫描引擎
- 全部功能后端+前端+端到端验证通过(25 包单测)
This commit is contained in:
2026-08-13 11:37:18 +08:00
parent 9501751792
commit c22669c31d
43 changed files with 3672 additions and 254 deletions
+145 -3
View File
@@ -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):