feat: add producer account management

This commit is contained in:
2026-08-04 15:21:39 +08:00
parent fb59e80fd0
commit 402f050b17
21 changed files with 223 additions and 36 deletions

View File

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

View File

@@ -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{},
}

View File

@@ -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},

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

View File

@@ -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,

View File

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

View File

@@ -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"),

View File

@@ -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 字符串,生成失败属于不可恢复的运行时错误。

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

View File

@@ -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) {

View 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" }

View File

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

View File

@@ -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{}),

View File

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

View File

@@ -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. 智能气阀归属和生命周期变更合法,并自动记录归属历史。

View File

@@ -82,7 +82,7 @@ const userStore = useUserStore();
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: 'admin',
username: '',
password: '',
});
const userInfo = reactive({

View File

@@ -82,7 +82,7 @@ const userStore = useUserStore();
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: 'admin',
username: '',
password: '',
});
const userInfo = reactive({

View File

@@ -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

View File

@@ -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'),

View File

@@ -82,7 +82,7 @@ const userStore = useUserStore();
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: 'admin',
username: '',
password: '',
});
const userInfo = reactive({