Files
platforms/backend/api/internal/logic/platform/task4_resources.go

176 lines
5.9 KiB
Go
Raw Normal View History

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