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{
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DevDeviceBinding 对应 dev_device_binding,保存设备授权绑定。
|
||||
type DevDeviceBinding struct {
|
||||
Entity // 公共实体字段
|
||||
SmartCylinderValveID uint64 `gorm:"column:smart_cylinder_valve_id;not null;index" json:"smart_cylinder_valve_id"` // smart_cylinder_valve_id 业务字段
|
||||
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段
|
||||
EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // effective_at 业务字段
|
||||
ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // expired_at 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DevDeviceBinding{}) }
|
||||
func (table *DevDeviceBinding) TableName() string { return "dev_device_binding" }
|
||||
@@ -1,15 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// DevSmartCylinderValve 对应 dev_smart_cylinder_valve,保存智能瓶阀档案。
|
||||
type DevSmartCylinderValve struct {
|
||||
Entity // 公共实体字段
|
||||
DeviceNo string `gorm:"column:device_no;type:varchar(64);not null;uniqueIndex" json:"device_no"` // device_no 业务字段
|
||||
Model string `gorm:"column:model;type:varchar(64);not null;default:''" json:"model"` // model 业务字段
|
||||
OnlineStatus string `gorm:"column:online_status;type:varchar(32);not null;default:'offline'" json:"online_status"` // online_status 业务字段
|
||||
OwnerIdentity string `gorm:"column:owner_identity;type:varchar(36);not null;default:'';index" json:"owner_identity"` // owner_identity 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DevSmartCylinderValve{}) }
|
||||
func (table *DevSmartCylinderValve) TableName() string { return "dev_smart_cylinder_valve" }
|
||||
@@ -1,18 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DevTelemetry 对应 dev_telemetry,保存设备遥测摘要。
|
||||
type DevTelemetry struct {
|
||||
Entity // 公共实体字段
|
||||
SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;index" json:"smart_cylinder_valve_identity"` // smart_cylinder_valve_identity 业务字段
|
||||
ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null;index" json:"reported_at"` // reported_at 业务字段
|
||||
Payload string `gorm:"column:payload;type:text;not null;default:''" json:"payload"` // payload 业务字段
|
||||
QualityFlag string `gorm:"column:quality_flag;type:varchar(32);not null;default:'normal'" json:"quality_flag"` // quality_flag 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&DevTelemetry{}) }
|
||||
func (table *DevTelemetry) TableName() string { return "dev_telemetry" }
|
||||
26
backend/api/internal/models/product_info.go
Normal file
26
backend/api/internal/models/product_info.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// ProductInfo 对应 product_info,保存一物一码的实体产品档案。
|
||||
type ProductInfo struct {
|
||||
Entity // 公共实体字段
|
||||
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 产品唯一标识
|
||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 产品名称
|
||||
ProductTypeID uint64 `gorm:"column:product_type_id;not null;index" json:"product_type_id"` // 产品类型自增主键
|
||||
Params string `gorm:"column:params;type:text;not null;default:'{}'" json:"params"` // 产品参数 JSON 对象文本
|
||||
WarehouseID uint64 `gorm:"column:warehouse_id;not null;default:0;index" json:"warehouse_id"` // 当前实际库房自增主键
|
||||
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // 当前归属气站自增主键
|
||||
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前归属配送站自增主键
|
||||
UserAccountID uint64 `gorm:"column:user_account_id;not null;default:0;index" json:"user_account_id"` // 当前归属用户自增主键
|
||||
ProducedAt time.Time `gorm:"column:produced_at;type:timestamptz;not null" json:"produced_at"` // 生产时间
|
||||
IsEnabled bool `gorm:"column:is_enabled;not null;default:false" json:"is_enabled"` // 是否启用
|
||||
EnabledAt *time.Time `gorm:"column:enabled_at;type:timestamptz" json:"enabled_at"` // 首次启用时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductInfo{}) }
|
||||
func (table *ProductInfo) TableName() string { return "product_info" }
|
||||
26
backend/api/internal/models/product_owner.go
Normal file
26
backend/api/internal/models/product_owner.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// ProductOwner 对应 product_owner,保存产品库房与责任归属的不可变快照。
|
||||
type ProductOwner struct {
|
||||
Entity // 公共实体字段
|
||||
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index" json:"product_info_id"` // 产品档案自增主键
|
||||
WarehouseID uint64 `gorm:"column:warehouse_id;not null;default:0;index" json:"warehouse_id"` // 当前实际库房自增主键快照
|
||||
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // 当前归属气站自增主键快照
|
||||
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前归属配送站自增主键快照
|
||||
UserAccountID uint64 `gorm:"column:user_account_id;not null;default:0;index" json:"user_account_id"` // 当前归属用户自增主键快照
|
||||
Action string `gorm:"column:action;type:varchar(32);not null" json:"action"` // 归属变更动作
|
||||
OccurredAt time.Time `gorm:"column:occurred_at;type:timestamptz;not null;index" json:"occurred_at"` // 实际发生时间
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 变更原因
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:'';index" json:"operator_identity"` // 操作者业务标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 操作者姓名快照
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductOwner{}) }
|
||||
func (table *ProductOwner) TableName() string { return "product_owner" }
|
||||
25
backend/api/internal/models/product_repair.go
Normal file
25
backend/api/internal/models/product_repair.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// ProductRepair 对应 product_repair,保存产品检修过程与结果。
|
||||
type ProductRepair struct {
|
||||
Entity // 公共实体字段
|
||||
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index" json:"product_info_id"` // 产品档案自增主键
|
||||
RepairNo string `gorm:"column:repair_no;type:varchar(64);not null;uniqueIndex" json:"repair_no"` // 检修单号
|
||||
RepairType string `gorm:"column:repair_type;type:varchar(32);not null" json:"repair_type"` // 检修类型
|
||||
StartedAt time.Time `gorm:"column:started_at;type:timestamptz;not null" json:"started_at"` // 开始时间
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
Result string `gorm:"column:result;type:varchar(32);not null;default:'pending'" json:"result"` // 检修结果
|
||||
TargetStatus string `gorm:"column:target_status;type:varchar(32);not null;default:''" json:"target_status"` // 完成后的产品状态
|
||||
Content string `gorm:"column:content;type:text;not null;default:''" json:"content"` // 检修内容
|
||||
Operator string `gorm:"column:operator;type:varchar(64);not null;default:''" json:"operator"` // 检修人员
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductRepair{}) }
|
||||
func (table *ProductRepair) TableName() string { return "product_repair" }
|
||||
13
backend/api/internal/models/product_type.go
Normal file
13
backend/api/internal/models/product_type.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// ProductType 对应 product_type,保存实体产品的类型与规格名称。
|
||||
type ProductType struct {
|
||||
Entity // 公共实体字段
|
||||
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 产品类型编码
|
||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 产品类型名称
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductType{}) }
|
||||
func (table *ProductType) TableName() string { return "product_type" }
|
||||
17
backend/api/internal/models/product_warehouse.go
Normal file
17
backend/api/internal/models/product_warehouse.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// ProductWarehouse 对应 product_warehouse,保存独立库房档案。
|
||||
type ProductWarehouse struct {
|
||||
Entity // 公共实体字段
|
||||
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 库房编码
|
||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 库房名称
|
||||
Address string `gorm:"column:address;type:varchar(255);not null;default:''" json:"address"` // 库房地址
|
||||
Manager string `gorm:"column:manager;type:varchar(64);not null;default:''" json:"manager"` // 库房负责人
|
||||
Phone string `gorm:"column:phone;type:varchar(32);not null;default:''" json:"phone"` // 联系电话
|
||||
IsEnabled bool `gorm:"column:is_enabled;not null;default:false" json:"is_enabled"` // 是否启用
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductWarehouse{}) }
|
||||
func (table *ProductWarehouse) TableName() string { return "product_warehouse" }
|
||||
@@ -27,7 +27,7 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
|
||||
registerDeliveryRoute(protected)
|
||||
registerStaffRoute(protected)
|
||||
registerUserRoute(protected)
|
||||
registerDeviceRoute(protected)
|
||||
registerProductRoute(protected)
|
||||
registerCommerceRoute(protected)
|
||||
registerFinanceRoute(protected)
|
||||
registerContentRoute(protected)
|
||||
@@ -50,13 +50,41 @@ func registerDeliveryRoute(group *gin.RouterGroup) {
|
||||
registerReadOnlyResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{})
|
||||
}
|
||||
|
||||
func registerDeviceRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/device/dev_smart_cylinder_valve", &models.DevSmartCylinderValve{}, []string{"device_no", "model", "online_status", "owner_identity"})
|
||||
registerRestrictedWritableResource(group, "/device/dev_device_binding", &models.DevDeviceBinding{}, []string{"effective_at", "expired_at"}, requiredRelation("smart_cylinder_valve_identity", "smart_cylinder_valve_id", &models.DevSmartCylinderValve{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
list, _, get, _ := platform.ResourceHandlers(&models.DevTelemetry{}, nil, nil)
|
||||
telemetry := group.Group("/device/dev_telemetry")
|
||||
telemetry.GET("", list)
|
||||
telemetry.GET("/:identity", get)
|
||||
func registerProductRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedNoDeleteResource(group, "/product_type", &models.ProductType{}, []string{"code", "name"})
|
||||
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone", "is_enabled"})
|
||||
|
||||
infoRelations := []platform.ResourceRelation{
|
||||
requiredRelation("product_type_identity", "product_type_id", &models.ProductType{}),
|
||||
optionalRelation("warehouse_identity", "warehouse_id", &models.ProductWarehouse{}),
|
||||
optionalRelation("gas_basic_identity", "gas_basic_id", &models.GasBasic{}),
|
||||
optionalRelation("delivery_basic_identity", "delivery_basic_id", &models.DeliveryBasic{}),
|
||||
optionalRelation("user_account_identity", "user_account_id", &models.UserAccount{}),
|
||||
}
|
||||
infoList, infoCreate, infoGet, infoUpdate := platform.ProductInfoHandlers(infoRelations...)
|
||||
info := group.Group("/product_info")
|
||||
info.GET("", infoList)
|
||||
info.POST("", infoCreate)
|
||||
info.GET("/:identity", infoGet)
|
||||
info.PUT("/:identity", infoUpdate)
|
||||
info.PATCH("/:identity/status", platform.UpdateProductInfoStatus)
|
||||
|
||||
repairList, repairCreate, repairGet, repairUpdate := platform.ProductRepairHandlers(
|
||||
requiredRelation("product_info_identity", "product_info_id", &models.ProductInfo{}),
|
||||
)
|
||||
registerNoDeleteResource(group, "/product_repair", repairList, repairCreate, repairGet, repairUpdate, &models.ProductRepair{})
|
||||
|
||||
ownerList, ownerCreate, ownerGet := platform.ProductOwnerHandlers(
|
||||
requiredRelation("product_info_identity", "product_info_id", &models.ProductInfo{}),
|
||||
optionalRelation("warehouse_identity", "warehouse_id", &models.ProductWarehouse{}),
|
||||
optionalRelation("gas_basic_identity", "gas_basic_id", &models.GasBasic{}),
|
||||
optionalRelation("delivery_basic_identity", "delivery_basic_id", &models.DeliveryBasic{}),
|
||||
optionalRelation("user_account_identity", "user_account_id", &models.UserAccount{}),
|
||||
)
|
||||
owner := group.Group("/product_owner")
|
||||
owner.GET("", ownerList)
|
||||
owner.POST("", ownerCreate)
|
||||
owner.GET("/:identity", ownerGet)
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
@@ -138,6 +166,20 @@ func registerRestrictedWritableResource(group *gin.RouterGroup, path string, mod
|
||||
registerWritableResource(group, path, list, create, get, update, model)
|
||||
}
|
||||
|
||||
func registerNoDeleteResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) {
|
||||
resource := group.Group(path)
|
||||
resource.GET("", list)
|
||||
resource.POST("", create)
|
||||
resource.GET("/:identity", get)
|
||||
resource.PUT("/:identity", update)
|
||||
resource.PATCH("/:identity/status", func(ctx *gin.Context) { platform.UpdateRecordStatus(ctx, model) })
|
||||
}
|
||||
|
||||
func registerRestrictedNoDeleteResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...platform.ResourceRelation) {
|
||||
list, create, get, update := platform.ResourceHandlers(model, fields, fields, relations...)
|
||||
registerNoDeleteResource(group, path, list, create, get, update, model)
|
||||
}
|
||||
|
||||
func registerReadOnlyResource(group *gin.RouterGroup, path string, model any) {
|
||||
list, _, get, _ := platform.ResourceHandlers(model, nil, nil)
|
||||
resource := group.Group(path)
|
||||
|
||||
@@ -29,6 +29,12 @@ func TestEveryContractHasRegisteredRoute(t *testing.T) {
|
||||
case platform.AppendOnly:
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertNoRouteMethods(t, routes, path, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
case platform.Editable:
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, path+"/:identity/status", http.MethodPatch)
|
||||
assertNoRouteMethods(t, routes, path, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodDelete)
|
||||
default:
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
@@ -87,7 +93,7 @@ func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform/platform_role/:identity/menus", http.MethodPut)
|
||||
}
|
||||
|
||||
func TestPlatformDeviceCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) {
|
||||
func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) {
|
||||
engine := gin.New()
|
||||
RegisterPlatform("heqi", engine)
|
||||
|
||||
@@ -100,7 +106,6 @@ func TestPlatformDeviceCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/device/dev_smart_cylinder_valve", "/device/dev_device_binding",
|
||||
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_order_item", "/ec/ec_review",
|
||||
"/delivery/delivery_task", "/delivery/delivery_track",
|
||||
} {
|
||||
@@ -116,14 +121,17 @@ func TestPlatformDeviceCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.
|
||||
assertNoRouteMethods(t, routes, trackPoint+"/:identity", http.MethodPut, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, trackPoint+"/:identity/status", http.MethodPatch)
|
||||
|
||||
telemetry := "/heqi/platform/v1/device/dev_telemetry"
|
||||
assertRouteMethods(t, routes, telemetry, http.MethodGet)
|
||||
assertRouteMethods(t, routes, telemetry+"/:identity", http.MethodGet)
|
||||
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete} {
|
||||
if routes[telemetry][method] || routes[telemetry+"/:identity"][method] {
|
||||
t.Errorf("telemetry unexpectedly permits %s", method)
|
||||
}
|
||||
for _, resource := range []string{"/product_type", "/product_warehouse", "/product_info", "/product_repair"} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, path+"/:identity/status", http.MethodPatch)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodDelete)
|
||||
}
|
||||
owner := "/heqi/platform/v1/product_owner"
|
||||
assertRouteMethods(t, routes, owner, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, owner+"/:identity", http.MethodGet)
|
||||
assertNoRouteMethods(t, routes, owner+"/:identity", http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
|
||||
for _, resource := range []string{
|
||||
"/safety/safe_rule", "/safety/safe_event", "/safety/safe_inspection",
|
||||
|
||||
Reference in New Issue
Block a user