diff --git a/backend/api/internal/logic/common/resource.go b/backend/api/internal/logic/common/resource.go
index 8f198c4..531c545 100644
--- a/backend/api/internal/logic/common/resource.go
+++ b/backend/api/internal/logic/common/resource.go
@@ -386,9 +386,8 @@ func ResourceResponse(value any) any {
return stripInternalIDs(decoded)
}
-// PublicResourceResponse additionally resolves persisted relation keys into
-// their public identities. It is used by list/detail endpoints so an edit form
-// can round-trip the relation without ever receiving a surrogate database ID.
+// PublicResourceResponse preserves a resource's own ID for administrative
+// display while resolving and removing persistence-only relation IDs.
func PublicResourceResponse(value any) (any, error) {
encoded, err := json.Marshal(value)
if err != nil {
@@ -450,7 +449,7 @@ type relationIdentityRecord struct {
func projectRelationIdentities(value any) (any, error) {
groups := map[string]*relationIdentityGroup{}
- collectRelationIdentityReferences(value, groups)
+ collectRelationIdentityReferences(value, groups, true)
groupKeys := make([]string, 0, len(groups))
for key := range groups {
groupKeys = append(groupKeys, key)
@@ -477,12 +476,14 @@ func projectRelationIdentities(value any) (any, error) {
return value, nil
}
-func collectRelationIdentityReferences(value any, groups map[string]*relationIdentityGroup) {
+func collectRelationIdentityReferences(value any, groups map[string]*relationIdentityGroup, preserveRecordID bool) {
switch data := value.(type) {
case map[string]any:
for key, item := range data {
if key == "id" {
- delete(data, key)
+ if !preserveRecordID {
+ delete(data, key)
+ }
continue
}
if strings.HasSuffix(key, "_id") {
@@ -515,11 +516,11 @@ func collectRelationIdentityReferences(value any, groups map[string]*relationIde
delete(data, key)
continue
}
- collectRelationIdentityReferences(item, groups)
+ collectRelationIdentityReferences(item, groups, false)
}
case []any:
for _, item := range data {
- collectRelationIdentityReferences(item, groups)
+ collectRelationIdentityReferences(item, groups, preserveRecordID)
}
}
}
diff --git a/backend/api/internal/logic/common/resource_test.go b/backend/api/internal/logic/common/resource_test.go
index f38a16f..e650f83 100644
--- a/backend/api/internal/logic/common/resource_test.go
+++ b/backend/api/internal/logic/common/resource_test.go
@@ -50,6 +50,38 @@ func TestResourceResponseStripsInternalIDsRecursively(t *testing.T) {
}
}
+func TestPublicResourceResponsePreservesOnlyRecordID(t *testing.T) {
+ got, err := PublicResourceResponse(map[string]any{
+ "id": uint64(7), "identity": "record",
+ "child": map[string]any{"id": uint64(8), "identity": "child"},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ record := got.(map[string]any)
+ if record["id"] != float64(7) {
+ t.Fatalf("record ID = %#v, want 7", record["id"])
+ }
+ if _, exists := record["child"].(map[string]any)["id"]; exists {
+ t.Fatal("nested database ID was exposed")
+ }
+}
+
+func TestPublicResourceResponsePreservesListRecordIDs(t *testing.T) {
+ got, err := PublicResourceResponse([]map[string]any{
+ {"id": uint64(11), "identity": "first"},
+ {"id": uint64(12), "identity": "second"},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ list := got.([]any)
+ if list[0].(map[string]any)["id"] != float64(11) ||
+ list[1].(map[string]any)["id"] != float64(12) {
+ t.Fatalf("list record IDs were not preserved: %#v", list)
+ }
+}
+
func TestPublicFieldProtectionMasksGasorderContacts(t *testing.T) {
value := map[string]any{"contact_name": "张三", "contact_phone": "13800138000"}
ProtectPublicFields(value, false, false, false)
diff --git a/backend/api/internal/logic/platform/delivery/delivery.go b/backend/api/internal/logic/platform/delivery/delivery.go
index cafa75a..c462795 100644
--- a/backend/api/internal/logic/platform/delivery/delivery.go
+++ b/backend/api/internal/logic/platform/delivery/delivery.go
@@ -1,16 +1,38 @@
package delivery
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"
+ "gorm.io/gorm"
)
// ListDeliveryBasic 查询配送点分页列表。
-func ListDeliveryBasic(ctx *gin.Context) { common.ListPage[models.DeliveryBasic](ctx) }
+func ListDeliveryBasic(ctx *gin.Context) {
+ gasIdentities := strings.Split(strings.TrimSpace(ctx.Query("gas_basic_identities")), ",")
+ if len(gasIdentities) == 1 && gasIdentities[0] == "" {
+ gasIdentities = nil
+ }
+ if len(gasIdentities) > 100 {
+ infra.Response.Error(ctx, errcode.ErrInvalidArgument)
+ return
+ }
+ common.ListPageFiltered[models.DeliveryBasic](ctx, func(query *gorm.DB) *gorm.DB {
+ if len(gasIdentities) == 0 {
+ return query
+ }
+ return query.Where(
+ "gas_basic_id IN (SELECT id FROM gas_basic WHERE identity IN ? AND status <> ?)",
+ gasIdentities,
+ common.StatusArchived,
+ )
+ })
+}
// GetDeliveryBasic 查询一个配送点。
func GetDeliveryBasic(ctx *gin.Context) { common.GetByIdentity[models.DeliveryBasic](ctx) }
diff --git a/backend/api/internal/logic/platform/gas/gas.go b/backend/api/internal/logic/platform/gas/gas.go
index 1959a82..3031935 100644
--- a/backend/api/internal/logic/platform/gas/gas.go
+++ b/backend/api/internal/logic/platform/gas/gas.go
@@ -1,12 +1,18 @@
package gas
import (
+ "errors"
+ "strings"
+ "time"
+
"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"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
)
// ListGasBasic 查询可燃气体站分页列表。
@@ -46,3 +52,78 @@ func UpdateGasBasic(ctx *gin.Context) {
}
common.UpdateAllowedByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude}, []string{"name", "credit_code", "principal", "address", "longitude", "latitude"})
}
+
+// UpdateGasBasicStatus 仅允许已审核气站在启用和停用之间切换。
+func UpdateGasBasicStatus(ctx *gin.Context) {
+ var request struct {
+ Status int `json:"status" binding:"required"`
+ }
+ if err := ctx.ShouldBindJSON(&request); err != nil ||
+ (request.Status != common.StatusEnable && request.Status != common.StatusDisable) {
+ infra.Response.Error(ctx, errcode.ErrInvalidArgument)
+ return
+ }
+ result := impl.DBService.Model(&models.GasBasic{}).
+ Where("identity = ? AND status IN ?", ctx.Param("identity"), []int{common.StatusEnable, common.StatusDisable}).
+ Update("status", request.Status)
+ if result.Error != nil {
+ infra.Response.Error(ctx, result.Error)
+ return
+ }
+ if result.RowsAffected != 1 {
+ infra.Response.Error(ctx, errcode.ErrInvalidArgument)
+ return
+ }
+ infra.Response.Success(ctx, gin.H{"updated": true})
+}
+
+// ReviewGasBasic 审核待审核气站;不通过时必须填写理由。
+func ReviewGasBasic(ctx *gin.Context) {
+ var request struct {
+ Approved bool `json:"approved"`
+ Reason string `json:"reason" binding:"max=2000"`
+ }
+ if err := ctx.ShouldBindJSON(&request); err != nil ||
+ (!request.Approved && strings.TrimSpace(request.Reason) == "") {
+ infra.Response.Error(ctx, errcode.ErrInvalidArgument)
+ return
+ }
+
+ operatorIdentity, operatorName := common.PlatformOperator(ctx)
+ targetStatus := common.StatusDisable
+ reviewStatus := common.StatusRejected
+ if request.Approved {
+ targetStatus = common.StatusEnable
+ reviewStatus = common.StatusApproved
+ }
+ now := time.Now()
+ err := impl.DBService.Transaction(func(tx *gorm.DB) error {
+ var station models.GasBasic
+ if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
+ Where("identity = ? AND status <> ?", ctx.Param("identity"), common.StatusArchived).
+ First(&station).Error; err != nil {
+ return err
+ }
+ if station.Status != common.StatusDraft {
+ return errors.New("gas station is not pending review")
+ }
+ if err := tx.Model(&station).Update("status", targetStatus).Error; err != nil {
+ return err
+ }
+ review := models.GasBasicReview{
+ Entity: common.NewEntity(common.StatusEnable),
+ GasBasicID: station.ID,
+ ReviewStatus: reviewStatus,
+ ReviewReason: strings.TrimSpace(request.Reason),
+ ReviewerIdentity: operatorIdentity,
+ ReviewerName: operatorName,
+ ReviewedAt: now,
+ }
+ return tx.Create(&review).Error
+ })
+ if err != nil {
+ common.RespondRecordError(ctx, err)
+ return
+ }
+ infra.Response.Success(ctx, gin.H{"updated": true, "status": targetStatus})
+}
diff --git a/backend/api/internal/logic/platform/platform/access.go b/backend/api/internal/logic/platform/platform/access.go
index 6b38f1d..af648cf 100644
--- a/backend/api/internal/logic/platform/platform/access.go
+++ b/backend/api/internal/logic/platform/platform/access.go
@@ -46,6 +46,9 @@ func platformMenuAllowsRequest(menus []platformbase.Menu, requestPath, method st
(resource == "delivery_basic" || resource == "staff_account") {
return true
}
+ if method == "GET" && menu.Identity == "gas_basic" && resource == "delivery_basic" {
+ return true
+ }
if method == "GET" && relative == "wallet_basic" &&
(menu.Identity == "gas_basic" || menu.Identity == "delivery_basic" ||
menu.Identity == "staff" || menu.Identity == "user_account" ||
diff --git a/backend/api/internal/logic/platform/platform/access_test.go b/backend/api/internal/logic/platform/platform/access_test.go
index 598631d..8a9c405 100644
--- a/backend/api/internal/logic/platform/platform/access_test.go
+++ b/backend/api/internal/logic/platform/platform/access_test.go
@@ -58,6 +58,16 @@ func TestOwnerMenusCanReadEmbeddedWallets(t *testing.T) {
}
}
+func TestGasManagementCanReadDeliveryPointCounts(t *testing.T) {
+ menus := []platformbase.Menu{{Identity: "gas_basic"}}
+ if !platformMenuAllowsRequest(menus, "/heqi/platform/v1/delivery_basic", "GET") {
+ t.Fatal("gas management could not read its delivery point counts")
+ }
+ if platformMenuAllowsRequest(menus, "/heqi/platform/v1/delivery_basic", "POST") {
+ t.Fatal("gas management unexpectedly received delivery point creation access")
+ }
+}
+
func TestLocationScopeValuesAreExplicit(t *testing.T) {
if !validLocationScope("standard") || !validLocationScope("precise") {
t.Fatal("supported location scopes were rejected")
diff --git a/backend/api/internal/models/gas_basic_review.go b/backend/api/internal/models/gas_basic_review.go
new file mode 100644
index 0000000..04e05ff
--- /dev/null
+++ b/backend/api/internal/models/gas_basic_review.go
@@ -0,0 +1,23 @@
+package models
+
+import (
+ "time"
+
+ "git.apinb.com/bsm-sdk/core/database"
+)
+
+// GasBasicReview 对应 gas_basic_review,保存气站审核记录。
+type GasBasicReview struct {
+ Entity // 公共实体字段
+ GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 气站自增主键
+ ReviewStatus int `gorm:"column:review_status;not null;index" json:"review_status"` // 审核结果:已通过或已驳回
+ ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核不通过理由
+ ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:'';index" json:"reviewer_identity"` // 审核人业务标识
+ ReviewerName string `gorm:"column:reviewer_name;type:varchar(64);not null;default:''" json:"reviewer_name"` // 审核人姓名快照
+ ReviewedAt time.Time `gorm:"column:reviewed_at;type:timestamptz;not null" json:"reviewed_at"` // 审核时间
+}
+
+func init() { database.AppendMigrate(&GasBasicReview{}) }
+
+// TableName 返回与模型、文件名一致的单数数据表名。
+func (table *GasBasicReview) TableName() string { return "gas_basic_review" }
diff --git a/backend/api/internal/routers/platform.go b/backend/api/internal/routers/platform.go
index 7d3e3dc..47affc4 100644
--- a/backend/api/internal/routers/platform.go
+++ b/backend/api/internal/routers/platform.go
@@ -49,7 +49,14 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
}
func registerGasRoute(group *gin.RouterGroup) {
- registerWritableResource(group, "/gas_basic", gas.ListGasBasic, gas.CreateGasBasic, gas.GetGasBasic, gas.UpdateGasBasic, &models.GasBasic{})
+ basic := group.Group("/gas_basic")
+ basic.GET("", gas.ListGasBasic)
+ basic.POST("", gas.CreateGasBasic)
+ basic.GET("/:identity", gas.GetGasBasic)
+ basic.PUT("/:identity", gas.UpdateGasBasic)
+ basic.PATCH("/:identity/status", gas.UpdateGasBasicStatus)
+ basic.POST("/:identity/review", gas.ReviewGasBasic)
+ basic.DELETE("/:identity", func(ctx *gin.Context) { common.ArchiveRecord(ctx, &models.GasBasic{}) })
registerWritableResource(group, "/gas_account", gas.ListGasAccount, gas.CreateGasAccount, gas.GetGasAccount, gas.UpdateGasAccount, &models.GasAccount{})
}
diff --git a/backend/api/internal/routers/platform_test.go b/backend/api/internal/routers/platform_test.go
index 3c64f8b..f357575 100644
--- a/backend/api/internal/routers/platform_test.go
+++ b/backend/api/internal/routers/platform_test.go
@@ -63,6 +63,18 @@ func TestPlatformGasRouteUsesGasBasic(t *testing.T) {
t.Fatal("gas_basic list route is not registered")
}
+func TestPlatformGasRouteExposesReview(t *testing.T) {
+ engine := gin.New()
+ RegisterPlatform("heqi", engine)
+
+ for _, route := range engine.Routes() {
+ if route.Method == http.MethodPost && route.Path == "/heqi/platform/v1/gas_basic/:identity/review" {
+ return
+ }
+ }
+ t.Fatal("gas_basic review route is not registered")
+}
+
func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) {
engine := gin.New()
RegisterPlatform("heqi", engine)
diff --git a/frontend/platform_admin/src/views/shared/CrudListPage.vue b/frontend/platform_admin/src/views/shared/CrudListPage.vue
index 821eb11..221d707 100644
--- a/frontend/platform_admin/src/views/shared/CrudListPage.vue
+++ b/frontend/platform_admin/src/views/shared/CrudListPage.vue
@@ -3,8 +3,14 @@
返回机构列表
- 刷新
- 新建
+
+
+ 刷新
+
+
+
+ 新建
+
@@ -16,7 +22,7 @@
-
+
{{ displayFieldValue(field, record) }}
@@ -25,6 +31,9 @@
{{ accountCounts[String(record.identity)] ?? 0 }}
+
+ {{ deliveryPointCounts[String(record.identity)] ?? 0 }}
+
@@ -40,6 +49,13 @@
未开通
+
+
+
+ {{ recordStatusLabel(Number(record.status)) }}
+
+
+
@@ -47,7 +63,7 @@
查看资质
详情
编辑
- 审核
+ 审核
删除
@@ -57,6 +73,76 @@
+
+
+
+
+
+
+
+
+
+
+ {{ Number(record.status) === 1 ? '启用' : '停用' }}
+
+
+
+
+
+
+ 编辑
+
+ {{ Number(record.status) === 1 ? '禁用' : '启用' }}
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -89,6 +175,37 @@
{{ displayValue(key, value) }}
+
+
当前状态
+
+ {{ recordStatusLabel(Number(detail.status)) }}
+
+
+ 修改状态
+
+
+
+
+
气站审核
+
+ 审核通过
+
+ 审核不通过
+
+
@@ -126,6 +243,7 @@