fix: harden platform response projections

This commit is contained in:
2026-07-27 10:42:16 +08:00
parent 7ebe25fe34
commit a545559fa8
4 changed files with 195 additions and 29 deletions

View File

@@ -6,6 +6,7 @@ import (
"errors"
"io"
"reflect"
"sort"
"strings"
"git.apinb.com/bsm-sdk/core/errcode"
@@ -102,7 +103,12 @@ func listResource(ctx *gin.Context, model any) {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"total": total, "list": publicResourceResponse(list.Elem().Interface())})
response, err := publicResourceResponse(list.Elem().Interface())
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
}
func getResource(ctx *gin.Context, model any) {
@@ -111,7 +117,12 @@ func getResource(ctx *gin.Context, model any) {
respondRecordError(ctx, err)
return
}
infra.Response.Success(ctx, publicResourceResponse(data.Interface()))
response, err := publicResourceResponse(data.Interface())
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, response)
}
func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
@@ -233,14 +244,14 @@ func resourceResponse(value any) any {
// publicResourceResponse additionally resolves persisted relation keys into
// their public identities. It is used by list/detail endpoints so an edit form
// can round-trip the relation without ever receiving a surrogate database ID.
func publicResourceResponse(value any) any {
func publicResourceResponse(value any) (any, error) {
encoded, err := json.Marshal(value)
if err != nil {
return value
return nil, err
}
var decoded any
if err := json.Unmarshal(encoded, &decoded); err != nil {
return value
return nil, err
}
return projectRelationIdentities(decoded)
}
@@ -269,7 +280,54 @@ var relationIdentityKeys = map[string]string{
"delivery_point_id": "delivery_basic_identity",
}
func projectRelationIdentities(value any) any {
type relationIdentityReference struct {
target map[string]any
identityKey string
id uint64
}
type relationIdentityGroup struct {
model any
ids []uint64
seen map[uint64]struct{}
references []relationIdentityReference
}
type relationIdentityRecord struct {
ID uint64
Identity string
}
func projectRelationIdentities(value any) (any, error) {
groups := map[string]*relationIdentityGroup{}
collectRelationIdentityReferences(value, groups)
groupKeys := make([]string, 0, len(groups))
for key := range groups {
groupKeys = append(groupKeys, key)
}
sort.Strings(groupKeys)
for _, key := range groupKeys {
group := groups[key]
var rows []relationIdentityRecord
if err := impl.DBService.Model(group.model).Select("id", "identity").Where("id IN ?", group.ids).Find(&rows).Error; err != nil {
return nil, err
}
identities := make(map[uint64]string, len(rows))
for _, row := range rows {
identities[row.ID] = row.Identity
}
for _, reference := range group.references {
identity, found := identities[reference.id]
if !found {
return nil, errors.New("related identity not found")
}
reference.target[reference.identityKey] = identity
}
}
return value, nil
}
func collectRelationIdentityReferences(value any, groups map[string]*relationIdentityGroup) {
switch data := value.(type) {
case map[string]any:
for key, item := range data {
@@ -288,22 +346,35 @@ func projectRelationIdentities(value any) any {
identityKey = "subject_identity"
}
if model != nil {
data[identityKey] = relationIdentity(item, model)
if id, ok := responseRelationID(item); ok && id != 0 {
key := reflect.TypeOf(model).String()
group := groups[key]
if group == nil {
group = &relationIdentityGroup{model: model, seen: map[uint64]struct{}{}}
groups[key] = group
}
if _, found := group.seen[id]; !found {
group.ids = append(group.ids, id)
group.seen[id] = struct{}{}
}
group.references = append(group.references, relationIdentityReference{target: data, identityKey: identityKey, id: id})
} else {
data[identityKey] = ""
}
}
delete(data, key)
continue
}
data[key] = projectRelationIdentities(item)
collectRelationIdentityReferences(item, groups)
}
case []any:
for index := range data {
data[index] = projectRelationIdentities(data[index])
for _, item := range data {
collectRelationIdentityReferences(item, groups)
}
}
return value
}
func relationIdentity(value any, model any) string {
func responseRelationID(value any) (uint64, bool) {
var id uint64
switch raw := value.(type) {
case float64:
@@ -312,15 +383,10 @@ func relationIdentity(value any, model any) string {
id = raw
case int:
id = uint64(raw)
default:
return 0, false
}
if id == 0 {
return ""
}
var related struct{ Identity string }
if err := impl.DBService.Model(model).Select("identity").Where("id = ?", id).First(&related).Error; err != nil {
return ""
}
return related.Identity
return id, true
}
func settlementSubjectModel(value any) any {
@@ -418,7 +484,12 @@ func GetEcOrder(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, publicResourceResponse(gin.H{"order": order, "items": items}))
response, err := publicResourceResponse(gin.H{"order": order, "items": items})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, response)
}
// GetDeliveryTrack returns time-ordered points. Precise coordinates are only
@@ -444,7 +515,12 @@ func GetDeliveryTrack(ctx *gin.Context) {
points[index].Latitude = ""
}
}
infra.Response.Success(ctx, publicResourceResponse(gin.H{"track": track, "points": points}))
response, err := publicResourceResponse(gin.H{"track": track, "points": points})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, response)
}
type ecCategoryView struct {