修复气站合同用户权限与服务关系边界
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user