package platform import ( "encoding/json" "errors" "reflect" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/bsm-sdk/core/middleware" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ResourceHandlers supplies the common identity-based CRUD boundary used by // platform resources whose writable fields are explicitly declared by routes. func ResourceHandlers(model any, createFields, updateFields []string) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) { return func(ctx *gin.Context) { listResource(ctx, model) }, func(ctx *gin.Context) { createResource(ctx, model, createFields) }, func(ctx *gin.Context) { getResource(ctx, model) }, func(ctx *gin.Context) { updateResource(ctx, model, updateFields) } } func listResource(ctx *gin.Context, model any) { page, size := pageSize(ctx) list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem())) var total int64 query := impl.DBService.Model(model) if err := query.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) return } if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(list.Interface()).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"total": total, "list": list.Elem().Interface()}) } func getResource(ctx *gin.Context, model any) { data := reflect.New(reflect.TypeOf(model).Elem()) if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(data.Interface()).Error; err != nil { respondRecordError(ctx, err) return } infra.Response.Success(ctx, data.Interface()) } func createResource(ctx *gin.Context, model any, allowedFields []string) { var input map[string]any if err := ctx.ShouldBindJSON(&input); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } values := filterFields(input, allowedFields) encoded, err := json.Marshal(values) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } data := reflect.New(reflect.TypeOf(model).Elem()) if err := json.Unmarshal(encoded, data.Interface()); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } data.Elem().FieldByName("Entity").Set(reflect.ValueOf(newEntity("draft"))) if err := impl.DBService.Create(data.Interface()).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, data.Interface()) } func updateResource(ctx *gin.Context, model any, allowedFields []string) { var input map[string]any if err := ctx.ShouldBindJSON(&input); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } updateAllowedByIdentity(ctx, model, input, allowedFields) } // DisposeSafetyEvent atomically updates an event and appends its operator-owned // action record. Disposal records deliberately have no update or delete route. func DisposeSafetyEvent(ctx *gin.Context) { claims, err := middleware.ParseAuth(ctx) if err != nil { infra.Response.Error(ctx, err) return } var request struct { Action string `json:"action" binding:"required,max=64"` Reason string `json:"reason" binding:"max=2000"` Status string `json:"status" binding:"max=32"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if request.Status == "" { request.Status = "disposed" } var disposal models.SafEventDisposal err = impl.DBService.Transaction(func(transaction *gorm.DB) error { var event models.SafEvent if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&event).Error; err != nil { return err } if result := transaction.Model(&models.SafEvent{}).Where("identity = ?", event.Identity).Update("status", request.Status); result.Error != nil { return result.Error } else if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } disposal = models.SafEventDisposal{ Entity: newEntity("enabled"), SafEventIdentity: event.Identity, Action: request.Action, Reason: request.Reason, OperatorIdentity: claims.Identity, } return transaction.Create(&disposal).Error }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, disposal) } // GetEcOrder returns the order together with its immutable item snapshots. func GetEcOrder(ctx *gin.Context) { var order models.EcOrder if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil { respondRecordError(ctx, err) return } var items []models.EcOrderItem if err := impl.DBService.Where("ec_order_id = ?", order.ID).Order("id asc").Find(&items).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"order": order, "items": items}) } // GetDeliveryTrack returns time-ordered points. Precise coordinates are only // exposed to tokens explicitly granted the location_scope=precise claim. func GetDeliveryTrack(ctx *gin.Context) { var track models.DeliveryTrack if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&track).Error; err != nil { respondRecordError(ctx, err) return } var points []models.DeliveryTrackPoint if err := impl.DBService.Where("delivery_track_id = ?", track.ID).Order("occurred_at asc").Find(&points).Error; err != nil { infra.Response.Error(ctx, err) return } precise := false if claims, err := middleware.ParseAuth(ctx); err == nil { precise = claims.Extend["location_scope"] == "precise" } if !precise { for index := range points { points[index].Longitude = "" points[index].Latitude = "" } } infra.Response.Success(ctx, gin.H{"track": track, "points": points}) }