refactor product domain models
This commit is contained in:
361
backend/api/internal/logic/platform/product.go
Normal file
361
backend/api/internal/logic/platform/product.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var productOwnerActions = map[string]bool{
|
||||
"created": true, "warehouse": true, "assigned": true, "returned": true, "manual": true,
|
||||
}
|
||||
|
||||
var productLifecycleStatuses = map[string]bool{
|
||||
"pending": true, "in_stock": true, "in_transit": true, "in_use": true, "repairing": true, "scrapped": true,
|
||||
}
|
||||
|
||||
func ProductInfoHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"code", "name", "params", "produced_at", "is_enabled", "status", "action", "reason", "remark"}
|
||||
return func(ctx *gin.Context) { listResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { createProductInfo(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { updateProductInfo(ctx, fields, relations) }
|
||||
}
|
||||
|
||||
func createProductInfo(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductInfo{}, fields, relations)
|
||||
if err != nil || !validParamsText(values["params"]) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
delete(values, "action")
|
||||
delete(values, "reason")
|
||||
delete(values, "remark")
|
||||
data := models.ProductInfo{Entity: newEntity("pending"), Params: "{}", IsEnabled: false}
|
||||
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || data.ProducedAt.IsZero() {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data.Status = "pending"
|
||||
if data.IsEnabled {
|
||||
now := time.Now()
|
||||
data.EnabledAt = &now
|
||||
data.Status = initialProductStatus(data)
|
||||
}
|
||||
operatorIdentity, operatorName := productOperator(ctx)
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&data).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(newProductOwner(data, "created", "", "", operatorIdentity, operatorName, time.Now())).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func updateProductInfo(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, fields, relations, false)
|
||||
if err != nil || len(values) == 0 || !validParamsText(values["params"]) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
action, _ := input["action"].(string)
|
||||
reason, _ := input["reason"].(string)
|
||||
remark, _ := input["remark"].(string)
|
||||
delete(values, "action")
|
||||
delete(values, "reason")
|
||||
delete(values, "remark")
|
||||
operatorIdentity, operatorName := productOperator(ctx)
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var current models.ProductInfo
|
||||
if err := tx.Where("identity = ?", ctx.Param("identity")).First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if _, changingCode := values["code"]; changingCode {
|
||||
delete(values, "code")
|
||||
}
|
||||
if status, ok := values["status"].(string); ok && !productLifecycleStatuses[status] {
|
||||
return errors.New("invalid product status")
|
||||
}
|
||||
if enabled, ok := values["is_enabled"].(bool); ok && enabled && !current.IsEnabled {
|
||||
if current.Status == "scrapped" {
|
||||
return errors.New("scrapped product cannot be enabled")
|
||||
}
|
||||
if current.EnabledAt == nil {
|
||||
now := time.Now()
|
||||
values["enabled_at"] = &now
|
||||
if _, supplied := values["status"]; !supplied {
|
||||
preview := current
|
||||
_ = decodeValues(values, &preview)
|
||||
values["status"] = initialProductStatus(preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
ownershipChanged := ownershipValuesChanged(current, values)
|
||||
if ownershipChanged && !productOwnerActions[action] {
|
||||
return errors.New("invalid ownership action")
|
||||
}
|
||||
if err := tx.Model(¤t).Updates(values).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !ownershipChanged {
|
||||
return nil
|
||||
}
|
||||
if err := tx.Where("id = ?", current.ID).First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(newProductOwner(current, action, reason, remark, operatorIdentity, operatorName, time.Now())).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func ProductOwnerHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"action", "occurred_at", "reason", "remark"}
|
||||
return listProductOwners,
|
||||
func(ctx *gin.Context) { createProductOwner(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductOwner{}) }
|
||||
}
|
||||
|
||||
func listProductOwners(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
var list []models.ProductOwner
|
||||
var total int64
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(&models.ProductOwner{}), &models.ProductOwner{})
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("occurred_at desc, id desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
|
||||
}
|
||||
|
||||
func UpdateProductInfoStatus(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Status string `json:"status" binding:"required,max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !productLifecycleStatuses[request.Status] {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.ProductInfo{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
func ProductRepairHandlers(relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"repair_no", "repair_type", "started_at", "completed_at", "result", "target_status", "content", "operator", "remark"}
|
||||
return func(ctx *gin.Context) { listResource(ctx, &models.ProductRepair{}) },
|
||||
func(ctx *gin.Context) { createProductRepair(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, &models.ProductRepair{}) },
|
||||
func(ctx *gin.Context) { updateProductRepair(ctx, fields, relations) }
|
||||
}
|
||||
|
||||
func createProductRepair(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductRepair{}, fields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data := models.ProductRepair{Entity: newEntity("active"), Result: "pending"}
|
||||
if err := decodeValues(values, &data); err != nil || data.Result != "pending" || !validRepair(data) || data.ProductInfoID == 0 || data.RepairNo == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&data).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&models.ProductInfo{}).Where("id = ?", data.ProductInfoID).Update("status", "repairing").Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func updateProductRepair(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, fields, relations, false)
|
||||
if err != nil || len(values) == 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var current models.ProductRepair
|
||||
if err := tx.Where("identity = ?", ctx.Param("identity")).First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Result != "pending" {
|
||||
if len(values) != 1 {
|
||||
return errors.New("completed repair is locked")
|
||||
}
|
||||
if _, ok := values["remark"]; !ok {
|
||||
return errors.New("only remark can be updated")
|
||||
}
|
||||
}
|
||||
preview := current
|
||||
if err := decodeValues(values, &preview); err != nil || !validRepair(preview) {
|
||||
return errors.New("invalid repair")
|
||||
}
|
||||
if err := tx.Model(¤t).Updates(values).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Result == "pending" && preview.Result != "pending" {
|
||||
return tx.Model(&models.ProductInfo{}).Where("id = ?", preview.ProductInfoID).Update("status", preview.TargetStatus).Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func validRepair(repair models.ProductRepair) bool {
|
||||
if repair.StartedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
switch repair.Result {
|
||||
case "pending":
|
||||
return repair.CompletedAt == nil
|
||||
case "passed", "failed":
|
||||
return repair.CompletedAt != nil && !repair.CompletedAt.Before(repair.StartedAt) &&
|
||||
productLifecycleStatuses[repair.TargetStatus] && repair.TargetStatus != "repairing"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func createProductOwner(ctx *gin.Context, fields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, &models.ProductOwner{}, fields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
action, _ := values["action"].(string)
|
||||
if action != "manual" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data := models.ProductOwner{Entity: newEntity("recorded")}
|
||||
if err := decodeValues(values, &data); err != nil || data.ProductInfoID == 0 || data.OccurredAt.IsZero() {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data.OperatorIdentity, data.OperatorName = productOperator(ctx)
|
||||
if err := impl.DBService.Create(&data).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondCreatedResource(ctx, &data)
|
||||
}
|
||||
|
||||
func validParamsText(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if !ok || strings.TrimSpace(text) == "" {
|
||||
return false
|
||||
}
|
||||
var object map[string]any
|
||||
return json.Unmarshal([]byte(text), &object) == nil && object != nil
|
||||
}
|
||||
|
||||
func decodeValues(values map[string]any, target any) error {
|
||||
encoded, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(encoded, target)
|
||||
}
|
||||
|
||||
func initialProductStatus(product models.ProductInfo) string {
|
||||
if product.WarehouseID != 0 {
|
||||
return "in_stock"
|
||||
}
|
||||
if product.UserAccountID != 0 {
|
||||
return "in_use"
|
||||
}
|
||||
return "pending"
|
||||
}
|
||||
|
||||
func ownershipValuesChanged(current models.ProductInfo, values map[string]any) bool {
|
||||
for key, old := range map[string]uint64{
|
||||
"warehouse_id": current.WarehouseID, "gas_basic_id": current.GasBasicID,
|
||||
"delivery_basic_id": current.DeliveryBasicID, "user_account_id": current.UserAccountID,
|
||||
} {
|
||||
if raw, ok := values[key]; ok {
|
||||
if next, ok := numericID(raw); ok && next != old {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func numericID(value any) (uint64, bool) {
|
||||
switch raw := value.(type) {
|
||||
case uint64:
|
||||
return raw, true
|
||||
case float64:
|
||||
return uint64(raw), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func newProductOwner(product models.ProductInfo, action, reason, remark, operatorIdentity, operatorName string, occurredAt time.Time) *models.ProductOwner {
|
||||
return &models.ProductOwner{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
|
||||
ProductInfoID: product.ID, WarehouseID: product.WarehouseID, GasBasicID: product.GasBasicID,
|
||||
DeliveryBasicID: product.DeliveryBasicID, UserAccountID: product.UserAccountID,
|
||||
Action: action, OccurredAt: occurredAt, Reason: reason, Remark: remark,
|
||||
OperatorIdentity: operatorIdentity, OperatorName: operatorName,
|
||||
}
|
||||
}
|
||||
|
||||
func productOperator(ctx *gin.Context) (string, string) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
if err := impl.DBService.Select("display_name").Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
return claims.Identity, ""
|
||||
}
|
||||
return claims.Identity, account.DisplayName
|
||||
}
|
||||
60
backend/api/internal/logic/platform/product_test.go
Normal file
60
backend/api/internal/logic/platform/product_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
)
|
||||
|
||||
func TestValidParamsTextRequiresJSONObjectText(t *testing.T) {
|
||||
for _, value := range []any{`{"capacity":15}`, `{}`} {
|
||||
if !validParamsText(value) {
|
||||
t.Fatalf("valid JSON object text was rejected: %v", value)
|
||||
}
|
||||
}
|
||||
for _, value := range []any{"", "[]", `"text"`, "{", 1} {
|
||||
if validParamsText(value) {
|
||||
t.Fatalf("invalid params text was accepted: %v", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialProductStatusPrefersActualWarehouse(t *testing.T) {
|
||||
if got := initialProductStatus(models.ProductInfo{WarehouseID: 1, UserAccountID: 2}); got != "in_stock" {
|
||||
t.Fatalf("warehouse product status = %s", got)
|
||||
}
|
||||
if got := initialProductStatus(models.ProductInfo{UserAccountID: 2}); got != "in_use" {
|
||||
t.Fatalf("user product status = %s", got)
|
||||
}
|
||||
if got := initialProductStatus(models.ProductInfo{}); got != "pending" {
|
||||
t.Fatalf("unlocated product status = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidRepairEnforcesCompletionRules(t *testing.T) {
|
||||
started := time.Now()
|
||||
completed := started.Add(time.Hour)
|
||||
if !validRepair(models.ProductRepair{StartedAt: started, Result: "pending"}) {
|
||||
t.Fatal("pending repair was rejected")
|
||||
}
|
||||
if !validRepair(models.ProductRepair{StartedAt: started, CompletedAt: &completed, Result: "passed", TargetStatus: "in_stock"}) {
|
||||
t.Fatal("completed repair was rejected")
|
||||
}
|
||||
if validRepair(models.ProductRepair{StartedAt: started, Result: "passed", TargetStatus: "in_stock"}) {
|
||||
t.Fatal("completed result without completion time was accepted")
|
||||
}
|
||||
if validRepair(models.ProductRepair{StartedAt: completed, CompletedAt: &started, Result: "failed", TargetStatus: "in_stock"}) {
|
||||
t.Fatal("completion before start was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnershipValuesChangedOnlyForActualDifference(t *testing.T) {
|
||||
current := models.ProductInfo{WarehouseID: 1, GasBasicID: 2}
|
||||
if ownershipValuesChanged(current, map[string]any{"warehouse_id": uint64(1)}) {
|
||||
t.Fatal("unchanged warehouse produced a history record")
|
||||
}
|
||||
if !ownershipValuesChanged(current, map[string]any{"gas_basic_id": uint64(3)}) {
|
||||
t.Fatal("changed ownership was not detected")
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
Writable ResourceMode = "writable"
|
||||
ReadOnly ResourceMode = "readonly"
|
||||
AppendOnly ResourceMode = "append_only"
|
||||
Editable ResourceMode = "editable"
|
||||
)
|
||||
|
||||
// ResourceContract is the expected cross-layer representation of one resource.
|
||||
@@ -41,6 +42,8 @@ func (d ResourceDefinition) Allows(method string) bool {
|
||||
return method == http.MethodGet
|
||||
case AppendOnly:
|
||||
return method == http.MethodGet || method == http.MethodPost
|
||||
case Editable:
|
||||
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch
|
||||
case Writable:
|
||||
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch || method == http.MethodDelete
|
||||
default:
|
||||
@@ -74,7 +77,7 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("delivery", "delivery_basic", Writable, "list"), resourceContract("delivery", "delivery_account", Writable, "list"),
|
||||
resourceContract("staff", "staff_account", Writable, "list"), resourceContract("staff", "staff_credential", Writable, "list"),
|
||||
resourceContract("user", "user_account", Writable, "list"), resourceContract("user", "user_address", Writable, "list"), resourceContract("user", "user_service_relation", Writable, "list"),
|
||||
resourceContract("device", "dev_smart_cylinder_valve", Writable, "list"), resourceContract("device", "dev_device_binding", Writable, "list"), resourceContract("device", "dev_telemetry", ReadOnly, "list"),
|
||||
resourceContract("product", "product_type", Editable, "list"), resourceContract("product", "product_warehouse", Editable, "list"), resourceContract("product", "product_info", Editable, "list"), resourceContract("product", "product_repair", Editable, "list"), resourceContract("product", "product_owner", AppendOnly, "list"),
|
||||
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", Writable, "list"), resourceContract("ec", "ec_order", Writable, "list"), resourceContract("ec", "ec_order_item", Writable, "list"), resourceContract("ec", "ec_review", Writable, "list"),
|
||||
resourceContract("delivery", "delivery_task", Writable, "list"), resourceContract("delivery", "delivery_track", Writable, "list"), resourceContract("delivery", "delivery_track_point", ReadOnly, "list"),
|
||||
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
|
||||
@@ -89,6 +92,9 @@ func resourceContract(domain, name string, mode ResourceMode, pageKind string) R
|
||||
}
|
||||
|
||||
func resourcePath(domain, name string) string {
|
||||
if domain == "product" {
|
||||
return "/" + name
|
||||
}
|
||||
switch name {
|
||||
case "staff_account":
|
||||
return "/staff/account"
|
||||
|
||||
@@ -28,6 +28,8 @@ func TestExpectedResources(t *testing.T) {
|
||||
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "wallet", "wallet_ledger", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_info", Editable, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_owner", AppendOnly, "list")
|
||||
}
|
||||
|
||||
func TestContentUsesCMSResourceAndRemovedDomainsAreAbsent(t *testing.T) {
|
||||
@@ -91,6 +93,11 @@ func TestResourceDefinitionAllowsOnlyMethodsForEachMode(t *testing.T) {
|
||||
{"append-only GET", AppendOnly, http.MethodGet, true},
|
||||
{"append-only POST", AppendOnly, http.MethodPost, true},
|
||||
{"append-only PUT", AppendOnly, http.MethodPut, false},
|
||||
{"editable GET", Editable, http.MethodGet, true},
|
||||
{"editable POST", Editable, http.MethodPost, true},
|
||||
{"editable PUT", Editable, http.MethodPut, true},
|
||||
{"editable PATCH", Editable, http.MethodPatch, true},
|
||||
{"editable DELETE", Editable, http.MethodDelete, false},
|
||||
{"writable GET", Writable, http.MethodGet, true},
|
||||
{"writable POST", Writable, http.MethodPost, true},
|
||||
{"writable PUT", Writable, http.MethodPut, true},
|
||||
|
||||
@@ -299,7 +299,14 @@ func resolveResourceRelations(input map[string]any, allowedFields []string, rela
|
||||
continue
|
||||
}
|
||||
identity, ok := raw.(string)
|
||||
if !ok || strings.TrimSpace(identity) == "" {
|
||||
if !ok {
|
||||
return nil, errors.New("invalid relation identity")
|
||||
}
|
||||
if strings.TrimSpace(identity) == "" && !relation.Required {
|
||||
values[relation.Column] = uint64(0)
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(identity) == "" {
|
||||
return nil, errors.New("invalid relation identity")
|
||||
}
|
||||
id, err := resolveIdentityID(relation.Model, identity, true)
|
||||
@@ -366,21 +373,23 @@ func publicResourceResponse(value any) (any, error) {
|
||||
}
|
||||
|
||||
var relationIdentityModels = map[string]any{
|
||||
"gas_basic_id": &models.GasBasic{},
|
||||
"gas_station_id": &models.GasBasic{},
|
||||
"delivery_basic_id": &models.DeliveryBasic{},
|
||||
"delivery_point_id": &models.DeliveryBasic{},
|
||||
"user_account_id": &models.UserAccount{},
|
||||
"staff_account_id": &models.StaffAccount{},
|
||||
"smart_cylinder_valve_id": &models.DevSmartCylinderValve{},
|
||||
"ec_category_id": &models.EcCategory{},
|
||||
"ec_product_id": &models.EcProduct{},
|
||||
"ec_order_id": &models.EcOrder{},
|
||||
"delivery_task_id": &models.DeliveryTask{},
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"wallet_id": &models.Wallet{},
|
||||
"gas_basic_id": &models.GasBasic{},
|
||||
"gas_station_id": &models.GasBasic{},
|
||||
"delivery_basic_id": &models.DeliveryBasic{},
|
||||
"delivery_point_id": &models.DeliveryBasic{},
|
||||
"user_account_id": &models.UserAccount{},
|
||||
"staff_account_id": &models.StaffAccount{},
|
||||
"product_type_id": &models.ProductType{},
|
||||
"product_info_id": &models.ProductInfo{},
|
||||
"warehouse_id": &models.ProductWarehouse{},
|
||||
"ec_category_id": &models.EcCategory{},
|
||||
"ec_product_id": &models.EcProduct{},
|
||||
"ec_order_id": &models.EcOrder{},
|
||||
"delivery_task_id": &models.DeliveryTask{},
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"wallet_id": &models.Wallet{},
|
||||
}
|
||||
|
||||
var relationIdentityKeys = map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user