305 lines
11 KiB
Go
305 lines
11 KiB
Go
package resource
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/middleware"
|
||
"hyapp-admin-server/internal/model"
|
||
"hyapp-admin-server/internal/response"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
const resourceIdentityAutoGrantConfigGroup = "resource-identity-auto-grant-config"
|
||
|
||
var resourceIdentityAutoGrantTypes = []string{"host", "bd", "bd_leader", "agency", "manager"}
|
||
|
||
type resourceIdentityAutoGrantConfigDTO struct {
|
||
Items []resourceIdentityAutoGrantItemDTO `json:"items"`
|
||
UpdatedByAdminID int64 `json:"updatedByAdminId"`
|
||
CreatedAtMS int64 `json:"createdAtMs"`
|
||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||
}
|
||
|
||
type resourceIdentityAutoGrantItemDTO struct {
|
||
IdentityType string `json:"identityType"`
|
||
ResourceGroupID int64 `json:"resourceGroupId"`
|
||
ResourceGroup *resourceGroupDTO `json:"resourceGroup,omitempty"`
|
||
}
|
||
|
||
type resourceIdentityAutoGrantConfigValue struct {
|
||
Items []resourceIdentityAutoGrantStoredItem `json:"items"`
|
||
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||
}
|
||
|
||
type resourceIdentityAutoGrantStoredItem struct {
|
||
IdentityType string `json:"identity_type"`
|
||
ResourceGroupID int64 `json:"resource_group_id"`
|
||
}
|
||
|
||
type resourceIdentityAutoGrantConfigRequest struct {
|
||
Items []resourceIdentityAutoGrantItemRequest `json:"items"`
|
||
}
|
||
|
||
type resourceIdentityAutoGrantItemRequest struct {
|
||
IdentityType string `json:"identity_type"`
|
||
IdentityTypeCamel string `json:"identityType"`
|
||
ResourceGroupID int64 `json:"resource_group_id"`
|
||
ResourceGroupIDJS int64 `json:"resourceGroupId"`
|
||
}
|
||
|
||
func (h *Handler) GetResourceIdentityAutoGrantConfig(c *gin.Context) {
|
||
if h.store == nil {
|
||
response.ServerError(c, "后台配置存储未初始化")
|
||
return
|
||
}
|
||
|
||
// 身份资源组属于 admin 后台低频配置,不进入 wallet 协议;读取时从 admin_app_configs 取当前 App 的快照。
|
||
config, err := h.loadIdentityAutoGrantConfig(c)
|
||
if err != nil {
|
||
response.ServerError(c, "加载身份资源组配置失败")
|
||
return
|
||
}
|
||
response.OK(c, config)
|
||
}
|
||
|
||
func (h *Handler) UpdateResourceIdentityAutoGrantConfig(c *gin.Context) {
|
||
if h.store == nil {
|
||
response.ServerError(c, "后台配置存储未初始化")
|
||
return
|
||
}
|
||
var req resourceIdentityAutoGrantConfigRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "身份资源组配置参数不正确")
|
||
return
|
||
}
|
||
|
||
// 前端保存的是整份配置;后端也要求 5 个身份一次性提交,避免半包请求把未提交身份误清空。
|
||
items, err := normalizeIdentityAutoGrantItems(req.Items)
|
||
if err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
// resource_group_id=0 表示不自动发放;大于 0 必须先确认 wallet 中存在,防止保存悬空资源组。
|
||
if err := h.validateIdentityAutoGrantResourceGroups(c, items); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
payload, err := json.Marshal(resourceIdentityAutoGrantConfigValue{
|
||
Items: items,
|
||
UpdatedByAdminID: actorID(c),
|
||
})
|
||
if err != nil {
|
||
response.ServerError(c, "保存身份资源组配置失败")
|
||
return
|
||
}
|
||
appCode := appctx.FromContext(c.Request.Context())
|
||
// 配置按 App 隔离;Key 使用中间件解析出的 app_code,禁止请求体自行覆盖作用域。
|
||
item := model.AppConfig{
|
||
Group: resourceIdentityAutoGrantConfigGroup,
|
||
Key: appCode,
|
||
Value: string(payload),
|
||
Description: "用户身份自动发放资源组配置",
|
||
}
|
||
if err := h.store.UpsertAppConfigs([]model.AppConfig{item}); err != nil {
|
||
response.ServerError(c, "保存身份资源组配置失败")
|
||
return
|
||
}
|
||
|
||
// 保存后重新读取并补资源组详情,保证返回值和 GET 接口结构一致,前端无需自己拼保存后的展示数据。
|
||
config, err := h.loadIdentityAutoGrantConfig(c)
|
||
if err != nil {
|
||
response.ServerError(c, "加载身份资源组配置失败")
|
||
return
|
||
}
|
||
h.auditLog(c, "update-identity-auto-grant-config", "resource_identity_auto_grant_configs", appCode, "success", identityAutoGrantAuditDetail(items))
|
||
response.OK(c, config)
|
||
}
|
||
|
||
func (h *Handler) loadIdentityAutoGrantConfig(c *gin.Context) (resourceIdentityAutoGrantConfigDTO, error) {
|
||
appCode := appctx.FromContext(c.Request.Context())
|
||
item, err := h.store.GetAppConfig(resourceIdentityAutoGrantConfigGroup, appCode)
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
// 首次进入页面没有配置行时返回完整身份集合,前端可以直接展示空配置而不是走错误态。
|
||
return resourceIdentityAutoGrantConfigDTO{Items: defaultIdentityAutoGrantItems()}, nil
|
||
}
|
||
if err != nil {
|
||
return resourceIdentityAutoGrantConfigDTO{}, err
|
||
}
|
||
config, err := identityAutoGrantConfigFromValue(item.Value)
|
||
if err != nil {
|
||
return resourceIdentityAutoGrantConfigDTO{}, err
|
||
}
|
||
config.CreatedAtMS = item.CreatedAtMS
|
||
config.UpdatedAtMS = item.UpdatedAtMS
|
||
// 资源组详情只用于展示已选项名称;补全失败不应该让整个配置页打不开。
|
||
h.fillIdentityAutoGrantResourceGroups(c, config.Items)
|
||
return config, nil
|
||
}
|
||
|
||
func identityAutoGrantConfigFromValue(raw string) (resourceIdentityAutoGrantConfigDTO, error) {
|
||
var value resourceIdentityAutoGrantConfigValue
|
||
if strings.TrimSpace(raw) != "" {
|
||
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||
return resourceIdentityAutoGrantConfigDTO{}, err
|
||
}
|
||
}
|
||
itemsByType := make(map[string]resourceIdentityAutoGrantStoredItem, len(value.Items))
|
||
for _, item := range value.Items {
|
||
identityType := strings.TrimSpace(item.IdentityType)
|
||
if isIdentityAutoGrantType(identityType) && item.ResourceGroupID >= 0 {
|
||
// 读取历史配置时按当前支持的身份集合过滤,旧脏数据不继续扩散到前端表单。
|
||
itemsByType[identityType] = resourceIdentityAutoGrantStoredItem{IdentityType: identityType, ResourceGroupID: item.ResourceGroupID}
|
||
}
|
||
}
|
||
items := make([]resourceIdentityAutoGrantItemDTO, 0, len(resourceIdentityAutoGrantTypes))
|
||
for _, identityType := range resourceIdentityAutoGrantTypes {
|
||
// 输出顺序固定,避免前端每次打开弹窗时身份行抖动。
|
||
item := itemsByType[identityType]
|
||
items = append(items, resourceIdentityAutoGrantItemDTO{
|
||
IdentityType: identityType,
|
||
ResourceGroupID: item.ResourceGroupID,
|
||
})
|
||
}
|
||
return resourceIdentityAutoGrantConfigDTO{Items: items, UpdatedByAdminID: value.UpdatedByAdminID}, nil
|
||
}
|
||
|
||
func defaultIdentityAutoGrantItems() []resourceIdentityAutoGrantItemDTO {
|
||
items := make([]resourceIdentityAutoGrantItemDTO, 0, len(resourceIdentityAutoGrantTypes))
|
||
for _, identityType := range resourceIdentityAutoGrantTypes {
|
||
items = append(items, resourceIdentityAutoGrantItemDTO{IdentityType: identityType})
|
||
}
|
||
return items
|
||
}
|
||
|
||
func normalizeIdentityAutoGrantItems(items []resourceIdentityAutoGrantItemRequest) ([]resourceIdentityAutoGrantStoredItem, error) {
|
||
if len(items) != len(resourceIdentityAutoGrantTypes) {
|
||
return nil, fmt.Errorf("必须配置所有身份")
|
||
}
|
||
|
||
itemByType := make(map[string]resourceIdentityAutoGrantStoredItem, len(items))
|
||
for _, item := range items {
|
||
// 当前前端提交 snake_case;这里兼容 camelCase,避免旧构建或调试脚本因为字段名差异无法保存。
|
||
identityType := strings.TrimSpace(firstNonEmpty(item.IdentityType, item.IdentityTypeCamel))
|
||
if !isIdentityAutoGrantType(identityType) {
|
||
return nil, fmt.Errorf("身份参数不正确")
|
||
}
|
||
if _, exists := itemByType[identityType]; exists {
|
||
return nil, fmt.Errorf("身份不能重复配置")
|
||
}
|
||
// 0 是显式关闭自动发放,所以只拒绝负数;正数存在性在后续 wallet 查询里验证。
|
||
resourceGroupID := item.ResourceGroupID
|
||
if resourceGroupID == 0 {
|
||
resourceGroupID = item.ResourceGroupIDJS
|
||
}
|
||
if resourceGroupID < 0 {
|
||
return nil, fmt.Errorf("资源组必须是有效资源组")
|
||
}
|
||
itemByType[identityType] = resourceIdentityAutoGrantStoredItem{
|
||
IdentityType: identityType,
|
||
ResourceGroupID: resourceGroupID,
|
||
}
|
||
}
|
||
|
||
normalized := make([]resourceIdentityAutoGrantStoredItem, 0, len(resourceIdentityAutoGrantTypes))
|
||
for _, identityType := range resourceIdentityAutoGrantTypes {
|
||
// 按白名单顺序重建数组,存储层不依赖前端传入顺序。
|
||
item, ok := itemByType[identityType]
|
||
if !ok {
|
||
return nil, fmt.Errorf("必须配置所有身份")
|
||
}
|
||
normalized = append(normalized, item)
|
||
}
|
||
return normalized, nil
|
||
}
|
||
|
||
func (h *Handler) validateIdentityAutoGrantResourceGroups(c *gin.Context, items []resourceIdentityAutoGrantStoredItem) error {
|
||
if h.wallet == nil {
|
||
for _, item := range items {
|
||
if item.ResourceGroupID > 0 {
|
||
// 没有 wallet client 时只能接受全关闭配置,避免把未校验的正数资源组写入运行配置。
|
||
return fmt.Errorf("资源组服务未初始化")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
checked := make(map[int64]struct{}, len(items))
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
for _, item := range items {
|
||
if item.ResourceGroupID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := checked[item.ResourceGroupID]; ok {
|
||
continue
|
||
}
|
||
checked[item.ResourceGroupID] = struct{}{}
|
||
// 保存前只校验唯一资源组 ID,一份配置里多个身份共用同组时不重复打 wallet。
|
||
_, err := h.wallet.GetResourceGroup(ctx, &walletv1.GetResourceGroupRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GroupId: item.ResourceGroupID,
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("资源组 %d 不存在或不可用", item.ResourceGroupID)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (h *Handler) fillIdentityAutoGrantResourceGroups(c *gin.Context, items []resourceIdentityAutoGrantItemDTO) {
|
||
if h.wallet == nil {
|
||
return
|
||
}
|
||
groupByID := make(map[int64]*resourceGroupDTO, len(items))
|
||
ctx, cancel := h.walletRequestContext(c)
|
||
defer cancel()
|
||
for index := range items {
|
||
groupID := items[index].ResourceGroupID
|
||
if groupID == 0 {
|
||
continue
|
||
}
|
||
if group, ok := groupByID[groupID]; ok {
|
||
items[index].ResourceGroup = group
|
||
continue
|
||
}
|
||
// 已保存配置可能指向被禁用或被删除的资源组;详情查询失败时保留 ID,让前端至少能展示 #ID 并允许重新选择。
|
||
resp, err := h.wallet.GetResourceGroup(ctx, &walletv1.GetResourceGroupRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appctx.FromContext(c.Request.Context()),
|
||
GroupId: groupID,
|
||
})
|
||
if err != nil {
|
||
continue
|
||
}
|
||
group := resourceGroupFromProto(resp.GetGroup())
|
||
groupByID[groupID] = &group
|
||
items[index].ResourceGroup = &group
|
||
}
|
||
}
|
||
|
||
func isIdentityAutoGrantType(value string) bool {
|
||
for _, candidate := range resourceIdentityAutoGrantTypes {
|
||
if value == candidate {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func identityAutoGrantAuditDetail(items []resourceIdentityAutoGrantStoredItem) string {
|
||
parts := make([]string, 0, len(items))
|
||
for _, item := range items {
|
||
parts = append(parts, fmt.Sprintf("%s=%d", item.IdentityType, item.ResourceGroupID))
|
||
}
|
||
return strings.Join(parts, ",")
|
||
}
|