统一合同气阀候选与绑定入口

This commit is contained in:
czl231
2026-08-31 22:04:22 +08:00
parent 9a23ab2f5a
commit 0f2475098f
23 changed files with 352 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
// 功能描述:实现配送点范围内的合同、合同气瓶和配送订单接口。 // 功能描述:实现配送点范围内的合同、合同气瓶和配送订单接口。
// 版本v1.4.1 // 版本v1.5.0
package delivery package delivery
import ( import (
@@ -22,6 +22,8 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
var errContractUserOutsidePoint = errors.New("签约用户已不属于当前配送点,只允许查看或终止合同")
func rewriteJSON(ctx *gin.Context, mutate func(map[string]any) bool) bool { func rewriteJSON(ctx *gin.Context, mutate func(map[string]any) bool) bool {
body, err := io.ReadAll(ctx.Request.Body) body, err := io.ReadAll(ctx.Request.Body)
if err != nil { if err != nil {
@@ -49,6 +51,16 @@ func scopedContract(ctx *gin.Context, identity string, pointID uint64) (models.G
return contract, true return contract, true
} }
// contractUserActiveAtPoint 校验合同用户仍属于合同气站及当前配送点。
func contractUserActiveAtPoint(databaseService *gorm.DB, contract models.GasorderContract, pointID uint64) bool {
var count int64
err := databaseService.Model(&models.UserServiceRelation{}).
Where("user_account_id = ? AND gas_basic_id = ? AND delivery_basic_id = ? AND status <> ?",
contract.UserAccountID, contract.GasBasicID, pointID, common.StatusArchived).
Count(&count).Error
return err == nil && count > 0
}
// deliveryContractListQuery 构造当前配送点合同列表;订单候选模式额外限定可履约状态。 // deliveryContractListQuery 构造当前配送点合同列表;订单候选模式额外限定可履约状态。
func deliveryContractListQuery(database *gorm.DB, gasID, pointID uint64, candidate string, now time.Time) *gorm.DB { func deliveryContractListQuery(database *gorm.DB, gasID, pointID uint64, candidate string, now time.Time) *gorm.DB {
query := platformgasorder.ContractPartyDisplayQuery(database). query := platformgasorder.ContractPartyDisplayQuery(database).
@@ -280,8 +292,15 @@ func BindContractProduct(ctx *gin.Context) {
} }
if !rewriteJSON(ctx, func(values map[string]any) bool { if !rewriteJSON(ctx, func(values map[string]any) bool {
identity, _ := values["gasorder_contract_identity"].(string) identity, _ := values["gasorder_contract_identity"].(string)
_, valid := scopedContract(ctx, identity, point.ID) contract, valid := scopedContract(ctx, identity, point.ID)
return valid if !valid {
return false
}
if !contractUserActiveAtPoint(db(), contract, point.ID) {
infra.Response.Error(ctx, errContractUserOutsidePoint)
return false
}
return true
}) { }) {
return return
} }
@@ -329,28 +348,54 @@ func GetContractRevision(ctx *gin.Context) {
respondRecord(ctx, query, &item) respondRecord(ctx, query, &item)
} }
// deliveryContractProductCandidateQuery 按合同用户及当前配送点服务关系限定候选气阀。
func deliveryContractProductCandidateQuery(databaseService *gorm.DB, contract models.GasorderContract, pointID uint64) *gorm.DB {
query := common.ActiveRecords(databaseService.Model(&models.ProductInfo{}))
return platformgasorder.ContractProductCandidateQuery(query, contract).
Where(`EXISTS (
SELECT 1 FROM user_service_relation AS candidate_relation
WHERE candidate_relation.user_account_id = product_info.user_account_id
AND candidate_relation.gas_basic_id = ?
AND candidate_relation.delivery_basic_id = ?
AND candidate_relation.status <> ?
AND candidate_relation.deleted_at IS NULL
)`, contract.GasBasicID, pointID, common.StatusArchived)
}
// ListProductCandidate 仅返回属于指定合同用户且仍在当前配送点服务范围内的候选气阀。
func ListProductCandidate(ctx *gin.Context) { func ListProductCandidate(ctx *gin.Context) {
point, _, ok := currentScope(ctx) point, _, ok := currentScope(ctx)
if !ok { if !ok {
return return
} }
query := common.ActiveRecords(db().Model(&models.ProductInfo{})). query := common.ActiveRecords(db().Model(&models.ProductInfo{})).Where("1 = 0")
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?", if contractIdentity := strings.TrimSpace(ctx.Query("contract_identity")); contractIdentity != "" {
common.StatusArchived).Where("user_service_relation.delivery_basic_id = ?", point.ID) contract, valid := scopedContract(ctx, contractIdentity, point.ID)
if !valid {
return
}
query = deliveryContractProductCandidateQuery(db(), contract, point.ID)
}
listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc") listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc")
} }
// GetProductCandidate 返回指定合同用户范围内的单条候选气阀。
func GetProductCandidate(ctx *gin.Context) { func GetProductCandidate(ctx *gin.Context) {
point, _, ok := currentScope(ctx) point, _, ok := currentScope(ctx)
if !ok { if !ok {
return return
} }
var item models.ProductInfo var item models.ProductInfo
query := common.ActiveRecords(db().Model(&models.ProductInfo{})). contractIdentity := strings.TrimSpace(ctx.Query("contract_identity"))
Select("product_info.*"). query := common.ActiveRecords(db().Model(&models.ProductInfo{})).Where("1 = 0")
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?", if contractIdentity != "" {
common.StatusArchived). contract, valid := scopedContract(ctx, contractIdentity, point.ID)
Where("product_info.identity = ? AND user_service_relation.delivery_basic_id = ?", ctx.Param("identity"), point.ID) if !valid {
return
}
query = deliveryContractProductCandidateQuery(db(), contract, point.ID)
}
query = query.Select("product_info.*").Where("product_info.identity = ?", ctx.Param("identity"))
respondRecord(ctx, query, &item) respondRecord(ctx, query, &item)
} }

