修复气站合同用户权限与服务关系边界

This commit is contained in:
czl231
2026-08-18 22:49:14 +08:00
parent 61a61f916b
commit 9a31cadf5f
27 changed files with 635 additions and 59 deletions

View File

@@ -1,3 +1,5 @@
// 功能描述:实现气站范围内的配送合同、合同气瓶和燃气配送订单接口。
// 版本v1.1.0
package gas
import (
@@ -5,6 +7,8 @@ import (
"encoding/json"
"io"
"reflect"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
@@ -59,8 +63,26 @@ func ListGasorderContract(ctx *gin.Context) {
if !ok {
return
}
listScoped(ctx, &models.GasorderContract{}, common.ActiveRecords(impl.DBService.Model(&models.GasorderContract{})).
Where("gas_basic_id = ?", station.ID), "gasorder_contract.created_at desc")
page, size := common.PageSize(ctx)
query := common.ApplyKeywordFilter(ctx,
platformgasorder.ContractPartyDisplayQuery(impl.DBService).
Where("gasorder_contract.gas_basic_id = ?", station.ID),
&models.GasorderContract{})
candidate := strings.TrimSpace(ctx.Query("candidate"))
if candidate == "order" || candidate == "binding" {
query = platformgasorder.FilterGasorderContractCandidates(query, candidate, time.Now())
}
var total int64
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
var list []platformgasorder.ContractPartyDisplay
if err := query.Order("gasorder_contract.created_at desc").Offset((page - 1) * size).Limit(size).Scan(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
respondList(ctx, list, total)
}
func GetGasorderContract(ctx *gin.Context) {

View File

@@ -58,22 +58,56 @@ func requireStaff(ctx *gin.Context, identity string, gasID uint64) (models.Staff
}
func requireUser(ctx *gin.Context, identity string, gasID uint64) (models.UserAccount, models.UserServiceRelation, bool) {
user, relation, err := findCurrentGasUser(impl.DBService, identity, gasID)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return models.UserAccount{}, models.UserServiceRelation{}, false
}
return user, relation, true
}
// findCurrentGasUser 查询当前气站有效服务关系内的用户,不直接写入 HTTP 响应,供只读合同兜底复用。
func findCurrentGasUser(databaseService *gorm.DB, identity string, gasID uint64) (models.UserAccount, models.UserServiceRelation, error) {
var user models.UserAccount
var relation models.UserServiceRelation
err := common.ActiveRecords(impl.DBService.Model(&models.UserAccount{})).
err := common.ActiveRecords(databaseService.Model(&models.UserAccount{})).
Select("user_account.*").
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_account.identity = ? AND user_service_relation.gas_basic_id = ?", identity, gasID).
First(&user).Error
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return models.UserAccount{}, models.UserServiceRelation{}, false
return models.UserAccount{}, models.UserServiceRelation{}, err
}
if err := common.ActiveRecords(impl.DBService).
if err := common.ActiveRecords(databaseService).
Where("user_account_id = ? AND gas_basic_id = ?", user.ID, gasID).First(&relation).Error; err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return models.UserAccount{}, models.UserServiceRelation{}, false
return models.UserAccount{}, models.UserServiceRelation{}, err
}
return user, relation, true
return user, relation, nil
}
// contractScopedUserRecord 带出合同保存的默认配送点,供无服务关系用户只读展示本站合同上下文。
type contractScopedUserRecord struct {
models.UserAccount
ContractDeliveryBasicID uint64 `gorm:"column:contract_delivery_basic_id"`
}
// findContractScopedUser 仅查询当前气站合同中实际引用的用户,禁止借详情接口枚举全局用户。
func findContractScopedUser(databaseService *gorm.DB, identity string, gasID uint64, contractIdentity string) (contractScopedUserRecord, error) {
var record contractScopedUserRecord
err := contractScopedUserQuery(databaseService, identity, gasID, contractIdentity).
First(&record).Error
return record, err
}
// contractScopedUserQuery 统一合同用户只读详情与头像的气站范围查询条件。
func contractScopedUserQuery(databaseService *gorm.DB, identity string, gasID uint64, contractIdentity string) *gorm.DB {
query := common.ActiveRecords(databaseService.Model(&models.UserAccount{})).
Select("user_account.*, gasorder_contract.delivery_basic_id AS contract_delivery_basic_id").
Joins("JOIN gasorder_contract ON gasorder_contract.user_account_id = user_account.id AND gasorder_contract.status <> ?", common.StatusArchived).
Where("user_account.identity = ? AND gasorder_contract.gas_basic_id = ?", identity, gasID)
if contractIdentity != "" {
query = query.Where("gasorder_contract.identity = ?", contractIdentity)
}
return query.Order("gasorder_contract.created_at DESC")
}

View File

@@ -17,6 +17,12 @@ type gasUserListItem struct {
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id" json:"delivery_basic_id"`
}
// gasUserDetail 标识用户是否仅因当前气站合同而可见,前端据此进入只读模式。
type gasUserDetail struct {
models.UserAccount
ContractReadonly bool `json:"contract_readonly"`
}
// scopedUserList 将用户列表与当前气站唯一有效服务关系绑定,避免返回跨站归属。
func scopedUserList(databaseService *gorm.DB, gasBasicID uint64) *gorm.DB {
return common.ActiveRecords(databaseService.Model(&models.UserAccount{})).
@@ -52,8 +58,35 @@ func GetUser(ctx *gin.Context) {
if !ok {
return
}
user, relation, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
if !ok {
user, relation, err := findCurrentGasUser(impl.DBService, ctx.Param("identity"), station.ID)
contractReadonly := false
var contractDeliveryBasicID uint64
if err != nil {
contractUser, contractErr := findContractScopedUser(
impl.DBService, ctx.Param("identity"), station.ID, ctx.Query("contract_identity"),
)
err = contractErr
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user = contractUser.UserAccount
contractReadonly = true
contractDeliveryBasicID = contractUser.ContractDeliveryBasicID
}
if contractReadonly {
// 合同范围只返回用户主档,不泄露地址、钱包或其他服务关系上下文。
detail := gasUserDetail{UserAccount: user, ContractReadonly: true}
response, responseErr := common.PublicResourceResponse(gin.H{
"user": detail,
"relation": gin.H{"delivery_basic_id": contractDeliveryBasicID},
"addresses": []models.UserAddress{},
})
if responseErr != nil {
infra.Response.Error(ctx, responseErr)
return
}
infra.Response.Success(ctx, response)
return
}
var addresses []models.UserAddress
@@ -69,15 +102,21 @@ func GetUser(ctx *gin.Context) {
infra.Response.Success(ctx, response)
}
// GetUserAvatar 返回与当前气站存在服务关系的用户受保护头像。
// GetUserAvatar 返回与当前气站存在服务关系或合同引用关系的用户受保护头像。
func GetUserAvatar(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
user, _, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
if !ok {
return
user, _, err := findCurrentGasUser(impl.DBService, ctx.Param("identity"), station.ID)
if err != nil {
contractUser, contractErr := findContractScopedUser(impl.DBService, ctx.Param("identity"), station.ID, "")
err = contractErr
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user = contractUser.UserAccount
}
upload.ServeAvatar(ctx, user.Avatar)
}

View File

@@ -30,3 +30,28 @@ func TestScopedUserListIncludesDeliveryRelation(t *testing.T) {
t.Fatalf("user list must keep gas scope, got: %s", statement)
}
}
// TestContractScopedUserRequiresCurrentGasContract 验证只读用户详情同时受用户标识与当前气站合同约束。
func TestContractScopedUserRequiresCurrentGasContract(t *testing.T) {
connection, _, err := sqlmock.New()
if err != nil {
t.Fatalf("create SQL mock: %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("open GORM database: %v", err)
}
statement := contractScopedUserQuery(databaseService, "user-identity", 42, "contract-identity").
Find(&[]contractScopedUserRecord{}).Statement.SQL.String()
if !strings.Contains(statement, "JOIN gasorder_contract ON gasorder_contract.user_account_id = user_account.id") {
t.Fatalf("contract-scoped detail must join contract, got: %s", statement)
}
if !strings.Contains(statement, "user_account.identity =") || !strings.Contains(statement, "gasorder_contract.gas_basic_id =") {
t.Fatalf("contract-scoped detail must keep user and gas scope, got: %s", statement)
}
if !strings.Contains(statement, "gasorder_contract.identity =") || !strings.Contains(statement, "contract_delivery_basic_id") {
t.Fatalf("contract-scoped detail must keep contract context and delivery point, got: %s", statement)
}
}