2026-06-25 16:14:07 +08:00

305 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, ",")
}