View File

@@ -1,5 +1,5 @@
// 功能描述:验证配送点创建订单的合同、地址和合同气瓶候选查询范围。 // 功能描述:验证配送点创建订单的合同、地址和合同气瓶候选查询范围。
// 版本v1.0.0。 // 版本v1.1.0。
package delivery package delivery
import ( import (
@@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
@@ -76,3 +77,19 @@ func TestDeliveryOrderContractProductCandidates(t *testing.T) {
"gasorder_contract_product.product_code", "gasorder_contract_product.product_code",
) )
} }
// TestDeliveryContractProductBindingCandidates 验证配送点候选同时受合同用户和当前配送点服务关系约束。
func TestDeliveryContractProductBindingCandidates(t *testing.T) {
database := candidateTestDatabase(t)
statement := database.ToSQL(func(tx *gorm.DB) *gorm.DB {
contract := models.GasorderContract{Entity: models.Entity{ID: 27}, UserAccountID: 31, GasBasicID: 7, DeliveryBasicID: 9}
return deliveryContractProductCandidateQuery(tx, contract, 9).Find(&[]models.ProductInfo{})
})
assertSQLFragments(t, statement,
"product_info.user_account_id = 31",
"candidate_binding.gasorder_contract_id = 27",
"candidate_relation.gas_basic_id = 7",
"candidate_relation.delivery_basic_id = 9",
"candidate_relation.user_account_id = product_info.user_account_id",
)
}

View File

@@ -1,5 +1,5 @@
// 功能描述:实现气站范围内的配送合同、合同气瓶和燃气配送订单接口。 // 功能描述:实现气站范围内的配送合同、合同气瓶和燃气配送订单接口。
// 版本v1.2.0 // 版本v1.3.0
package gas package gas
import ( import (
@@ -233,29 +233,52 @@ func GetGasorderContractProduct(ctx *gin.Context) {
respondScopedRecord(ctx, query, &models.GasorderContractProduct{}) respondScopedRecord(ctx, query, &models.GasorderContractProduct{})
} }
// ListContractProductCandidate 仅返回当前气站服务用户名下可用于合同绑定的气瓶 // gasContractProductCandidateQuery 按合同用户和当前气站服务关系限定可绑定智能气阀
func gasContractProductCandidateQuery(databaseService *gorm.DB, contract models.GasorderContract, gasBasicID uint64) *gorm.DB {
query := common.ActiveRecords(databaseService.Model(&models.ProductInfo{}))
return platformgasorder.ContractProductCandidateQuery(query, contract).
Where(`EXISTS (
SELECT 1 FROM user_service_relation AS candidate_relation
WHERE candidate_relation.user_account_id = product_info.user_account_id
AND candidate_relation.gas_basic_id = ?
AND candidate_relation.status <> ?
AND candidate_relation.deleted_at IS NULL
)`, gasBasicID, common.StatusArchived)
}
// ListContractProductCandidate 仅返回属于指定合同用户且仍在当前气站服务范围内的可绑定气阀。
func ListContractProductCandidate(ctx *gin.Context) { func ListContractProductCandidate(ctx *gin.Context) {
station, ok := currentGas(ctx) station, ok := currentGas(ctx)
if !ok { if !ok {
return return
} }
query := common.ActiveRecords(impl.DBService.Model(&models.ProductInfo{})). query := common.ActiveRecords(impl.DBService.Model(&models.ProductInfo{})).Where("1 = 0")
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?", if contractIdentity := strings.TrimSpace(ctx.Query("contract_identity")); contractIdentity != "" {
common.StatusArchived). contract, valid := scopedContract(ctx, contractIdentity, station.ID)
Where("user_service_relation.gas_basic_id = ?", station.ID) if !valid {
return
}
query = gasContractProductCandidateQuery(impl.DBService, contract, station.ID)
}
listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc") listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc")
} }
// GetContractProductCandidate 返回当前气站服务用户范围内的单条候选气 // GetContractProductCandidate 返回指定合同用户范围内的单条候选气
func GetContractProductCandidate(ctx *gin.Context) { func GetContractProductCandidate(ctx *gin.Context) {
station, ok := currentGas(ctx) station, ok := currentGas(ctx)
if !ok { if !ok {
return return
} }
query := common.ActiveRecords(impl.DBService.Model(&models.ProductInfo{})). contractIdentity := strings.TrimSpace(ctx.Query("contract_identity"))
Select("product_info.*"). query := common.ActiveRecords(impl.DBService.Model(&models.ProductInfo{})).Where("1 = 0")
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?", common.StatusArchived). if contractIdentity != "" {
Where("product_info.identity = ? AND user_service_relation.gas_basic_id = ?", ctx.Param("identity"), station.ID) contract, valid := scopedContract(ctx, contractIdentity, station.ID)
if !valid {
return
}
query = gasContractProductCandidateQuery(impl.DBService, contract, station.ID)
}
query = query.Select("product_info.*").Where("product_info.identity = ?", ctx.Param("identity"))
respondScopedRecord(ctx, query, &models.ProductInfo{}) respondScopedRecord(ctx, query, &models.ProductInfo{})
} }

View File

