feat: add producer account management
This commit is contained in:
@@ -20,7 +20,7 @@ func TestInitPlatformAccessSeedsRootRole(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 AND "platform_role"."deleted_at" IS NULL ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
WithArgs("root", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "role_code", "name", "location_scope", "is_system"}).
|
||||
AddRow(uint64(1), "root-role", 1, "root", "Root", "precise", true))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -408,6 +408,7 @@ var relationIdentityModels = map[string]any{
|
||||
"user_account_id": &models.UserAccount{},
|
||||
"staff_account_id": &models.StaffAccount{},
|
||||
"product_type_id": &models.ProductType{},
|
||||
"producer_account_id": &models.ProducerAccount{},
|
||||
"product_info_id": &models.ProductInfo{},
|
||||
"warehouse_id": &models.ProductWarehouse{},
|
||||
"ec_category_id": &models.EcCategory{},
|
||||
@@ -419,7 +420,7 @@ var relationIdentityModels = map[string]any{
|
||||
"gasorder_track_id": &models.GasorderTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"wallet_basic_id": &models.WalletBasic{},
|
||||
"payment_order_id": &models.PaymentOrder{},
|
||||
"payment_order_id": &models.PaymentOrder{},
|
||||
"wallet_bank_id": &models.WalletBank{},
|
||||
"related_record_id": &models.WalletRecord{},
|
||||
}
|
||||
|
||||
@@ -46,9 +46,10 @@ var PlatformMenus = [][]Menu{
|
||||
},
|
||||
{
|
||||
{Identity: "device", GroupCode: "device", Name: "智能气阀管理", Icon: "icon-common", Path: "/product", SortNo: 60, Status: common.StatusEnable},
|
||||
{Identity: "product_type", ParentIdentity: "device", GroupCode: "device", Name: "类型管理", Path: "/product/product-type", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "product_warehouse", ParentIdentity: "device", GroupCode: "device", Name: "库房管理", Path: "/product/warehouse", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "product_info", ParentIdentity: "device", GroupCode: "device", Name: "智能气阀管理", Path: "/product/product-info", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "producer_account", ParentIdentity: "device", GroupCode: "device", Name: "生产商管理", Path: "/product/producers", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "product_type", ParentIdentity: "device", GroupCode: "device", Name: "类型管理", Path: "/product/product-type", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "product_warehouse", ParentIdentity: "device", GroupCode: "device", Name: "库房管理", Path: "/product/warehouse", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "product_info", ParentIdentity: "device", GroupCode: "device", Name: "智能气阀管理", Path: "/product/product-info", SortNo: 4, Status: common.StatusEnable},
|
||||
},
|
||||
{
|
||||
{Identity: "gasorder", GroupCode: "gasorder", Name: "气体配送订单管理", Icon: "icon-list", Path: "/gasorder", SortNo: 70, Status: common.StatusEnable},
|
||||
|
||||
108
backend/api/internal/logic/platform/product/producer.go
Normal file
108
backend/api/internal/logic/platform/product/producer.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type producerAccountCreateRequest struct {
|
||||
ProducerCode string `json:"producer_code" binding:"required,max=64"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
CreditCode string `json:"credit_code" binding:"max=64"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type producerAccountUpdateRequest struct {
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
CreditCode string `json:"credit_code" binding:"max=64"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
Password string `json:"password"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func ListProducerAccount(ctx *gin.Context) { common.ListResource(ctx, &models.ProducerAccount{}) }
|
||||
func GetProducerAccount(ctx *gin.Context) { common.GetResource(ctx, &models.ProducerAccount{}) }
|
||||
|
||||
func CreateProducerAccount(ctx *gin.Context) {
|
||||
var request producerAccountCreateRequest
|
||||
if ctx.ShouldBindJSON(&request) != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
roleCode := strings.TrimSpace(request.RoleCode)
|
||||
if roleCode == "" {
|
||||
roleCode = "admin"
|
||||
}
|
||||
data := models.ProducerAccount{Entity: common.NewEntity(common.StatusEnable), ProducerCode: strings.TrimSpace(request.ProducerCode), Name: strings.TrimSpace(request.Name), CreditCode: strings.TrimSpace(request.CreditCode), Principal: strings.TrimSpace(request.Principal), Phone: strings.TrimSpace(request.Phone), Address: strings.TrimSpace(request.Address), Username: strings.TrimSpace(request.Username), DisplayName: strings.TrimSpace(request.DisplayName), PasswordHash: hash, RoleCode: roleCode, Remark: request.Remark}
|
||||
if err := impl.DBService.Create(&data).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, data)
|
||||
}
|
||||
|
||||
func UpdateProducerAccount(ctx *gin.Context) {
|
||||
var request producerAccountUpdateRequest
|
||||
if ctx.ShouldBindJSON(&request) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values := gin.H{"name": strings.TrimSpace(request.Name), "credit_code": strings.TrimSpace(request.CreditCode), "principal": strings.TrimSpace(request.Principal), "phone": strings.TrimSpace(request.Phone), "address": strings.TrimSpace(request.Address), "display_name": strings.TrimSpace(request.DisplayName), "role_code": strings.TrimSpace(request.RoleCode), "remark": request.Remark}
|
||||
if request.Password != "" {
|
||||
if !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
values["password_hash"] = hash
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.ProducerAccount{}, values, []string{"name", "credit_code", "principal", "phone", "address", "display_name", "role_code", "remark", "password_hash"})
|
||||
}
|
||||
|
||||
func DeleteProducerAccount(ctx *gin.Context) {
|
||||
var producer models.ProducerAccount
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&producer).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var references int64
|
||||
if err := impl.DBService.Unscoped().Model(&models.ProductInfo{}).Where("producer_account_id = ?", producer.ID).Count(&references).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if references > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Delete(&producer).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"deleted": true})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func createProductInfo(ctx *gin.Context, fields []string, relations []common.Res
|
||||
delete(values, "reason")
|
||||
delete(values, "remark")
|
||||
data := models.ProductInfo{Entity: common.NewEntity(common.StatusDisable), ProductStatus: common.StatusPending, Params: "{}"}
|
||||
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || data.ProducedAt.IsZero() || !validProductOwnership(data) {
|
||||
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || !validProductProducer(data) || data.ProducedAt.IsZero() || !validProductOwnership(data) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func updateProductInfo(ctx *gin.Context, fields []string, relations []common.Res
|
||||
delete(values, "code")
|
||||
}
|
||||
preview := current
|
||||
if err := decodeValues(values, &preview); err != nil || !validProductOwnership(preview) {
|
||||
if err := decodeValues(values, &preview); err != nil || !validProductProducer(preview) || !validProductOwnership(preview) {
|
||||
return errors.New("product can have at most one current owner")
|
||||
}
|
||||
ownershipChanged := ownershipValuesChanged(current, values)
|
||||
@@ -379,6 +379,10 @@ func validProductOwnership(product models.ProductInfo) bool {
|
||||
return count <= 1
|
||||
}
|
||||
|
||||
func validProductProducer(product models.ProductInfo) bool {
|
||||
return product.ProducerAccountID != 0
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -85,3 +85,12 @@ func TestProductHasAtMostOneCurrentOwner(t *testing.T) {
|
||||
t.Fatal("conflicting warehouse and user owners were accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductRequiresProducer(t *testing.T) {
|
||||
if validProductProducer(models.ProductInfo{}) {
|
||||
t.Fatal("product without producer was accepted")
|
||||
}
|
||||
if !validProductProducer(models.ProductInfo{ProducerAccountID: 1}) {
|
||||
t.Fatal("product with producer was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package platform
|
||||
package platform
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -81,7 +81,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("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", ReadOnly, "list"),
|
||||
resourceContract("product", "producer_account", Writable, "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", ReadOnly, "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", ReadOnly, "list"), resourceContract("ec", "ec_order", ReadOnly, "list"), resourceContract("ec", "ec_order_item", ReadOnly, "list"), resourceContract("ec", "ec_review", ReadOnly, "list"),
|
||||
resourceContract("gasorder", "gasorder_contract", Managed, "list"), resourceContract("gasorder", "gasorder_contract_product", AppendOnly, "list"), resourceContract("gasorder", "gasorder_contract_revision", ReadOnly, "list"),
|
||||
resourceContract("gasorder", "gasorder_basic", AppendOnly, "list"), resourceContract("gasorder", "gasorder_item", ReadOnly, "list"), resourceContract("gasorder", "gasorder_assign", ReadOnly, "list"), resourceContract("gasorder", "gasorder_status", ReadOnly, "list"),
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Entity 是所有主表共享字段。id 是数据库自增主键,identity 是应用生成的 UUID V7 业务标识。
|
||||
type Entity struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 数据库自增主键
|
||||
Identity string `gorm:"column:identity;type:varchar(36);not null;uniqueIndex" json:"identity"` // UUID V7 业务标识
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;index" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null" json:"updated_at"` // 更新时间
|
||||
Status int `gorm:"column:status;not null;default:0;index" json:"status"` // 通用记录状态
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 数据库自增主键
|
||||
Identity string `gorm:"column:identity;type:varchar(36);not null;uniqueIndex" json:"identity"` // UUID V7 业务标识
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null;index" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除时间,非空表示记录已删除但仍保留在数据库中
|
||||
Status int `gorm:"column:status;not null;default:0;index" json:"status"` // 通用记录状态
|
||||
}
|
||||
|
||||
// NewIdentity 生成时间有序的 UUID V7 字符串,生成失败属于不可恢复的运行时错误。
|
||||
|
||||
21
backend/api/internal/models/entity_test.go
Normal file
21
backend/api/internal/models/entity_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestEntityDeletedAtEnablesSoftDelete(t *testing.T) {
|
||||
field, ok := reflect.TypeOf(Entity{}).FieldByName("DeletedAt")
|
||||
if !ok {
|
||||
t.Fatal("Entity 缺少 DeletedAt 软删除字段")
|
||||
}
|
||||
if field.Type != reflect.TypeOf(gorm.DeletedAt{}) {
|
||||
t.Fatalf("DeletedAt 类型为 %v,期望 gorm.DeletedAt", field.Type)
|
||||
}
|
||||
if got := field.Tag.Get("gorm"); got != "index" {
|
||||
t.Fatalf("DeletedAt gorm 标签为 %q,期望 %q", got, "index")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ func TestPlatformModelTableNames(t *testing.T) {
|
||||
{name: "account", table: &PlatformAccount{}, want: "platform_account"},
|
||||
{name: "role", table: &PlatformRole{}, want: "platform_role"},
|
||||
{name: "role menu", table: &PlatformRoleMenu{}, want: "platform_role_menu"},
|
||||
{name: "producer account", table: &ProducerAccount{}, want: "producer_account"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
22
backend/api/internal/models/producer_account.go
Normal file
22
backend/api/internal/models/producer_account.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// ProducerAccount 对应 producer_account,保存可登录的生产商企业主数据。
|
||||
type ProducerAccount struct {
|
||||
Entity // 公共实体字段
|
||||
ProducerCode string `gorm:"column:producer_code;type:varchar(64);not null;uniqueIndex" json:"producer_code"` // 生产商编码
|
||||
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 生产商名称
|
||||
CreditCode string `gorm:"column:credit_code;type:varchar(64);not null;default:'';index" json:"credit_code"` // 统一社会信用代码
|
||||
Principal string `gorm:"column:principal;type:varchar(64);not null;default:''" json:"principal"` // 企业负责人
|
||||
Phone string `gorm:"column:phone;type:varchar(32);not null;default:''" json:"phone"` // 联系电话
|
||||
Address string `gorm:"column:address;type:varchar(255);not null;default:''" json:"address"` // 企业地址
|
||||
Username string `gorm:"column:username;type:varchar(64);not null;uniqueIndex" json:"username"` // 登录用户名
|
||||
DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 登录展示名称
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null" json:"-"` // 登录密码哈希
|
||||
RoleCode string `gorm:"column:role_code;type:varchar(64);not null;default:'admin'" json:"role_code"` // 生产商端角色编码
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProducerAccount{}) }
|
||||
func (table *ProducerAccount) TableName() string { return "producer_account" }
|
||||
@@ -8,18 +8,19 @@ import (
|
||||
|
||||
// ProductInfo 对应 product_info,保存一物一码的实体产品档案。
|
||||
type ProductInfo struct {
|
||||
Entity // 公共实体字段
|
||||
ProductStatus int `gorm:"column:product_status;not null;default:10;index" json:"product_status"` // 产品业务状态
|
||||
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"` // 生产时间
|
||||
EnabledAt *time.Time `gorm:"column:enabled_at;type:timestamptz" json:"enabled_at"` // 首次启用时间
|
||||
Entity // 公共实体字段
|
||||
ProductStatus int `gorm:"column:product_status;not null;default:10;index" json:"product_status"` // 产品业务状态
|
||||
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"` // 产品名称
|
||||
ProducerAccountID uint64 `gorm:"column:producer_account_id;not null;default:0;index" json:"producer_account_id"` // 生产商自增主键;历史数据迁移前可为 0,新建和编辑必须关联有效生产商
|
||||
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"` // 生产时间
|
||||
EnabledAt *time.Time `gorm:"column:enabled_at;type:timestamptz" json:"enabled_at"` // 首次启用时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ProductInfo{}) }
|
||||
|
||||
@@ -107,10 +107,19 @@ func registerGasorderRoute(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func registerProductRoute(group *gin.RouterGroup) {
|
||||
producer := group.Group("/producer_account")
|
||||
producer.GET("", product.ListProducerAccount)
|
||||
producer.POST("", product.CreateProducerAccount)
|
||||
producer.GET("/:identity", product.GetProducerAccount)
|
||||
producer.PUT("/:identity", product.UpdateProducerAccount)
|
||||
producer.PATCH("/:identity/status", func(ctx *gin.Context) { common.UpdateRecordStatus(ctx, &models.ProducerAccount{}) })
|
||||
producer.DELETE("/:identity", product.DeleteProducerAccount)
|
||||
|
||||
registerRestrictedNoDeleteResource(group, "/product_type", &models.ProductType{}, []string{"code", "name"})
|
||||
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone"})
|
||||
|
||||
infoRelations := []common.ResourceRelation{
|
||||
requiredRelation("producer_account_identity", "producer_account_id", &models.ProducerAccount{}),
|
||||
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{}),
|
||||
|
||||
@@ -166,6 +166,10 @@ func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing
|
||||
assertRouteMethods(t, routes, path+"/:identity/status", http.MethodPatch)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodDelete)
|
||||
}
|
||||
producer := "/heqi/platform/v1/producer_account"
|
||||
assertRouteMethods(t, routes, producer, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, producer+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
assertRouteMethods(t, routes, producer+"/:identity/status", http.MethodPatch)
|
||||
owner := "/heqi/platform/v1/product_owner"
|
||||
assertRouteMethods(t, routes, owner, http.MethodGet)
|
||||
assertNoRouteMethods(t, routes, owner, http.MethodPost)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
- 气站管理系统:站点经营、站内库存和日常订单运营仍由后续气站端主责。
|
||||
- 配送点管理系统:末端调度、配送仓和配送作业仍由后续配送点端主责。
|
||||
- 生产管理系统:设备生产、质检、序列号注入和出厂追溯尚未在本后台实现。
|
||||
- 生产管理系统:设备生产、质检、序列号注入和出厂追溯仍由独立生产端主责;平台后台仅治理生产商账户主数据及智能气阀的生产商归属。
|
||||
- API 中心:开放 API 产品、调用方、凭证和配额管理尚未实现。
|
||||
- 用户端与服务端 App:本后台不承担用户设备控制、配送员定位采集或现场取证。
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
## 6. 已实现功能域
|
||||
|
||||
当前资源契约共 47 个资源。下表中的路径均相对于 `/heqi/platform/v1`。
|
||||
当前资源契约共 48 个资源。下表中的路径均相对于 `/heqi/platform/v1`。
|
||||
|
||||
### 6.1 机构管理
|
||||
|
||||
@@ -143,13 +143,14 @@
|
||||
|
||||
| 资源 | 路径 | 模式 | 已实现能力 |
|
||||
| --- | --- | --- | --- |
|
||||
| 生产商 | `/producer_account` | 可写 | 生产商企业与登录账户新增、查询、编辑、启停和软删除;已被智能气阀引用时禁止删除 |
|
||||
| 类型 | `/product_type` | 可编辑 | 类型新增、编辑和启停 |
|
||||
| 库房 | `/product_warehouse` | 可编辑 | 库房新增、编辑和启停 |
|
||||
| 智能气阀 | `/product_info` | 可编辑 | 档案新增、编辑、启停和专用生命周期流转 |
|
||||
| 检修记录 | `/product_repair` | 可编辑 | 检修记录新增、编辑和状态调整 |
|
||||
| 归属记录 | `/product_owner` | 只读 | 查询每次归属变更的动作、操作者、原因和时间 |
|
||||
|
||||
智能气阀必须关联类型,可选关联库房、气站、配送点或用户。归属组合由后端校验,不允许形成非法多重归属。生命周期支持待处理、在库、运输中、使用中、维修中和已报废;状态变化使用 `/product_info/:identity/lifecycle`,不能通过通用编辑直接改写。归属变化自动追加归属记录。
|
||||
智能气阀必须关联生产商和类型,可选关联库房、气站、配送点或用户。生产商登录密码仅保存哈希且不通过资源接口返回;独立生产端登录路由待生产管理系统落地时接入。归属组合由后端校验,不允许形成非法多重归属。生命周期支持待处理、在库、运输中、使用中、维修中和已报废;状态变化使用 `/product_info/:identity/lifecycle`,不能通过通用编辑直接改写。归属变化自动追加归属记录。
|
||||
|
||||
### 6.5 配送合同
|
||||
|
||||
@@ -275,7 +276,7 @@
|
||||
|
||||
1. 匿名用户只能访问健康检查和登录;受保护资源缺少有效 JWT 时被拒绝。
|
||||
2. 非 root 角色只能访问已分配菜单及其明确依赖资源,不能通过猜测路径访问同级资源。
|
||||
3. 47 个资源的后端契约、实际路由、前端资源定义和页面加载关系一致。
|
||||
3. 48 个资源的后端契约、实际路由、前端资源定义和页面加载关系一致。
|
||||
4. 所有表保留自增 `id` 主键;主资源通过 UUID V7 `identity` 对外访问,前端不依赖内部 ID。
|
||||
5. 气站必须先审核再启停,驳回原因和审核人可追溯。
|
||||
6. 智能气阀归属和生命周期变更合法,并自动记录归属历史。
|
||||
|
||||
@@ -82,7 +82,7 @@ const userStore = useUserStore();
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const userInfo = reactive({
|
||||
|
||||
@@ -82,7 +82,7 @@ const userStore = useUserStore();
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const userInfo = reactive({
|
||||
|
||||
@@ -59,6 +59,7 @@ export type ResourceUiDefinition = {
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
code: '编码',
|
||||
producer_code: '生产商编码',
|
||||
name: '名称',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
@@ -299,9 +300,10 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
|
||||
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
|
||||
|
||||
define('producer_account', '生产商管理', 'writable', [f('producer_code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('phone'), f('address'), f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), f('remark')]),
|
||||
define('product_type', '智能气阀类型', 'editable', [f('code', { required: true }), f('name', { required: true })]),
|
||||
define('product_warehouse', '智能气阀库房', 'editable', [f('code', { required: true }), f('name', { required: true }), f('address'), f('manager'), f('phone')]),
|
||||
define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [
|
||||
define('product_info', '智能气阀', 'editable', [f('code', { required: true }), f('name', { required: true }), relation('producer_account_identity', '/producer_account', true), relation('product_type_identity', '/product_type', true), f('params', { required: true }), relation('warehouse_identity', '/product_warehouse'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('user_account_identity', '/user_account'), f('produced_at', { required: true })], 'list', [
|
||||
{ name: '修改智能气阀状态', resource: '/product_info/:identity/lifecycle', method: 'PATCH', fields: [f('product_status', { required: true, type: 'select', options: [{ label: '待处理', value: 10 }, { label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '维修中', value: 31 }, { label: '已报废', value: 27 }] })] },
|
||||
]),
|
||||
define('product_repair', '智能气阀检修记录', 'editable', [relation('product_info_identity', '/product_info', true), f('repair_no', { required: true }), f('repair_type', { required: true }), f('started_at', { required: true }), f('completed_at'), f('result', { type: 'select', options: [{ label: '待处理', value: 'pending' }, { label: '通过', value: 'passed' }, { label: '未通过', value: 'failed' }] }), f('target_product_status', { type: 'select', options: [{ label: '在库', value: 28 }, { label: '运输中', value: 29 }, { label: '使用中', value: 30 }, { label: '报废', value: 27 }] }), f('content'), f('operator'), f('remark')]),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -81,6 +81,7 @@ const routes: AppRouteRecordRaw[] = [
|
||||
child('user', 'contract-revisions', 'contract-revisions', '合同修订记录', '/gasorder_contract_revision', 'gasorder_contract', true, 'user-contracts'),
|
||||
]),
|
||||
group('product', 'product', '智能气阀管理', 'icon-common', 50, [
|
||||
child('product', 'producers', 'producers', '生产商管理', '/producer_account', 'producer_account'),
|
||||
child('product', 'product-type', 'type', '类型管理', '/product_type', 'product_type'),
|
||||
child('product', 'warehouse', 'warehouse', '库房管理', '/product_warehouse', 'product_warehouse'),
|
||||
child('product', 'product-info', 'info', '智能气阀管理', '/product_info', 'product_info'),
|
||||
|
||||
@@ -82,7 +82,7 @@ const userStore = useUserStore();
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const userInfo = reactive({
|
||||
|
||||
Reference in New Issue
Block a user