@@ -1,11 +1,12 @@
// 功能描述:验证气站合同与配送订单展示查询的对象权限范围。 // 功能描述:验证气站合同与配送订单展示查询的对象权限范围。
// 版本v1.0.0 // 版本v1.1.0
package gas package gas
import ( import (
"strings" "strings"
"testing" "testing"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
@@ -35,3 +36,28 @@ func TestScopedOrderDisplayQuery(t *testing.T) {
} }
} }
} }
// TestGasContractProductCandidates 验证气站候选按指定合同用户过滤,而非返回本站全部用户设备。
func TestGasContractProductCandidates(t *testing.T) {
connection, _, err := sqlmock.New()
if err != nil {
t.Fatalf("创建 SQL 模拟失败:%v", err)
}
t.Cleanup(func() { _ = connection.Close() })
databaseService, err := gorm.Open(postgres.New(postgres.Config{Conn: connection}), &gorm.Config{DryRun: true})
if err != nil {
t.Fatalf("创建 GORM 数据库失败:%v", err)
}
statement := gasContractProductCandidateQuery(databaseService, models.GasorderContract{Entity: models.Entity{ID: 17}, UserAccountID: 23}, 42).
Find(&[]models.ProductInfo{}).Statement.SQL.String()
for _, fragment := range []string{
"product_info.user_account_id =",
"candidate_binding.gasorder_contract_id =",
"candidate_relation.gas_basic_id =",
"candidate_relation.user_account_id = product_info.user_account_id",
} {
if !strings.Contains(statement, fragment) {
t.Fatalf("气站合同气阀候选 SQL 缺少 %q%s", fragment, statement)
}
}
}

View File

@@ -1,5 +1,5 @@
// 功能描述:为配送合同提供关联主体名称和当前服务关系状态,不新增数据库字段。 // 功能描述:为配送合同提供关联主体名称和当前服务关系状态,不新增数据库字段。
// 版本v1.1.0 // 版本v1.2.0
package gasorder package gasorder
import ( import (
@@ -15,6 +15,7 @@ type ContractPartyDisplay struct {
GasBasicDisplayName string `gorm:"column:gas_basic_display_name" json:"gas_basic_display_name"` GasBasicDisplayName string `gorm:"column:gas_basic_display_name" json:"gas_basic_display_name"`
DeliveryDisplayName string `gorm:"column:delivery_basic_display_name" json:"delivery_basic_display_name"` DeliveryDisplayName string `gorm:"column:delivery_basic_display_name" json:"delivery_basic_display_name"`
UserServiceActive bool `gorm:"column:user_service_active" json:"user_service_active"` UserServiceActive bool `gorm:"column:user_service_active" json:"user_service_active"`
UserDeliveryActive bool `gorm:"column:user_delivery_service_active" json:"user_delivery_service_active"`
} }
// ContractPartyDisplayQuery 仅通过合同已保存的关联主键读取名称,不开放平台主体全集。 // ContractPartyDisplayQuery 仅通过合同已保存的关联主键读取名称,不开放平台主体全集。
@@ -33,7 +34,18 @@ func ContractPartyDisplayQuery(databaseService *gorm.DB) *gorm.DB {
AND contract_relation.gas_basic_id = gasorder_contract.gas_basic_id AND contract_relation.gas_basic_id = gasorder_contract.gas_basic_id
AND contract_relation.status <> ? AND contract_relation.status <> ?
AND contract_relation.deleted_at IS NULL AND contract_relation.deleted_at IS NULL
) AS user_service_active`, common.StatusArchived). ) AS user_service_active,
CASE
WHEN gasorder_contract.delivery_basic_id = 0 THEN FALSE
ELSE EXISTS (
SELECT 1 FROM user_service_relation AS delivery_relation
WHERE delivery_relation.user_account_id = gasorder_contract.user_account_id
AND delivery_relation.gas_basic_id = gasorder_contract.gas_basic_id
AND delivery_relation.delivery_basic_id = gasorder_contract.delivery_basic_id
AND delivery_relation.status <> ?
AND delivery_relation.deleted_at IS NULL
)
END AS user_delivery_service_active`, common.StatusArchived, common.StatusArchived).
Joins("LEFT JOIN user_account AS contract_user ON contract_user.id = gasorder_contract.user_account_id"). Joins("LEFT JOIN user_account AS contract_user ON contract_user.id = gasorder_contract.user_account_id").
Joins("LEFT JOIN gas_basic AS contract_gas ON contract_gas.id = gasorder_contract.gas_basic_id"). Joins("LEFT JOIN gas_basic AS contract_gas ON contract_gas.id = gasorder_contract.gas_basic_id").
Joins("LEFT JOIN delivery_basic AS contract_delivery ON contract_delivery.id = gasorder_contract.delivery_basic_id") Joins("LEFT JOIN delivery_basic AS contract_delivery ON contract_delivery.id = gasorder_contract.delivery_basic_id")

View File

@@ -0,0 +1,22 @@
// 功能描述:提供跨管理端一致的合同可绑定智能气阀候选查询。
// 版本v1.0.0。
package gasorder
import (
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"gorm.io/gorm"
)
// ContractProductCandidateQuery 仅返回属于合同用户、可用、未报废且未重复绑定的智能气阀。
func ContractProductCandidateQuery(query *gorm.DB, contract models.GasorderContract) *gorm.DB {
return query.
Where("product_info.status = ? AND product_info.product_status <> ? AND product_info.user_account_id = ?",
common.StatusEnable, common.StatusScrapped, contract.UserAccountID).
Where(`NOT EXISTS (
SELECT 1 FROM gasorder_contract_product AS candidate_binding
WHERE candidate_binding.product_info_id = product_info.id
AND candidate_binding.gasorder_contract_id = ?
AND candidate_binding.unbound_at IS NULL
)`, contract.ID)
}

View File

@@ -1,5 +1,5 @@
// 功能描述:验证平台配送合同、订单创建和调度状态规则。 // 功能描述:验证平台配送合同、订单创建和调度状态规则。
// 版本v1.3.0 // 版本v1.4.0
package gasorder package gasorder
import ( import (
@@ -100,6 +100,8 @@ func TestContractPartyDisplayQuery(t *testing.T) {
"delivery_basic_display_name", "delivery_basic_display_name",
"contract_relation.user_account_id = gasorder_contract.user_account_id", "contract_relation.user_account_id = gasorder_contract.user_account_id",
"contract_relation.gas_basic_id = gasorder_contract.gas_basic_id", "contract_relation.gas_basic_id = gasorder_contract.gas_basic_id",
"delivery_relation.delivery_basic_id = gasorder_contract.delivery_basic_id",
"user_delivery_service_active",
"gasorder_contract.gas_basic_id = 42", "gasorder_contract.gas_basic_id = 42",
} { } {
if !strings.Contains(statement, fragment) { if !strings.Contains(statement, fragment) {
@@ -108,6 +110,35 @@ func TestContractPartyDisplayQuery(t *testing.T) {
} }
} }
// TestContractProductCandidateQuery 验证三端共享候选严格按合同用户和设备可用性筛选。
func TestContractProductCandidateQuery(t *testing.T) {
sqlDatabase, _, err := sqlmock.New()
if err != nil {
t.Fatalf("创建模拟数据库失败:%v", err)
}
defer sqlDatabase.Close()
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
if err != nil {
t.Fatalf("创建 GORM 数据库失败:%v", err)
}
statement := database.ToSQL(func(tx *gorm.DB) *gorm.DB {
query := common.ActiveRecords(tx.Model(&models.ProductInfo{}))
return ContractProductCandidateQuery(query, models.GasorderContract{Entity: models.Entity{ID: 17}, UserAccountID: 23}).
Find(&[]models.ProductInfo{})
})
for _, fragment := range []string{
"product_info.status = 1",
"product_info.product_status <> 27",
"product_info.user_account_id = 23",
"candidate_binding.gasorder_contract_id = 17",
"candidate_binding.unbound_at IS NULL",
} {
if !strings.Contains(statement, fragment) {
t.Fatalf("合同气阀候选 SQL 缺少 %q%s", fragment, statement)
}
}
}
// TestFilterGasorderContractProductCandidates 验证订单候选仅包含当前合同未解绑气瓶并按业务字段排序。 // TestFilterGasorderContractProductCandidates 验证订单候选仅包含当前合同未解绑气瓶并按业务字段排序。
func TestFilterGasorderContractProductCandidates(t *testing.T) { func TestFilterGasorderContractProductCandidates(t *testing.T) {
sqlDatabase, _, err := sqlmock.New() sqlDatabase, _, err := sqlmock.New()

View File

@@ -11,6 +11,7 @@ import (
"git.apinb.com/bsm-sdk/core/middleware" "git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "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/logic/common"
platformgasorder "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gasorder"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models" "git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
@@ -47,9 +48,7 @@ func listProductInfo(ctx *gin.Context) {
return return
} }
common.ListPageFiltered[models.ProductInfo](ctx, func(query *gorm.DB) *gorm.DB { common.ListPageFiltered[models.ProductInfo](ctx, func(query *gorm.DB) *gorm.DB {
return query. return platformgasorder.ContractProductCandidateQuery(query, contract)
Where("status = ? AND product_status <> ? AND user_account_id = ?", common.StatusEnable, common.StatusScrapped, contract.UserAccountID).
Where("NOT EXISTS (SELECT 1 FROM gasorder_contract_product WHERE gasorder_contract_product.product_info_id = product_info.id AND gasorder_contract_product.gasorder_contract_id = ? AND gasorder_contract_product.unbound_at IS NULL)", contract.ID)
}) })
} }

View File

@@ -106,7 +106,7 @@
- 配送合同创建、草稿编辑和续签必须填写到期时间,且到期时间必须晚于生效时间;历史缺失值应提示补录,不得推测回填。 - 配送合同创建、草稿编辑和续签必须填写到期时间,且到期时间必须晚于生效时间;历史缺失值应提示补录,不得推测回填。
- 合同只能选择当前气站用户及当前气站配送点。 - 合同只能选择当前气站用户及当前气站配送点。
- 合同附件仅支持单个不超过 10 MiB 的 PDF创建和草稿编辑时通过受控接口上传、替换或移除详情页通过气站鉴权和合同归属校验后预览不向前端暴露内部存储地址。 - 合同附件仅支持单个不超过 10 MiB 的 PDF创建和草稿编辑时通过受控接口上传、替换或移除详情页通过气站鉴权和合同归属校验后预览不向前端暴露内部存储地址。
- 合同气瓶支持绑定和受控解绑。 - 合同气瓶支持绑定和受控解绑;绑定候选必须按所选合同筛选,只允许选择属于合同签约用户、已启用、未报废且未重复绑定的智能气阀。缺少合同上下文时不得返回当前气站其他用户的设备;无候选设备时应引导平台总后台先将设备归属变更为合同用户
- 合同修订记录只读,不允许修改或删除。 - 合同修订记录只读,不允许修改或删除。
- 合同修订记录不展示公共软删除时间等无业务意义的基础设施字段。 - 合同修订记录不展示公共软删除时间等无业务意义的基础设施字段。
- 合同状态只能通过专用动作接口推进,不能通用编辑 `contract_status` - 合同状态只能通过专用动作接口推进,不能通用编辑 `contract_status`

View File

@@ -113,7 +113,7 @@ Global:
- 配送点可完整管理当前配送点用户的配送合同。 - 配送点可完整管理当前配送点用户的配送合同。
- 支持创建、草稿编辑、启用、续签和终止。 - 支持创建、草稿编辑、启用、续签和终止。
- 启用仅适用于草稿合同;已终止合同不得直接启用,只能通过续签填写新的生效时间、到期时间和原因后恢复履约。 - 启用仅适用于草稿合同;已终止合同不得直接启用,只能通过续签填写新的生效时间、到期时间和原因后恢复履约。
- 支持绑定和受控解绑当前用户已有气瓶。 - 支持绑定和受控解绑当前用户已有气瓶;绑定候选必须按所选合同筛选,只允许选择属于合同签约用户、已启用、未报废且未重复绑定的智能气阀,并要求该用户当前仍属于本配送点。缺少合同上下文时不得返回本点其他用户的设备;无候选设备时应引导平台总后台先将设备归属变更为合同用户
- 合同修订记录只读。 - 合同修订记录只读。
- 服务端强制合同所属气站和配送点为当前范围。 - 服务端强制合同所属气站和配送点为当前范围。
- 不得查看或操作所属气站下其他配送点的合同。 - 不得查看或操作所属气站下其他配送点的合同。

View File

@@ -0,0 +1,26 @@
# 合同气阀候选范围统一操作日志
操作时间2026-08-31
操作类型:修改
影响模块:平台合同候选、气站合同、配送点合同
## 操作前状态
平台总后台按合同用户筛选可绑定智能气阀;气站端按当前气站全部服务用户筛选,配送点端按当前配送点全部服务用户筛选。组织端可能显示其他用户的设备,配送点绑定入口也未单独判断用户是否仍属于当前配送点。
## 具体操作
1. 提取平台、气站和配送点共用的合同智能气阀候选规则。
2. 气站和配送点候选接口要求合同上下文,并校验当前组织范围。
3. 配送点合同响应增加当前配送点服务关系派生状态。
4. 配送点绑定接口增加当前配送点服务关系校验。
5. 两个组织端的智能气阀下拉携带合同标识,并统一空状态引导。
6. 增加 SQL 范围测试和前端静态契约检查。
## 操作后状态
三个管理端均只展示当前合同用户可绑定的智能气阀。缺少合同上下文时不返回组织范围设备;配送点用户转出后不能再从页面或接口绑定气阀。
## 风险评估
候选范围较原组织端更严格,历史上依赖组织级宽泛候选的错误操作将不再可用。合同气瓶最终绑定接口原有的设备归属校验保持不变,不影响合法绑定记录及历史数据。

View File

@@ -0,0 +1,30 @@
# 操作日志:配送点合同气瓶绑定入口
操作时间2026-08-31
操作类型:扩展
影响模块:配送点合同列表、合同气瓶新建页
## 操作前状态
配送点需求、后端接口和隐藏子路由均支持合同气瓶绑定,但配送合同列表没有进入新建页的入口。
## 具体操作
1. 草稿合同且签约用户服务关系有效时显示“绑定气瓶”。
2. 点击后进入合同气瓶新建页,并传递当前合同唯一标识和返回地址。
3. 从列表进入时锁定预填合同,禁止切换到其他合同。
4. 增加入口、状态条件、目标路由和合同锁定的静态契约检查。
## 操作后状态
配送点管理员可从草稿合同所在行直接绑定气瓶;非草稿合同不显示入口。
## 验证结果
- `npm.cmd run type:check`:通过。
- `npm.cmd run resource-pages:check`:通过,详情 17、新建 9、编辑 5。
- `npm.cmd run build`通过2648 个模块完成生产构建。
## 风险评估
复用既有隐藏路由、接口与权限模型,仅增加缺失的前端入口和上下文锁定,不改变合同状态或数据范围。

View File

@@ -0,0 +1,31 @@
# 项目文档:合同气阀候选范围统一 v1.0
## 1. 项目概述
平台总后台、气站后台和配送点后台统一使用合同级智能气阀候选规则。组织端不再按全部服务用户返回设备,避免其他用户的智能气阀进入当前合同候选。
## 2. 核心规则
- 候选请求必须提供 `contract_identity`,缺少合同上下文时返回空列表。
- 智能气阀必须直接归属于合同签约用户。
- 智能气阀记录必须启用,设备生命周期不得为已报废。
- 已在当前合同中有效绑定的设备不得重复进入候选。
- 气站端要求合同用户当前仍属于合同气站。
- 配送点端要求合同用户当前仍属于合同气站及当前配送点。
- 配送点用户转出后隐藏绑定入口,绑定接口继续执行相同的服务端范围校验。
## 3. 页面行为
- 合同气瓶新建页先取得已锁定的合同,再携带 `contract_identity` 加载智能气阀候选。
- 切换合同会清空原智能气阀选择并重新加载。
- 无候选设备时提示管理员由平台总后台先将设备归属变更为合同用户。
## 4. 接口兼容性
继续使用现有 `/product_info` 候选资源和合同气瓶绑定接口,不新增数据库字段。新增的 `user_delivery_service_active` 是合同查询派生字段,用于配送点页面和服务端动作范围判断,不修改合同表结构。
## 5. 验证方法
- 运行平台合同、产品、气站合同及配送点订单逻辑测试。
- 运行气站与配送点资源页面静态契约检查。
- 运行两个组织端的 TypeScript 类型检查和生产构建。

View File

@@ -58,6 +58,8 @@ frontend/delivery_admin/
创建配送订单时以配送合同为父级收货地址只加载合同签约用户地址合同气瓶只加载当前未解绑绑定切换合同清空旧值并丢弃过期响应。候选为空或仍在加载时禁止保存金额区只提供预估最终金额继续由服务端计算。详细规则见《项目文档_配送点订单创建联动_v1.0.md》。 创建配送订单时以配送合同为父级收货地址只加载合同签约用户地址合同气瓶只加载当前未解绑绑定切换合同清空旧值并丢弃过期响应。候选为空或仍在加载时禁止保存金额区只提供预估最终金额继续由服务端计算。详细规则见《项目文档_配送点订单创建联动_v1.0.md》。
配送合同列表仅对草稿合同且签约用户服务关系仍有效的记录显示“绑定气瓶”。入口进入合同气瓶独立新建页,自动带入并锁定当前合同,保存或取消后可返回原合同列表;生效、过期、终止合同不显示该入口。
订单详情的“调整金额”动作读取当前配送费和优惠金额作为弹窗初始值,并完成分到元的展示转换;本次操作原因继续单独填写,不复用历史原因。 订单详情的“调整金额”动作读取当前配送费和优惠金额作为弹窗初始值,并完成分到元的展示转换;本次操作原因继续单独填写,不复用历史原因。
配送人员资质列表必须由配送人员列表进入。页面先通过受保护人员详情接口回查姓名,再以当前气站、配送点、`delivery` 角色和人员 ID 四重条件加载资质;缺少、无效或越权人员上下文时停止加载,绝不回退为全部资质。标题和上下文区域显示人员姓名及可复制唯一标识,表格隐藏重复人员列。新建页自动预填所属人员,创建和编辑均锁定该关系,返回地址只接受安全站内路径。 配送人员资质列表必须由配送人员列表进入。页面先通过受保护人员详情接口回查姓名,再以当前气站、配送点、`delivery` 角色和人员 ID 四重条件加载资质;缺少、无效或越权人员上下文时停止加载,绝不回退为全部资质。标题和上下文区域显示人员姓名及可复制唯一标识,表格隐藏重复人员列。新建页自动预填所属人员,创建和编辑均锁定该关系,返回地址只接受安全站内路径。

View File

@@ -77,6 +77,10 @@ assert(listPage.includes('avatarLoader.reset()'), '列表刷新未清理头像
assert(listPage.includes('field.listRelationNameOnly && fieldValue(field, record)'), '关系列表字段尚未按气站端显示业务名称'); assert(listPage.includes('field.listRelationNameOnly && fieldValue(field, record)'), '关系列表字段尚未按气站端显示业务名称');
assert(listPage.includes('relationListName(field, record)'), '关系列表字段缺少服务端派生名称读取'); assert(listPage.includes('relationListName(field, record)'), '关系列表字段缺少服务端派生名称读取');
assert(listPage.includes('@open="openRelation(field, record)"'), '关系列表名称缺少关联详情入口'); assert(listPage.includes('@open="openRelation(field, record)"'), '关系列表名称缺少关联详情入口');
assert(listPage.includes("Number(record.contract_status) === 0"), '草稿合同缺少绑定气瓶入口状态限制');
assert(listPage.includes('record.user_delivery_service_active !== false'), '合同气瓶入口未校验用户仍属当前配送点');
assert(listPage.includes('@click="bindContractProduct(record)"'), '配送合同列表缺少绑定气瓶入口');
assert(listPage.includes("name: 'contract-products-create'"), '绑定气瓶入口未进入合同气瓶新建页');
const avatarLoader = read('src/views/shared/protected-list-avatar-loader.ts'); const avatarLoader = read('src/views/shared/protected-list-avatar-loader.ts');
assert(avatarLoader.includes('MAX_CONCURRENT_REQUESTS = 6'), '头像请求并发上限未与 5173 对齐'); assert(avatarLoader.includes('MAX_CONCURRENT_REQUESTS = 6'), '头像请求并发上限未与 5173 对齐');
assert(avatarLoader.includes("new Set(['staff_account', 'user_account'])"), '头像列表资源白名单不正确'); assert(avatarLoader.includes("new Set(['staff_account', 'user_account'])"), '头像列表资源白名单不正确');
@@ -97,6 +101,8 @@ assert(!listPage.includes('placeholder="关键字段模糊搜索"'), '列表仍
assert(listPage.includes('ensureCredentialContext'), '人员资质列表缺少服务端人员上下文校验'); assert(listPage.includes('ensureCredentialContext'), '人员资质列表缺少服务端人员上下文校验');
assert(listPage.includes("field.key === 'staff_account_identity'"), '人员范围资质列表未隐藏重复人员列'); assert(listPage.includes("field.key === 'staff_account_identity'"), '人员范围资质列表未隐藏重复人员列');
assert(recordPage.includes('已锁定,不可更换'), '人员资质独立页未锁定所属配送人员'); assert(recordPage.includes('已锁定,不可更换'), '人员资质独立页未锁定所属配送人员');
assert(recordPage.includes("definition.value.name === 'gasorder_contract_product'"), '合同气瓶新建页未识别合同预填上下文');
assert(recordPage.includes("field.key === 'gasorder_contract_identity'"), '预填合同未在合同气瓶新建页锁定');
const attachmentApi = read('src/api/contract-attachment.ts'); const attachmentApi = read('src/api/contract-attachment.ts');
const attachmentState = read('src/views/resource/use-contract-attachment.ts'); const attachmentState = read('src/views/resource/use-contract-attachment.ts');
const detailContent = read('src/views/resource/ResourceDetailContent.vue'); const detailContent = read('src/views/resource/ResourceDetailContent.vue');
@@ -124,15 +130,16 @@ assert(detailContent.includes("key.endsWith('_display_name')"), '详情页仍可
assert(detailContent.includes('hasDetailFieldLabel'), '详情页未启用中文字段白名单'); assert(detailContent.includes('hasDetailFieldLabel'), '详情页未启用中文字段白名单');
assert(detailContent.includes('查看参数'), '关联记录缺少完整 JSON 查看入口'); assert(detailContent.includes('查看参数'), '关联记录缺少完整 JSON 查看入口');
assert(detailContent.includes('智能气阀名称取当前设备资料'), '合同气瓶名称与快照语义未说明'); assert(detailContent.includes('智能气阀名称取当前设备资料'), '合同气瓶名称与快照语义未说明');
assert(resourceDisplay.includes("user_service_active: '签约用户仍属合同气站'"), '签约服务关系缺少中文业务标签'); assert(resourceDisplay.includes("user_delivery_service_active: '签约用户仍属当前配送点'"), '当前配送点服务关系缺少中文业务标签');
assert(resourceDisplay.includes("activate: '启用合同'"), '合同修订动作未中文化'); assert(resourceDisplay.includes("activate: '启用合同'"), '合同修订动作未中文化');
assert(recordPage.includes("record.value.user_service_active === false"), '失效服务关系下未限制合同动作'); assert(recordPage.includes("record.value.user_delivery_service_active === false"), '用户转出当前配送点后未限制合同动作');
assert(definitions.includes("relationFilters: { candidate: 'order' }"), '订单合同候选未限定可履约范围'); assert(definitions.includes("relationFilters: { candidate: 'order' }"), '订单合同候选未限定可履约范围');
assert(definitions.includes("f('request_no', { required: true })"), '请求流水号未按 5173 保持人工必填'); assert(definitions.includes("f('request_no', { required: true })"), '请求流水号未按 5173 保持人工必填');
assert(definitions.includes("f('contact_name', { required: true })"), '订单联系人未保持独立必填'); assert(definitions.includes("f('contact_name', { required: true })"), '订单联系人未保持独立必填');
assert(definitions.includes("f('contact_phone', { required: true })"), '订单联系电话未保持独立必填'); assert(definitions.includes("f('contact_phone', { required: true })"), '订单联系电话未保持独立必填');
assert(definitions.includes("filterKey: 'gasorder_contract_identity'"), '收货地址未按配送合同联动'); assert(definitions.includes("filterKey: 'gasorder_contract_identity'"), '收货地址未按配送合同联动');
assert(definitions.includes("filterKey: 'contract_identity'"), '合同气瓶未按配送合同联动'); assert(definitions.includes("filterKey: 'contract_identity'"), '合同气瓶未按配送合同联动');
assert(definitions.includes('该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户'), '合同气阀空状态缺少平台归属引导');
assert(definitions.includes("relationOptionLabelKey: 'address'"), '收货地址候选仍可能显示裸唯一标识'); assert(definitions.includes("relationOptionLabelKey: 'address'"), '收货地址候选仍可能显示裸唯一标识');
assert(definitions.includes("relationOptionDisplay: 'product-name-type'"), '合同气瓶缺少名称、编码和类型组合展示'); assert(definitions.includes("relationOptionDisplay: 'product-name-type'"), '合同气瓶缺少名称、编码和类型组合展示');
assert(relationLinkage.includes('const versions = new Map'), '关系联动缺少过期响应版本守卫'); assert(relationLinkage.includes('const versions = new Map'), '关系联动缺少过期响应版本守卫');

View File

@@ -22,7 +22,7 @@ const contracts: Record<string, ResourceDetailContract> = {
'contract_status', 'contract_status',
'title', 'title',
'user_account_identity', 'user_account_identity',
'user_service_active', 'user_delivery_service_active',
'gas_basic_identity', 'gas_basic_identity',
'delivery_basic_identity', 'delivery_basic_identity',
'default_delivery_fee', 'default_delivery_fee',

View File

@@ -1,6 +1,6 @@
/** /**
* 功能描述:统一配送点资源独立页面的字段名称和值展示。 * 功能描述:统一配送点资源独立页面的字段名称和值展示。
* 版本v1.1.0。 * 版本v1.2.0。
*/ */
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { ResourceRow } from './resource-record-form'; import type { ResourceRow } from './resource-record-form';
@@ -26,6 +26,7 @@ const aliases: Record<string, string> = {
contract: '合同', contract: '合同',
order: '订单', order: '订单',
user_service_active: '签约用户仍属合同气站', user_service_active: '签约用户仍属合同气站',
user_delivery_service_active: '签约用户仍属当前配送点',
user_account_display_name: '用户账户', user_account_display_name: '用户账户',
gas_basic_display_name: '气站', gas_basic_display_name: '气站',
delivery_basic_display_name: '配送点', delivery_basic_display_name: '配送点',

View File

@@ -421,7 +421,7 @@ const platformResources: ResourceUiDefinition[] = [
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }), ], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [ define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true, { label: '智能气阀', placeholder: '请选择该合同用户可绑定的智能气阀', relationEmptyText: '该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户', relationLinkage: { parentKey: 'gasorder_contract_identity', filterKey: 'contract_identity', requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的智能气阀' } }), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]), ]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []), define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
@@ -514,7 +514,7 @@ const gasOverrides: ResourceUiDefinition[] = [
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }), ], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [ define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true, { label: '智能气阀', placeholder: '请选择该合同用户可绑定的智能气阀', relationEmptyText: '该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户', relationLinkage: { parentKey: 'gasorder_contract_identity', filterKey: 'contract_identity', requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的智能气阀' } }), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]), ]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []), define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
@@ -553,7 +553,7 @@ const deliveryOverrides: ResourceUiDefinition[] = [
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }), ], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [ define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true, { label: '智能气阀', placeholder: '请选择该合同用户可绑定的智能气阀', relationEmptyText: '该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户', relationLinkage: { parentKey: 'gasorder_contract_identity', filterKey: 'contract_identity', requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的智能气阀' } }), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]), ]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []), define('gasorder_contract_revision', '合同修订记录', 'readonly', []),

View File

@@ -20,7 +20,7 @@
:color="contractStatusColor(Number(entry.rawValue))" :color="contractStatusColor(Number(entry.rawValue))"
>{{ entry.value }}</a-tag> >{{ entry.value }}</a-tag>
<a-tag <a-tag
v-else-if="entry.key === 'user_service_active'" v-else-if="['user_service_active', 'user_delivery_service_active'].includes(entry.key)"
class="detail-status-tag" class="detail-status-tag"
:color="entry.rawValue === true ? 'green' : 'red'" :color="entry.rawValue === true ? 'green' : 'red'"
>{{ entry.value }}</a-tag> >{{ entry.value }}</a-tag>

View File

@@ -1,4 +1,4 @@
<!-- 功能描述承载配送点标准资源的新建详情和编辑独立页面版本v1.1.0 --> <!-- 功能描述承载配送点标准资源的新建详情和编辑独立页面版本v1.2.0 -->
<template> <template>
<div class="record-page"> <div class="record-page">
<header class="page-header"> <header class="page-header">
@@ -218,7 +218,16 @@ const summaryRecord = computed(() => (mode.value === 'detail' ? record.value : {
const formFields = computed(() => const formFields = computed(() =>
recordFormFields(definition.value.name, definition.value.fields, mode.value as 'create' | 'edit') recordFormFields(definition.value.name, definition.value.fields, mode.value as 'create' | 'edit')
.filter((field) => field.key !== 'avatar') .filter((field) => field.key !== 'avatar')
.map((field) => (isCredentialResource.value && field.key === 'staff_account_identity' ? { ...field, readonly: true } : field)), .map((field) => {
const lockedCredentialOwner = isCredentialResource.value && field.key === 'staff_account_identity';
const lockedContract =
mode.value === 'create' &&
definition.value.name === 'gasorder_contract_product' &&
field.key === 'gasorder_contract_identity' &&
route.query.relation_key === 'gasorder_contract_identity' &&
Boolean(route.query.owner_identity);
return lockedCredentialOwner || lockedContract ? { ...field, readonly: true } : field;
}),
); );
const relationBusy = computed(() => relations.isLoading(formFields.value)); const relationBusy = computed(() => relations.isLoading(formFields.value));
const relationUnavailableMessage = computed(() => relations.unavailableMessage(formFields.value)); const relationUnavailableMessage = computed(() => relations.unavailableMessage(formFields.value));
@@ -226,7 +235,7 @@ const canEditRecord = computed(() => definition.value.canEdit && !recordEditReas
const visibleActions = computed(() => const visibleActions = computed(() =>
(definition.value.detailActions ?? []).filter( (definition.value.detailActions ?? []).filter(
(action) => (action) =>
!(definition.value.name === 'gasorder_contract' && record.value.user_service_active === false && action.name !== '终止合同') && !(definition.value.name === 'gasorder_contract' && record.value.user_delivery_service_active === false && action.name !== '终止合同') &&
(!action.visibleFor || action.visibleFor.values.includes(record.value[action.visibleFor.field] as string | number)), (!action.visibleFor || action.visibleFor.values.includes(record.value[action.visibleFor.field] as string | number)),
), ),
); );

View File

@@ -1,4 +1,4 @@
<!-- 功能描述展示配送点标准资源列表并导航到独立记录页版本v1.3.0 --> <!-- 功能描述展示配送点标准资源列表并导航到独立记录页版本v1.4.0 -->
<template> <template>
<a-card :title="listTitle" :bordered="false"> <a-card :title="listTitle" :bordered="false">
<template v-if="isCredentialList" #extra> <template v-if="isCredentialList" #extra>
@@ -104,11 +104,17 @@
<a-table-column title="系统唯一标识" :width="170"> <a-table-column title="系统唯一标识" :width="170">
<template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template> <template #cell="{ record }"><IdentityText :value="String(record.identity)" /></template>
</a-table-column> </a-table-column>
<a-table-column title="操作" :width="220" fixed="right"> <a-table-column title="操作" :width="definition.name === 'gasorder_contract' ? 285 : 220" fixed="right">
<template #cell="{ record }"> <template #cell="{ record }">
<a-space> <a-space>
<a-button v-if="definition.name === 'staff_account'" size="mini" @click="viewCredentials(record)">查看资质</a-button> <a-button v-if="definition.name === 'staff_account'" size="mini" @click="viewCredentials(record)">查看资质</a-button>
<a-button size="mini" @click="openRecord('detail', record)">详情</a-button> <a-button size="mini" @click="openRecord('detail', record)">详情</a-button>
<a-button
v-if="definition.name === 'gasorder_contract' && Number(record.contract_status) === 0 && record.user_delivery_service_active !== false"
size="mini"
type="primary"
@click="bindContractProduct(record)"
>绑定气瓶</a-button>
<a-tooltip v-if="definition.canEdit" :content="recordEditReason(definition, record) || '编辑记录'"> <a-tooltip v-if="definition.canEdit" :content="recordEditReason(definition, record) || '编辑记录'">
<span> <span>
<a-button <a-button
@@ -361,6 +367,18 @@ function openRecord(mode: 'detail' | 'edit', row: ResourceRow) {
); );
} }
/** 从草稿合同列表进入合同气瓶新建页,并预填当前合同。 */
function bindContractProduct(row: ResourceRow) {
return router.push({
name: 'contract-products-create',
query: {
relation_key: 'gasorder_contract_identity',
owner_identity: String(row.identity ?? ''),
return_to: route.fullPath,
},
});
}
/** 进入当前配送人员的资质列表并保留来源。 */ /** 进入当前配送人员的资质列表并保留来源。 */
function viewCredentials(row: ResourceRow) { function viewCredentials(row: ResourceRow) {
return router.push({ return router.push({

View File

@@ -111,6 +111,12 @@ if (
) { ) {
fail('签约用户转出后未限制合同编辑和业务动作'); fail('签约用户转出后未限制合同编辑和业务动作');
} }
if (
!resourceSource.includes("filterKey: 'contract_identity'") ||
!resourceSource.includes('该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户')
) {
fail('合同气阀候选未按合同联动或缺少平台归属引导');
}
if ( if (
!ruleSource.includes('row.contract_readonly === true') || !ruleSource.includes('row.contract_readonly === true') ||
!recordSource.includes('record.value.contract_readonly === true') || !recordSource.includes('record.value.contract_readonly === true') ||

View File

@@ -557,7 +557,7 @@ const platformResources: ResourceUiDefinition[] = [
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }), ], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [ define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true, { label: '智能气阀', placeholder: '请选择该合同用户可绑定的智能气阀', relationEmptyText: '该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的智能气阀' } }), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]), ]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []), define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
@@ -650,7 +650,7 @@ const gasOverrides: ResourceUiDefinition[] = [
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } }, { name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } }, { name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }), ], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [ define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true, { label: '智能气阀', placeholder: '请选择该合同用户可绑定的智能气阀', relationEmptyText: '该合同用户暂无可绑定的智能气阀,请先由平台总后台将设备归属变更为该用户', relationLinkage: { parentKey: 'gasorder_contract_identity', optionParentKey: '', filterKey: 'contract_identity', filterOnly: true, requiresParent: true, parentChangeMessage: '配送合同已变更,请重新选择该合同用户的智能气阀' } }), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason }, { name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]), ]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []), define('gasorder_contract_revision', '合同修订记录', 'readonly', []),