refactor platform menus and entity statuses

This commit is contained in:
david
2026-07-29 13:18:52 +08:00
parent 978dc446d9
commit afd9dbdeb5
48 changed files with 510 additions and 421 deletions

View File

@@ -40,7 +40,7 @@ func FilterFields(values map[string]any, allowedFields []string) gin.H {
// UpdateRecordStatus 更新主表状态,停用和归档均保留历史记录。
func UpdateRecordStatus(ctx *gin.Context, model any) {
var request struct {
Status string `json:"status" binding:"required,max=32"`
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -51,10 +51,10 @@ func UpdateRecordStatus(ctx *gin.Context, model any) {
// ArchiveRecord 通过 archived 状态实现逻辑删除,不物理删除主数据。
func ArchiveRecord(ctx *gin.Context, model any) {
UpdateAllowedByIdentity(ctx, model, gin.H{"status": "archived"}, []string{"status"})
UpdateAllowedByIdentity(ctx, model, gin.H{"status": StatusArchived}, []string{"status"})
}
func NewEntity(status string) models.Entity {
func NewEntity(status int) models.Entity {
return models.Entity{Identity: models.NewIdentity(), Status: status, Version: 1}
}

View File

@@ -4,8 +4,6 @@ import (
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
)
@@ -18,24 +16,3 @@ func RequirePlatformRoot(ctx *gin.Context) bool {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return false
}
// LoadPlatformMenus returns the menus granted to one platform role.
func LoadPlatformMenus(roleCode string) ([]models.PlatformMenu, error) {
var menus []models.PlatformMenu
if roleCode == "root" {
err := impl.DBService.Order("sort_no asc, id asc").Find(&menus).Error
return menus, err
}
var role models.PlatformRole
if err := impl.DBService.Where("role_code = ? AND status = ?", roleCode, "enabled").First(&role).Error; err != nil {
return nil, err
}
err := impl.DBService.
Select("platform_menu.*").
Joins("JOIN platform_role_menu ON platform_role_menu.platform_menu_id = platform_menu.id").
Where("platform_role_menu.platform_role_id = ? AND platform_menu.status = ?", role.ID, "enabled").
Order("sort_no asc, id asc").
Find(&menus).Error
return menus, err
}

View File

@@ -84,7 +84,7 @@ func createResource(ctx *gin.Context, model any, allowedFields []string, relatio
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data.Elem().FieldByName("Entity").Set(reflect.ValueOf(NewEntity("draft")))
data.Elem().FieldByName("Entity").Set(reflect.ValueOf(NewEntity(StatusDraft)))
if err := impl.DBService.Create(data.Interface()).Error; err != nil {
infra.Response.Error(ctx, err)
return
@@ -347,7 +347,6 @@ var relationIdentityModels = map[string]any{
"gasorder_basic_id": &models.GasorderBasic{},
"gasorder_track_id": &models.GasorderTrack{},
"platform_role_id": &models.PlatformRole{},
"platform_menu_id": &models.PlatformMenu{},
"wallet_basic_id": &models.WalletBasic{},
"wallet_payment_id": &models.WalletPayment{},
"wallet_bank_id": &models.WalletBank{},

View File

@@ -0,0 +1,55 @@
package common
// 公共实体状态。所有嵌入 models.Entity 的模型统一使用这些整数值。
const (
StatusDraft = 0 // 草稿
StatusEnable = 1 // 启用
StatusDisable = 2 // 停用
StatusArchived = 3 // 已归档
StatusFrozen = 4 // 已冻结
StatusPending = 10 // 待处理
StatusActive = 11 // 生效中
StatusExpired = 12 // 已过期
StatusTerminated = 13 // 已终止
StatusRecorded = 14 // 已记录
StatusBound = 15 // 已绑定
StatusCreated = 16 // 已创建
StatusOrdered = 17 // 已下单
StatusAssigned = 18 // 已分配
StatusFilling = 19 // 充装中
StatusReady = 20 // 已就绪
StatusException = 21 // 异常
StatusCancelled = 22 // 已取消
StatusCompleted = 23 // 已完成
StatusPosted = 24 // 已入账
StatusApproved = 25 // 已通过
StatusRejected = 26 // 已驳回
StatusScrapped = 27 // 已报废
StatusInStock = 28 // 在库
StatusInTransit = 29 // 运输中
StatusInUse = 30 // 使用中
StatusRepairing = 31 // 维修中
StatusOpen = 32 // 待受理
StatusDelivering = 33 // 配送中
StatusAwaitingConfirmation = 34 // 待确认
StatusPaid = 35 // 已支付
StatusPublished = 36 // 已发布
StatusSuccess = 37 // 成功
StatusMatched = 38 // 已匹配
)
var statusNames = map[int]string{
StatusDraft: "draft", StatusEnable: "enabled", StatusDisable: "disabled", StatusArchived: "archived", StatusFrozen: "frozen",
StatusPending: "pending", StatusActive: "active", StatusExpired: "expired", StatusTerminated: "terminated",
StatusRecorded: "recorded", StatusBound: "bound", StatusCreated: "created", StatusOrdered: "ordered",
StatusAssigned: "assigned", StatusFilling: "filling", StatusReady: "ready", StatusException: "exception",
StatusCancelled: "cancelled", StatusCompleted: "completed", StatusPosted: "posted", StatusApproved: "approved",
StatusRejected: "rejected", StatusScrapped: "scrapped", StatusInStock: "in_stock", StatusInTransit: "in_transit",
StatusInUse: "in_use", StatusRepairing: "repairing", StatusOpen: "open", StatusDelivering: "delivering",
StatusAwaitingConfirmation: "awaiting_confirmation",
StatusPaid: "paid", StatusPublished: "published", StatusSuccess: "success", StatusMatched: "matched",
}
// StatusName 返回状态整数对应的稳定英文名称,供审计快照字段使用。
func StatusName(status int) string { return statusNames[status] }

View File

@@ -49,7 +49,7 @@ func Login(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
if account.Status != "enabled" {
if account.Status != common.StatusEnable {
infra.Response.Error(ctx, errcode.ErrAccountDisabled)
return
}
@@ -91,7 +91,7 @@ func CurrentProfile(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return
}
menus, err := common.LoadPlatformMenus(account.PlatformRoleCode)
menus, err := LoadPlatformMenus(account.PlatformRoleCode)
if err != nil {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
return

View File

@@ -42,7 +42,7 @@ func CreateDeliveryAccount(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
account := models.DeliveryAccount{Entity: common.NewEntity("enabled"), DeliveryBasicID: deliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
account := models.DeliveryAccount{Entity: common.NewEntity(common.StatusEnable), DeliveryBasicID: deliveryBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
if err := impl.DBService.Create(&account).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -33,7 +33,7 @@ func CreateDeliveryBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
delivery := models.DeliveryBasic{Entity: common.NewEntity("draft"), DeliveryCode: request.DeliveryCode, GasBasicID: gasBasicID, Name: request.Name, Principal: request.Principal, Address: request.Address}
delivery := models.DeliveryBasic{Entity: common.NewEntity(common.StatusDraft), DeliveryCode: request.DeliveryCode, GasBasicID: gasBasicID, Name: request.Name, Principal: request.Principal, Address: request.Address}
if err := impl.DBService.Create(&delivery).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -13,7 +13,7 @@ type ecCategoryView struct {
ParentIdentity string `json:"parent_identity,omitempty"`
Name string `json:"name"`
SortNo int `json:"sort_no"`
Status string `json:"status"`
Status int `json:"status"`
}
func ecCategoryViews(list []models.EcCategory) ([]ecCategoryView, error) {

View File

@@ -44,7 +44,7 @@ func CreateGasAccount(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
account := models.GasAccount{Entity: common.NewEntity("enabled"), GasBasicID: gasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
account := models.GasAccount{Entity: common.NewEntity(common.StatusEnable), GasBasicID: gasBasicID, Username: request.Username, DisplayName: request.DisplayName, PasswordHash: hash, RoleCode: request.RoleCode}
if err := impl.DBService.Create(&account).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -22,7 +22,7 @@ func CreateGasBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
request.Entity = common.NewEntity("draft")
request.Entity = common.NewEntity(common.StatusDraft)
if err := impl.DBService.Create(&request).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -148,7 +148,7 @@ func CreateGasorderContract(ctx *gin.Context) {
return
}
contract := models.GasorderContract{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "draft", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusDraft, Version: 1},
ContractNo: request.ContractNo, UserAccountID: userID, GasBasicID: gasID, DeliveryBasicID: deliveryID,
Title: request.Title, Terms: request.Terms, FileURI: request.FileURI, DefaultDeliveryFee: request.DefaultDeliveryFee,
SignedAt: request.SignedAt, EffectiveAt: request.EffectiveAt, ExpiredAt: request.ExpiredAt,
@@ -182,7 +182,7 @@ func UpdateGasorderContract(ctx *gin.Context) {
return
}
result := impl.DBService.Model(&models.GasorderContract{}).
Where("identity = ? AND status = ?", ctx.Param("identity"), "draft").
Where("identity = ? AND status = ?", ctx.Param("identity"), common.StatusDraft).
Updates(map[string]any{"delivery_basic_id": deliveryID, "title": request.Title, "terms": request.Terms,
"file_uri": request.FileURI, "default_delivery_fee": request.DefaultDeliveryFee,
"signed_at": request.SignedAt, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt})
@@ -194,11 +194,11 @@ func UpdateGasorderContract(ctx *gin.Context) {
}
func ActivateGasorderContract(ctx *gin.Context) {
changeGasorderContract(ctx, "activate", "active")
changeGasorderContract(ctx, "activate", common.StatusActive)
}
func TerminateGasorderContract(ctx *gin.Context) {
changeGasorderContract(ctx, "terminate", "terminated")
changeGasorderContract(ctx, "terminate", common.StatusTerminated)
}
func RenewGasorderContract(ctx *gin.Context) {
@@ -218,13 +218,13 @@ func RenewGasorderContract(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
return err
}
if contract.Status != "active" && contract.Status != "expired" && contract.Status != "terminated" {
if contract.Status != common.StatusActive && contract.Status != common.StatusExpired && contract.Status != common.StatusTerminated {
return errors.New("contract cannot be renewed")
}
if err := tx.Model(&contract).Updates(map[string]any{"status": "active", "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
if err := tx.Model(&contract).Updates(map[string]any{"status": common.StatusActive, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
return err
}
contract.Status, contract.EffectiveAt, contract.ExpiredAt = "active", request.EffectiveAt, request.ExpiredAt
contract.Status, contract.EffectiveAt, contract.ExpiredAt = common.StatusActive, request.EffectiveAt, request.ExpiredAt
return tx.Create(contractRevision(contract, "renew", request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
@@ -234,7 +234,7 @@ func RenewGasorderContract(ctx *gin.Context) {
infra.Response.Success(ctx, gin.H{"updated": true})
}
func changeGasorderContract(ctx *gin.Context, action, target string) {
func changeGasorderContract(ctx *gin.Context, action string, target int) {
var request struct {
Reason string `json:"reason" binding:"required"`
}
@@ -248,13 +248,13 @@ func changeGasorderContract(ctx *gin.Context, action, target string) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
return err
}
if action == "activate" && contract.Status != "draft" && contract.Status != "terminated" {
if action == "activate" && contract.Status != common.StatusDraft && contract.Status != common.StatusTerminated {
return errors.New("contract cannot be activated")
}
if action == "terminate" && contract.Status != "active" {
if action == "terminate" && contract.Status != common.StatusActive {
return errors.New("contract cannot be terminated")
}
if target == "active" {
if target == common.StatusActive {
now := time.Now()
if contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
return errors.New("contract outside effective period")
@@ -280,7 +280,7 @@ func changeGasorderContract(ctx *gin.Context, action, target string) {
func contractRevision(contract models.GasorderContract, action, reason, operatorIdentity, operatorName string) *models.GasorderContractRevision {
return &models.GasorderContractRevision{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded, Version: 1},
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.Status,
EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
@@ -302,13 +302,13 @@ func BindGasorderContractProduct(ctx *gin.Context) {
common.RespondRecordError(ctx, err)
return
}
if contract.Status != "draft" && contract.Status != "active" {
if contract.Status != common.StatusDraft && contract.Status != common.StatusActive {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var product models.ProductInfo
if err := impl.DBService.Where("identity = ?", request.ProductIdentity).First(&product).Error; err != nil ||
!product.IsEnabled || product.Status == "scrapped" || product.UserAccountID != contract.UserAccountID {
!product.IsEnabled || product.Status == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -318,7 +318,7 @@ func BindGasorderContractProduct(ctx *gin.Context) {
return
}
binding := models.GasorderContractProduct{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "bound", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusBound, Version: 1},
GasorderContractID: contract.ID, ProductInfoID: product.ID, ProductCode: product.Code,
ProductTypeName: productType.Name, ProductParams: product.Params, UnitPrice: request.UnitPrice, BoundAt: time.Now(),
}
@@ -376,7 +376,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
return err
}
now := time.Now()
if contract.Status != "active" || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
if contract.Status != common.StatusActive || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
return errors.New("contract is not active")
}
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
@@ -401,13 +401,13 @@ func CreateGasorderBasic(ctx *gin.Context) {
for _, binding := range bindings {
var product models.ProductInfo
if err := tx.Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
!product.IsEnabled || product.Status == "scrapped" || product.UserAccountID != contract.UserAccountID {
!product.IsEnabled || product.Status == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
return errors.New("contract product is no longer eligible")
}
var activeOrderCount int64
if err := tx.Table("gasorder_item").
Joins("JOIN gasorder_basic ON gasorder_basic.id = gasorder_item.gasorder_basic_id").
Where("gasorder_item.product_info_id = ? AND gasorder_basic.status NOT IN ?", binding.ProductInfoID, []string{"completed", "cancelled"}).
Where("gasorder_item.product_info_id = ? AND gasorder_basic.status NOT IN ?", binding.ProductInfoID, []int{common.StatusCompleted, common.StatusCancelled}).
Count(&activeOrderCount).Error; err != nil || activeOrderCount != 0 {
return errors.New("contract product already has an active order")
}
@@ -418,7 +418,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
return errors.New("invalid payable amount")
}
order = models.GasorderBasic{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "created", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusCreated, Version: 1},
OrderNo: models.NewIdentity(), RequestNo: request.RequestNo, GasorderContractID: contract.ID,
UserAccountID: contract.UserAccountID, CreatorType: request.CreatorType, CreatorID: creatorID,
CreatorIdentity: request.CreatorIdentity, GasBasicID: contract.GasBasicID, DeliveryBasicID: contract.DeliveryBasicID,
@@ -433,7 +433,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
}
for _, binding := range bindings {
item := models.GasorderItem{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "ordered", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusOrdered, Version: 1},
GasorderBasicID: order.ID, GasorderContractProductID: binding.ID, ProductInfoID: binding.ProductInfoID,
ProductCode: binding.ProductCode, ProductTypeName: binding.ProductTypeName,
ProductParams: binding.ProductParams, UnitPrice: binding.UnitPrice,
@@ -442,7 +442,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
return err
}
}
return tx.Create(gasorderStatusRecord(order.ID, "", "created", "order created", operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, common.StatusDraft, common.StatusCreated, "order created", operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -462,13 +462,13 @@ func AssignGasorderBasic(ctx *gin.Context) {
return
}
var delivery models.DeliveryBasic
if err := impl.DBService.Where("identity = ?", request.DeliveryIdentity).First(&delivery).Error; err != nil || delivery.Status != "enabled" {
if err := impl.DBService.Where("identity = ?", request.DeliveryIdentity).First(&delivery).Error; err != nil || delivery.Status != common.StatusEnable {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var staff models.StaffAccount
if err := impl.DBService.Where("identity = ?", request.StaffIdentity).First(&staff).Error; err != nil ||
staff.Status != "enabled" || staff.WorkStatus == "off_duty" {
staff.Status != common.StatusEnable || staff.WorkStatus == "off_duty" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -478,17 +478,17 @@ func AssignGasorderBasic(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if order.Status != "created" && order.Status != "assigned" {
if order.Status != common.StatusCreated && order.Status != common.StatusAssigned {
return errors.New("order cannot be assigned")
}
previous := order.Status
if err := tx.Model(&order).Updates(map[string]any{
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "status": "assigned",
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "status": common.StatusAssigned,
}).Error; err != nil {
return err
}
assignment := models.GasorderAssign{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded, Version: 1},
GasorderBasicID: order.ID, GasBasicID: order.GasBasicID, DeliveryBasicID: delivery.ID,
StaffAccountID: staff.ID, AssignerIdentity: operatorIdentity, AssignerName: operatorName,
AssignedAt: time.Now(), Reason: request.Reason,
@@ -496,8 +496,8 @@ func AssignGasorderBasic(ctx *gin.Context) {
if err := tx.Create(&assignment).Error; err != nil {
return err
}
if previous != "assigned" {
return tx.Create(gasorderStatusRecord(order.ID, previous, "assigned", request.Reason, operatorIdentity, operatorName)).Error
if previous != common.StatusAssigned {
return tx.Create(gasorderStatusRecord(order.ID, previous, common.StatusAssigned, request.Reason, operatorIdentity, operatorName)).Error
}
return nil
})
@@ -505,21 +505,24 @@ func AssignGasorderBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": "assigned"})
infra.Response.Success(ctx, gin.H{"updated": true, "status": common.StatusAssigned})
}
func GasorderStartFilling(ctx *gin.Context) {
transitionGasorder(ctx, "filling", map[string]bool{"assigned": true})
transitionGasorder(ctx, common.StatusFilling, map[int]bool{common.StatusAssigned: true})
}
func GasorderReady(ctx *gin.Context) {
transitionGasorder(ctx, "ready", map[string]bool{"filling": true})
transitionGasorder(ctx, common.StatusReady, map[int]bool{common.StatusFilling: true})
}
func GasorderCancel(ctx *gin.Context) {
transitionGasorder(ctx, "cancelled", map[string]bool{"created": true, "assigned": true})
transitionGasorder(ctx, common.StatusCancelled, map[int]bool{common.StatusCreated: true, common.StatusAssigned: true})
}
func GasorderException(ctx *gin.Context) {
transitionGasorder(ctx, "exception", map[string]bool{"filling": true, "ready": true, "delivering": true, "awaiting_confirmation": true})
transitionGasorder(ctx, common.StatusException, map[int]bool{
common.StatusFilling: true, common.StatusReady: true,
common.StatusDelivering: true, common.StatusAwaitingConfirmation: true,
})
}
func GasorderRecover(ctx *gin.Context) {
@@ -536,14 +539,14 @@ func GasorderRecover(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if order.Status != "exception" || order.PreviousStatus == "" {
if order.Status != common.StatusException || order.PreviousStatus == common.StatusDraft {
return errors.New("order cannot recover")
}
target := order.PreviousStatus
if err := tx.Model(&order).Updates(map[string]any{"status": target, "previous_status": ""}).Error; err != nil {
if err := tx.Model(&order).Updates(map[string]any{"status": target, "previous_status": common.StatusDraft}).Error; err != nil {
return err
}
return tx.Create(gasorderStatusRecord(order.ID, "exception", target, request.Reason, operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, common.StatusException, target, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -552,7 +555,7 @@ func GasorderRecover(ctx *gin.Context) {
infra.Response.Success(ctx, gin.H{"updated": true})
}
func transitionGasorder(ctx *gin.Context, target string, allowed map[string]bool) {
func transitionGasorder(ctx *gin.Context, target int, allowed map[int]bool) {
var request struct {
Reason string `json:"reason" binding:"required"`
}
@@ -566,11 +569,11 @@ func transitionGasorder(ctx *gin.Context, target string, allowed map[string]bool
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if !allowed[order.Status] || (target == "filling" && order.DeliveryBasicID == 0) {
if !allowed[order.Status] || (target == common.StatusFilling && order.DeliveryBasicID == 0) {
return errors.New("invalid order transition")
}
updates := map[string]any{"status": target}
if target == "exception" {
if target == common.StatusException {
updates["previous_status"] = order.Status
}
if err := tx.Model(&order).Updates(updates).Error; err != nil {
@@ -585,9 +588,9 @@ func transitionGasorder(ctx *gin.Context, target string, allowed map[string]bool
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
}
func gasorderStatusRecord(orderID uint64, from, to, reason, operatorIdentity, operatorName string) *models.GasorderStatus {
func gasorderStatusRecord(orderID uint64, from, to int, reason, operatorIdentity, operatorName string) *models.GasorderStatus {
return &models.GasorderStatus{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded, Version: 1},
GasorderBasicID: orderID, FromStatus: from, ToStatus: to,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
}

View File

@@ -19,21 +19,21 @@ func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) {
}
func TestGasorderStatusRecordIsImmutableSnapshot(t *testing.T) {
record := gasorderStatusRecord(7, "assigned", "filling", "start filling", "operator-a", "Operator")
if record.GasorderBasicID != 7 || record.FromStatus != "assigned" || record.ToStatus != "filling" {
record := gasorderStatusRecord(7, common.StatusAssigned, common.StatusFilling, "start filling", "operator-a", "Operator")
if record.GasorderBasicID != 7 || record.FromStatus != common.StatusAssigned || record.ToStatus != common.StatusFilling {
t.Fatalf("unexpected status record: %#v", record)
}
if record.Status != "recorded" || record.OccurredAt.IsZero() {
if record.Status != common.StatusRecorded || record.OccurredAt.IsZero() {
t.Fatalf("status record lacks immutable metadata: %#v", record)
}
}
func TestContractRevisionKeepsSingleContractHistory(t *testing.T) {
contract := models.GasorderContract{
Entity: models.Entity{ID: 9, Status: "active"},
Entity: models.Entity{ID: 9, Status: common.StatusActive},
}
revision := contractRevision(contract, "renew", "annual renewal", "operator-a", "Operator")
if revision.GasorderContractID != 9 || revision.Action != "renew" || revision.ContractStatus != "active" {
if revision.GasorderContractID != 9 || revision.Action != "renew" || revision.ContractStatus != common.StatusActive {
t.Fatalf("unexpected contract revision: %#v", revision)
}
}

View File

@@ -0,0 +1,147 @@
package platform
import (
"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"
)
// Menu 是平台后台的静态菜单定义,不对应数据库表。
type Menu struct {
Identity string `json:"identity"`
ParentIdentity string `json:"parent_identity,omitempty"`
MenuCode string `json:"menu_code"`
Name string `json:"name"`
Icon string `json:"icon"`
Path string `json:"path"`
SortNo int `json:"sort_no"`
Status int `json:"status"`
}
// PlatformMenus 使用二维数据组织菜单;每一行表示一个菜单分组。
var PlatformMenus = [][]Menu{
{
{Identity: "dashboard", MenuCode: "dashboard", Name: "工作台", Icon: "icon-dashboard", Path: "/dashboard", SortNo: 10, Status: common.StatusEnable},
{Identity: "dashboard_overview", ParentIdentity: "dashboard", MenuCode: "dashboard", Name: "数据概览", Path: "/dashboard/overview", SortNo: 1, Status: common.StatusEnable},
{Identity: "dashboard_reports", ParentIdentity: "dashboard", MenuCode: "dashboard", Name: "统计报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
},
{
{Identity: "gas", MenuCode: "gas", Name: "气站管理", Icon: "icon-storage", Path: "/gas", SortNo: 20, Status: common.StatusEnable},
{Identity: "gas_basic", ParentIdentity: "gas", MenuCode: "gas", Name: "气站", Path: "/gas/gas-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "gas_account", ParentIdentity: "gas", MenuCode: "gas", Name: "气站账户", Path: "/gas/gas-account", SortNo: 2, Status: common.StatusEnable},
},
{
{Identity: "delivery", MenuCode: "delivery", Name: "配送站管理", Icon: "icon-send", Path: "/delivery", SortNo: 30, Status: common.StatusEnable},
{Identity: "delivery_basic", ParentIdentity: "delivery", MenuCode: "delivery", Name: "配送站", Path: "/delivery/delivery-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "delivery_account", ParentIdentity: "delivery", MenuCode: "delivery", Name: "配送站账户", Path: "/delivery/delivery-account", SortNo: 2, Status: common.StatusEnable},
},
{
{Identity: "staff", MenuCode: "staff", Name: "工作人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 40, Status: common.StatusEnable},
{Identity: "staff_account", ParentIdentity: "staff", MenuCode: "staff", Name: "工作人员", Path: "/staff/staff-account", SortNo: 1, Status: common.StatusEnable},
{Identity: "staff_credential", ParentIdentity: "staff", MenuCode: "staff", Name: "人员资质", Path: "/staff/staff-credential", SortNo: 2, Status: common.StatusEnable},
},
{
{Identity: "user", MenuCode: "user", Name: "用户管理", Icon: "icon-user", Path: "/user", SortNo: 50, Status: common.StatusEnable},
{Identity: "user_account", ParentIdentity: "user", MenuCode: "user", Name: "用户账户", Path: "/user/user-account", SortNo: 1, Status: common.StatusEnable},
{Identity: "user_address", ParentIdentity: "user", MenuCode: "user", Name: "用户地址", Path: "/user/user-address", SortNo: 2, Status: common.StatusEnable},
{Identity: "user_service_relation", ParentIdentity: "user", MenuCode: "user", Name: "服务关系", Path: "/user/service-relation", SortNo: 3, Status: common.StatusEnable},
},
{
{Identity: "device", MenuCode: "device", Name: "产品管理", Icon: "icon-common", Path: "/product", SortNo: 60, Status: common.StatusEnable},
{Identity: "product_type", ParentIdentity: "device", MenuCode: "device", Name: "产品类型", Path: "/product/product-type", SortNo: 1, Status: common.StatusEnable},
{Identity: "product_warehouse", ParentIdentity: "device", MenuCode: "device", Name: "库房", Path: "/product/warehouse", SortNo: 2, Status: common.StatusEnable},
{Identity: "product_info", ParentIdentity: "device", MenuCode: "device", Name: "产品信息", Path: "/product/product-info", SortNo: 3, Status: common.StatusEnable},
},
{
{Identity: "gasorder", MenuCode: "delivery", Name: "气体配送订单管理", Icon: "icon-list", Path: "/gasorder", SortNo: 70, Status: common.StatusEnable},
{Identity: "gasorder_contract", ParentIdentity: "gasorder", MenuCode: "delivery", Name: "合同管理", Path: "/gasorder/contracts", SortNo: 1, Status: common.StatusEnable},
{Identity: "gasorder_basic", ParentIdentity: "gasorder", MenuCode: "delivery", Name: "配送订单", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
{Identity: "gasorder_track", ParentIdentity: "gasorder", MenuCode: "delivery", Name: "运行轨迹", Path: "/gasorder/tracks", SortNo: 3, Status: common.StatusEnable},
},
{
{Identity: "ec", MenuCode: "ec", Name: "商城管理", Icon: "icon-gift", Path: "/ec", SortNo: 80, Status: common.StatusEnable},
{Identity: "ec_category", ParentIdentity: "ec", MenuCode: "ec", Name: "商品分类", Path: "/ec/categories", SortNo: 1, Status: common.StatusEnable},
{Identity: "ec_product", ParentIdentity: "ec", MenuCode: "ec", Name: "商品", Path: "/ec/products", SortNo: 2, Status: common.StatusEnable},
{Identity: "ec_cart", ParentIdentity: "ec", MenuCode: "ec", Name: "购物车", Path: "/ec/carts", SortNo: 3, Status: common.StatusEnable},
{Identity: "ec_order", ParentIdentity: "ec", MenuCode: "ec", Name: "商城订单", Path: "/ec/orders", SortNo: 4, Status: common.StatusEnable},
{Identity: "ec_review", ParentIdentity: "ec", MenuCode: "ec", Name: "商品评价", Path: "/ec/reviews", SortNo: 5, Status: common.StatusEnable},
},
{
{Identity: "wallet", MenuCode: "wallet", Name: "钱包管理", Icon: "icon-safe", Path: "/wallet", SortNo: 90, Status: common.StatusEnable},
{Identity: "wallet_basic", ParentIdentity: "wallet", MenuCode: "wallet", Name: "钱包", Path: "/wallet/wallet-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "wallet_bank", ParentIdentity: "wallet", MenuCode: "wallet", Name: "银行卡", Path: "/wallet/banks", SortNo: 2, Status: common.StatusEnable},
{Identity: "wallet_payment", ParentIdentity: "wallet", MenuCode: "wallet", Name: "支付记录", Path: "/wallet/payments", SortNo: 3, Status: common.StatusEnable},
{Identity: "wallet_record", ParentIdentity: "wallet", MenuCode: "wallet", Name: "钱包流水", Path: "/wallet/records", SortNo: 4, Status: common.StatusEnable},
{Identity: "wallet_refund", ParentIdentity: "wallet", MenuCode: "wallet", Name: "退款记录", Path: "/wallet/refunds", SortNo: 5, Status: common.StatusEnable},
{Identity: "wallet_apply_cash", ParentIdentity: "wallet", MenuCode: "wallet", Name: "提现申请", Path: "/wallet/apply-cash", SortNo: 6, Status: common.StatusEnable},
},
{
{Identity: "finance", MenuCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 100, Status: common.StatusEnable},
{Identity: "fin_payment", ParentIdentity: "finance", MenuCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 1, Status: common.StatusEnable},
{Identity: "fin_settlement", ParentIdentity: "finance", MenuCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 2, Status: common.StatusEnable},
{Identity: "fin_reconciliation", ParentIdentity: "finance", MenuCode: "finance", Name: "财务对账", Path: "/finance/reconciliations", SortNo: 3, Status: common.StatusEnable},
},
{
{Identity: "content", MenuCode: "content", Name: "内容管理", Icon: "icon-file", Path: "/content", SortNo: 110, Status: common.StatusEnable},
{Identity: "cms_content", ParentIdentity: "content", MenuCode: "content", Name: "内容", Path: "/content/contents", SortNo: 1, Status: common.StatusEnable},
},
{
{Identity: "customer_service", MenuCode: "customer_service", Name: "客服管理", Icon: "icon-customer-service", Path: "/customer-service", SortNo: 120, Status: common.StatusEnable},
{Identity: "cs_ticket", ParentIdentity: "customer_service", MenuCode: "customer_service", Name: "客服工单", Path: "/customer-service/tickets", SortNo: 1, Status: common.StatusEnable},
},
{
{Identity: "platform", MenuCode: "platform", Name: "平台管理", Icon: "icon-settings", Path: "/platform", SortNo: 130, Status: common.StatusEnable},
{Identity: "platform_account", ParentIdentity: "platform", MenuCode: "platform", Name: "平台账户", Path: "/platform/accounts", SortNo: 1, Status: common.StatusEnable},
{Identity: "platform_role", ParentIdentity: "platform", MenuCode: "platform", Name: "平台角色", Path: "/platform/roles", SortNo: 2, Status: common.StatusEnable},
{Identity: "platform_menu", ParentIdentity: "platform", MenuCode: "platform", Name: "平台菜单", Path: "/platform/menus", SortNo: 3, Status: common.StatusEnable},
},
}
// AllPlatformMenus 返回静态菜单的扁平副本。
func AllPlatformMenus() []Menu {
menus := make([]Menu, 0, len(PlatformMenus))
for _, group := range PlatformMenus {
menus = append(menus, group...)
}
return menus
}
// FindPlatformMenu 根据稳定 identity 查找静态菜单。
func FindPlatformMenu(identity string) (Menu, bool) {
for _, menu := range AllPlatformMenus() {
if menu.Identity == identity {
return menu, true
}
}
return Menu{}, false
}
// LoadPlatformMenus 返回角色获授权的静态菜单root 默认拥有全部菜单。
func LoadPlatformMenus(roleCode string) ([]Menu, error) {
menus := AllPlatformMenus()
if roleCode == "root" {
return menus, nil
}
var role models.PlatformRole
if err := impl.DBService.Where("role_code = ? AND status = ?", roleCode, common.StatusEnable).First(&role).Error; err != nil {
return nil, err
}
var codes []string
if err := impl.DBService.Model(&models.PlatformRoleMenu{}).
Where("platform_role_id = ?", role.ID).
Pluck("menu_code", &codes).Error; err != nil {
return nil, err
}
allowed := make(map[string]struct{}, len(codes))
for _, code := range codes {
allowed[code] = struct{}{}
}
filtered := make([]Menu, 0, len(menus))
for _, menu := range menus {
if _, ok := allowed[menu.MenuCode]; ok {
filtered = append(filtered, menu)
}
}
return filtered, nil
}

View File

@@ -0,0 +1,41 @@
package platform
import "testing"
func TestPlatformMenusAreStaticTwoDimensionalData(t *testing.T) {
if len(PlatformMenus) == 0 {
t.Fatal("PlatformMenus must contain menu groups")
}
seen := make(map[string]bool)
for _, group := range PlatformMenus {
if len(group) < 2 {
t.Fatal("each platform menu group must include a first-level menu and at least one second-level menu")
}
parent := group[0]
if parent.ParentIdentity != "" {
t.Fatalf("first menu in group must be first-level: %#v", parent)
}
for index, menu := range group {
if menu.Identity == "" || menu.MenuCode == "" {
t.Fatalf("menu identity and code are required: %#v", menu)
}
if index > 0 && menu.ParentIdentity != parent.Identity {
t.Fatalf("second-level menu %s must reference parent %s", menu.Identity, parent.Identity)
}
if seen[menu.Identity] {
t.Fatalf("duplicate menu identity: %s", menu.Identity)
}
seen[menu.Identity] = true
}
}
}
func TestRootLoadsAllStaticPlatformMenus(t *testing.T) {
menus, err := LoadPlatformMenus("root")
if err != nil {
t.Fatal(err)
}
if len(menus) != len(AllPlatformMenus()) {
t.Fatalf("root menu count = %d, want %d", len(menus), len(AllPlatformMenus()))
}
}

View File

@@ -4,15 +4,14 @@ import (
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
"github.com/gin-gonic/gin"
"strings"
)
const platformMenusContextKey = "platform_authorized_menus"
func platformMenuAllowsPath(menus []models.PlatformMenu, requestPath string) bool {
func platformMenuAllowsPath(menus []platformbase.Menu, requestPath string) bool {
marker := "/platform/v1/"
index := strings.Index(requestPath, marker)
if index < 0 {
@@ -70,7 +69,7 @@ func RequirePlatformMenuAccess() gin.HandlerFunc {
ctx.Next()
return
}
menus, err := common.LoadPlatformMenus(claims.Role)
menus, err := platformbase.LoadPlatformMenus(claims.Role)
if err != nil || !platformMenuAllowsPath(menus, ctx.Request.URL.Path) {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()

View File

@@ -78,7 +78,7 @@ func CreatePlatformAccount(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
account := models.PlatformAccount{Entity: common.NewEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone}
account := models.PlatformAccount{Entity: common.NewEntity(common.StatusEnable), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone}
if err := impl.DBService.Create(&account).Error; err != nil {
infra.Response.Error(ctx, err)
return
@@ -138,5 +138,5 @@ func isAssignablePlatformRole(roleCode string) bool {
return false
}
var role models.PlatformRole
return impl.DBService.Where("role_code = ? AND status = ? AND is_system = ?", roleCode, "enabled", false).First(&role).Error == nil
return impl.DBService.Where("role_code = ? AND status = ? AND is_system = ?", roleCode, common.StatusEnable, false).First(&role).Error == nil
}

View File

@@ -4,125 +4,31 @@ import (
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/bsm-sdk/core/middleware"
"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"
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
"github.com/gin-gonic/gin"
)
type platformMenuRequest struct {
ParentIdentity string `json:"parent_identity"`
MenuCode string `json:"menu_code" binding:"required,max=64"`
Name string `json:"name" binding:"required,max=64"`
Icon string `json:"icon" binding:"max=64"`
Path string `json:"path" binding:"max=255"`
SortNo int `json:"sort_no"`
}
type platformMenuView struct {
Identity string `json:"identity"`
ParentIdentity string `json:"parent_identity,omitempty"`
MenuCode string `json:"menu_code"`
Name string `json:"name"`
Icon string `json:"icon"`
Path string `json:"path"`
SortNo int `json:"sort_no"`
Status string `json:"status"`
}
func platformMenuViews(list []models.PlatformMenu) []platformMenuView {
identities := make(map[uint64]string, len(list))
for _, item := range list {
identities[item.ID] = item.Identity
}
views := make([]platformMenuView, 0, len(list))
for _, item := range list {
views = append(views, platformMenuView{Identity: item.Identity, ParentIdentity: identities[item.ParentID], MenuCode: item.MenuCode, Name: item.Name, Icon: item.Icon, Path: item.Path, SortNo: item.SortNo, Status: item.Status})
}
return views
}
// GetPlatformMenu 返回一项静态菜单定义。
func GetPlatformMenu(ctx *gin.Context) {
var menu models.PlatformMenu
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&menu).Error; err != nil {
common.RespondRecordError(ctx, err)
menu, ok := platformbase.FindPlatformMenu(ctx.Param("identity"))
if !ok {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return
}
list := []models.PlatformMenu{menu}
if menu.ParentID != 0 {
var parent models.PlatformMenu
if err := impl.DBService.Select("id", "identity").First(&parent, menu.ParentID).Error; err == nil {
list = append(list, parent)
}
}
views := platformMenuViews(list)
infra.Response.Success(ctx, views[0])
infra.Response.Success(ctx, menu)
}
func CreatePlatformMenu(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
var request platformMenuRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
parentID, err := common.ResolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
menu := models.PlatformMenu{Entity: common.NewEntity("enabled"), ParentID: parentID, MenuCode: request.MenuCode, Name: request.Name, Icon: request.Icon, Path: request.Path, SortNo: request.SortNo}
if err := impl.DBService.Create(&menu).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, menu)
}
func UpdatePlatformMenu(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
var request platformMenuRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
parentID, err := common.ResolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.PlatformMenu{}, gin.H{"parent_id": parentID, "name": request.Name, "icon": request.Icon, "path": request.Path, "sort_no": request.SortNo}, []string{"parent_id", "name", "icon", "path", "sort_no"})
}
func UpdatePlatformMenuStatus(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
common.UpdateRecordStatus(ctx, &models.PlatformMenu{})
}
func ArchivePlatformMenu(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
}
common.ArchiveRecord(ctx, &models.PlatformMenu{})
}
// ListPlatformMenu 返回菜单树构建所需的有序菜单列表。
// ListPlatformMenu 返回当前角色获授权的静态菜单。
func ListPlatformMenu(ctx *gin.Context) {
claims, err := middleware.ParseAuth(ctx)
if err != nil {
infra.Response.Error(ctx, err)
return
}
list, err := common.LoadPlatformMenus(claims.Role)
list, err := platformbase.LoadPlatformMenus(claims.Role)
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"total": len(list), "list": platformMenuViews(list)})
infra.Response.Success(ctx, gin.H{"total": len(list), "list": list})
}

View File

@@ -25,7 +25,7 @@ func CreatePlatformRole(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
request.Entity = common.NewEntity("enabled")
request.Entity = common.NewEntity(common.StatusEnable)
request.IsSystem = false
if request.DataScope == "" {
request.DataScope = "global"
@@ -68,7 +68,7 @@ func UpdatePlatformRoleStatus(ctx *gin.Context) {
return
}
var request struct {
Status string `json:"status" binding:"required,max=32"`
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -100,5 +100,5 @@ func ArchivePlatformRole(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "archived"}, []string{"status"})
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": common.StatusArchived}, []string{"status"})
}

View File

@@ -6,6 +6,7 @@ import (
"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"
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -35,20 +36,19 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
if role.IsSystem {
return errSystemPlatformRole
}
var menus []models.PlatformMenu
if len(request.MenuIdentities) > 0 {
if err := transaction.Where("identity IN ?", request.MenuIdentities).Find(&menus).Error; err != nil {
return err
}
if len(menus) != len(request.MenuIdentities) {
menuCodes := make(map[string]struct{}, len(request.MenuIdentities))
for _, identity := range request.MenuIdentities {
menu, ok := platformbase.FindPlatformMenu(identity)
if !ok {
return gorm.ErrRecordNotFound
}
menuCodes[menu.MenuCode] = struct{}{}
}
if err := transaction.Where("platform_role_id = ?", role.ID).Delete(&models.PlatformRoleMenu{}).Error; err != nil {
return err
}
for _, menu := range menus {
relation := models.PlatformRoleMenu{PlatformRoleID: role.ID, PlatformMenuID: menu.ID}
for menuCode := range menuCodes {
relation := models.PlatformRoleMenu{PlatformRoleID: role.ID, MenuCode: menuCode}
if err := transaction.Create(&relation).Error; err != nil {
return err
}
@@ -79,14 +79,22 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
common.RespondRecordError(ctx, err)
return
}
var identities []string
if err := impl.DBService.Model(&models.PlatformMenu{}).
Joins("JOIN platform_role_menu ON platform_role_menu.platform_menu_id = platform_menu.id").
Where("platform_role_menu.platform_role_id = ?", role.ID).
Order("platform_menu.sort_no asc, platform_menu.id asc").
Pluck("platform_menu.identity", &identities).Error; err != nil {
var codes []string
if err := impl.DBService.Model(&models.PlatformRoleMenu{}).
Where("platform_role_id = ?", role.ID).
Pluck("menu_code", &codes).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
assigned := make(map[string]struct{}, len(codes))
for _, code := range codes {
assigned[code] = struct{}{}
}
identities := make([]string, 0, len(codes))
for _, menu := range platformbase.AllPlatformMenus() {
if _, ok := assigned[menu.MenuCode]; ok {
identities = append(identities, menu.Identity)
}
}
infra.Response.Success(ctx, gin.H{"menu_identities": identities})
}

View File

@@ -20,8 +20,9 @@ var productOwnerActions = map[string]bool{
"created": true, "warehouse": true, "assigned": true, "returned": true, "manual": true,
}
var productLifecycleStatuses = map[string]bool{
"pending": true, "in_stock": true, "in_transit": true, "in_use": true, "repairing": true, "scrapped": true,
var productLifecycleStatuses = map[int]bool{
common.StatusPending: true, common.StatusInStock: true, common.StatusInTransit: true,
common.StatusInUse: true, common.StatusRepairing: true, common.StatusScrapped: true,
}
func ProductInfoHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
@@ -41,12 +42,12 @@ func createProductInfo(ctx *gin.Context, fields []string, relations []common.Res
delete(values, "action")
delete(values, "reason")
delete(values, "remark")
data := models.ProductInfo{Entity: common.NewEntity("pending"), Params: "{}", IsEnabled: false}
data := models.ProductInfo{Entity: common.NewEntity(common.StatusPending), Params: "{}", IsEnabled: false}
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || data.ProducedAt.IsZero() {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data.Status = "pending"
data.Status = common.StatusPending
if data.IsEnabled {
now := time.Now()
data.EnabledAt = &now
@@ -92,11 +93,15 @@ func updateProductInfo(ctx *gin.Context, fields []string, relations []common.Res
if _, changingCode := values["code"]; changingCode {
delete(values, "code")
}
if status, ok := values["status"].(string); ok && !productLifecycleStatuses[status] {
return errors.New("invalid product status")
if rawStatus, supplied := values["status"]; supplied {
status, ok := intValue(rawStatus)
if !ok || !productLifecycleStatuses[status] {
return errors.New("invalid product status")
}
values["status"] = status
}
if enabled, ok := values["is_enabled"].(bool); ok && enabled && !current.IsEnabled {
if current.Status == "scrapped" {
if current.Status == common.StatusScrapped {
return errors.New("scrapped product cannot be enabled")
}
if current.EnabledAt == nil {
@@ -161,7 +166,7 @@ func listProductOwners(ctx *gin.Context) {
func UpdateProductInfoStatus(ctx *gin.Context) {
var request struct {
Status string `json:"status" binding:"required,max=32"`
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !productLifecycleStatuses[request.Status] {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -184,7 +189,7 @@ func createProductRepair(ctx *gin.Context, fields []string, relations []common.R
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data := models.ProductRepair{Entity: common.NewEntity("active"), Result: "pending"}
data := models.ProductRepair{Entity: common.NewEntity(common.StatusActive), Result: "pending"}
if err := decodeValues(values, &data); err != nil || data.Result != "pending" || !validRepair(data) || data.ProductInfoID == 0 || data.RepairNo == "" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
@@ -193,7 +198,7 @@ func createProductRepair(ctx *gin.Context, fields []string, relations []common.R
if err := tx.Create(&data).Error; err != nil {
return err
}
return tx.Model(&models.ProductInfo{}).Where("id = ?", data.ProductInfoID).Update("status", "repairing").Error
return tx.Model(&models.ProductInfo{}).Where("id = ?", data.ProductInfoID).Update("status", common.StatusRepairing).Error
})
if err != nil {
infra.Response.Error(ctx, err)
@@ -254,7 +259,7 @@ func validRepair(repair models.ProductRepair) bool {
return repair.CompletedAt == nil
case "passed", "failed":
return repair.CompletedAt != nil && !repair.CompletedAt.Before(repair.StartedAt) &&
productLifecycleStatuses[repair.TargetStatus] && repair.TargetStatus != "repairing"
productLifecycleStatuses[repair.TargetStatus] && repair.TargetStatus != common.StatusRepairing
default:
return false
}
@@ -271,7 +276,7 @@ func createProductOwner(ctx *gin.Context, fields []string, relations []common.Re
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data := models.ProductOwner{Entity: common.NewEntity("recorded")}
data := models.ProductOwner{Entity: common.NewEntity(common.StatusRecorded)}
if err := decodeValues(values, &data); err != nil || data.ProductInfoID == 0 || data.OccurredAt.IsZero() {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
@@ -304,14 +309,14 @@ func decodeValues(values map[string]any, target any) error {
return json.Unmarshal(encoded, target)
}
func initialProductStatus(product models.ProductInfo) string {
func initialProductStatus(product models.ProductInfo) int {
if product.WarehouseID != 0 {
return "in_stock"
return common.StatusInStock
}
if product.UserAccountID != 0 {
return "in_use"
return common.StatusInUse
}
return "pending"
return common.StatusPending
}
func ownershipValuesChanged(current models.ProductInfo, values map[string]any) bool {
@@ -339,9 +344,20 @@ func numericID(value any) (uint64, bool) {
}
}
func intValue(value any) (int, bool) {
switch raw := value.(type) {
case int:
return raw, true
case float64:
return int(raw), raw == float64(int(raw))
default:
return 0, false
}
}
func newProductOwner(product models.ProductInfo, action, reason, remark, operatorIdentity, operatorName string, occurredAt time.Time) *models.ProductOwner {
return &models.ProductOwner{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "recorded", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded, Version: 1},
ProductInfoID: product.ID, WarehouseID: product.WarehouseID, GasBasicID: product.GasBasicID,
DeliveryBasicID: product.DeliveryBasicID, UserAccountID: product.UserAccountID,
Action: action, OccurredAt: occurredAt, Reason: reason, Remark: remark,

View File

@@ -4,6 +4,7 @@ import (
"testing"
"time"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
)
@@ -21,14 +22,14 @@ func TestValidParamsTextRequiresJSONObjectText(t *testing.T) {
}
func TestInitialProductStatusPrefersActualWarehouse(t *testing.T) {
if got := initialProductStatus(models.ProductInfo{WarehouseID: 1, UserAccountID: 2}); got != "in_stock" {
t.Fatalf("warehouse product status = %s", got)
if got := initialProductStatus(models.ProductInfo{WarehouseID: 1, UserAccountID: 2}); got != common.StatusInStock {
t.Fatalf("warehouse product status = %d", got)
}
if got := initialProductStatus(models.ProductInfo{UserAccountID: 2}); got != "in_use" {
t.Fatalf("user product status = %s", got)
if got := initialProductStatus(models.ProductInfo{UserAccountID: 2}); got != common.StatusInUse {
t.Fatalf("user product status = %d", got)
}
if got := initialProductStatus(models.ProductInfo{}); got != "pending" {
t.Fatalf("unlocated product status = %s", got)
if got := initialProductStatus(models.ProductInfo{}); got != common.StatusPending {
t.Fatalf("unlocated product status = %d", got)
}
}
@@ -38,13 +39,13 @@ func TestValidRepairEnforcesCompletionRules(t *testing.T) {
if !validRepair(models.ProductRepair{StartedAt: started, Result: "pending"}) {
t.Fatal("pending repair was rejected")
}
if !validRepair(models.ProductRepair{StartedAt: started, CompletedAt: &completed, Result: "passed", TargetStatus: "in_stock"}) {
if !validRepair(models.ProductRepair{StartedAt: started, CompletedAt: &completed, Result: "passed", TargetStatus: common.StatusInStock}) {
t.Fatal("completed repair was rejected")
}
if validRepair(models.ProductRepair{StartedAt: started, Result: "passed", TargetStatus: "in_stock"}) {
if validRepair(models.ProductRepair{StartedAt: started, Result: "passed", TargetStatus: common.StatusInStock}) {
t.Fatal("completed result without completion time was accepted")
}
if validRepair(models.ProductRepair{StartedAt: completed, CompletedAt: &started, Result: "failed", TargetStatus: "in_stock"}) {
if validRepair(models.ProductRepair{StartedAt: completed, CompletedAt: &started, Result: "failed", TargetStatus: common.StatusInStock}) {
t.Fatal("completion before start was accepted")
}
}

View File

@@ -3,6 +3,7 @@ package platform
import (
"net/http"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"github.com/gin-gonic/gin"
)
@@ -55,7 +56,7 @@ func (d ResourceDefinition) Allows(method string) bool {
}
func archiveValues() gin.H {
return gin.H{"status": "archived"}
return gin.H{"status": common.StatusArchived}
}
func filterFields(values map[string]any, allowedFields []string) gin.H {
@@ -87,7 +88,7 @@ func ExpectedResources() []ResourceContract {
resourceContract("gasorder", "gasorder_track", ReadOnly, "list"), resourceContract("gasorder", "gasorder_track_point", ReadOnly, "list"), resourceContract("gasorder", "gasorder_confirm", ReadOnly, "list"), resourceContract("gasorder", "gasorder_payment", ReadOnly, "list"),
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
resourceContract("platform", "platform_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
resourceContract("platform", "platform_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", ReadOnly, "tree"),
resourceContract("wallet", "wallet_basic", ReadOnly, "list"), resourceContract("wallet", "wallet_bank", ReadOnly, "list"), resourceContract("wallet", "wallet_payment", ReadOnly, "list"), resourceContract("wallet", "wallet_record", ReadOnly, "list"), resourceContract("wallet", "wallet_refund", ReadOnly, "list"), resourceContract("wallet", "wallet_apply_cash", ReadOnly, "list"),
}
}

View File

@@ -31,7 +31,7 @@ func CreateStaffCredential(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
credential := models.StaffCredential{Entity: common.NewEntity("enabled"), StaffAccountID: staffAccountID, CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt}
credential := models.StaffCredential{Entity: common.NewEntity(common.StatusEnable), StaffAccountID: staffAccountID, CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt}
if err := impl.DBService.Create(&credential).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -47,7 +47,7 @@ func CreateStaff(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
staff := models.StaffAccount{Entity: common.NewEntity("draft"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, WorkStatus: request.WorkStatus}
staff := models.StaffAccount{Entity: common.NewEntity(common.StatusDraft), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, WorkStatus: request.WorkStatus}
if staff.WorkStatus == "" {
staff.WorkStatus = "off_duty"
}

View File

@@ -30,7 +30,7 @@ func CreateUserAddress(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
address := models.UserAddress{Entity: common.NewEntity("enabled"), UserAccountID: userAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
address := models.UserAddress{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
if err := impl.DBService.Create(&address).Error; err != nil {
infra.Response.Error(ctx, err)
return
@@ -70,7 +70,7 @@ func CreateUserServiceRelation(ctx *gin.Context) {
if !ok {
return
}
relation := models.UserServiceRelation{Entity: common.NewEntity("enabled"), UserAccountID: userAccountID, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, StaffAccountID: staffAccountID}
relation := models.UserServiceRelation{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, StaffAccountID: staffAccountID}
if err := impl.DBService.Create(&relation).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -34,7 +34,7 @@ func CreateUser(ctx *gin.Context) {
infra.Response.Error(ctx, err)
return
}
user := models.UserAccount{Entity: common.NewEntity("enabled"), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName}
user := models.UserAccount{Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash, Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName}
if err := impl.DBService.Create(&user).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -133,7 +133,7 @@ func GetOrCreateOwnerWallet(ctx *gin.Context) {
return err
}
candidate := models.WalletBasic{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable, Version: 1},
OwnerType: ownerType, OwnerID: ownerID, OwnerIdentity: ownerIdentity,
}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&candidate).Error; err != nil {
@@ -169,9 +169,10 @@ func resolveWalletOwner(ownerType, ownerIdentity string) (uint64, error) {
func UpdateWalletBasicStatus(ctx *gin.Context) {
var request struct {
Status string `json:"status" binding:"required"`
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || (request.Status != "enabled" && request.Status != "disabled") {
if err := ctx.ShouldBindJSON(&request); err != nil ||
(request.Status != common.StatusEnable && request.Status != common.StatusDisable && request.Status != common.StatusFrozen) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -209,7 +210,7 @@ func RechargeWalletBasic(ctx *gin.Context) {
}
updates := map[string]any{"balance": gorm.Expr("balance + ?", request.Amount)}
query := tx.Model(&models.WalletBasic{}).
Where("id = ? AND status = ? AND balance <= ?", wallet.ID, "enabled", math.MaxInt64-request.Amount)
Where("id = ? AND status = ? AND balance <= ?", wallet.ID, common.StatusEnable, math.MaxInt64-request.Amount)
if request.Withdrawable {
updates["withdrawal_balance"] = gorm.Expr("withdrawal_balance + ?", request.Amount)
query = query.Where("withdrawal_balance <= ?", math.MaxInt64-request.Amount)
@@ -226,7 +227,7 @@ func RechargeWalletBasic(ctx *gin.Context) {
}
now := time.Now()
record = models.WalletRecord{
Entity: models.Entity{Identity: models.NewIdentity(), Status: "posted", Version: 1},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusPosted, Version: 1},
WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: request.RequestNo,
Direction: "income", TradeType: "recharge", Amount: request.Amount,
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
@@ -250,14 +251,14 @@ func RechargeWalletBasic(ctx *gin.Context) {
}
func ApproveWalletApplyCash(ctx *gin.Context) {
reviewWalletApplyCash(ctx, "approved")
reviewWalletApplyCash(ctx, common.StatusApproved)
}
func RejectWalletApplyCash(ctx *gin.Context) {
reviewWalletApplyCash(ctx, "rejected")
reviewWalletApplyCash(ctx, common.StatusRejected)
}
func reviewWalletApplyCash(ctx *gin.Context, targetStatus string) {
func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
if !common.RequirePlatformRoot(ctx) {
return
}
@@ -278,11 +279,11 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus string) {
if application.Status == targetStatus {
return nil
}
if application.Status != "pending" {
if application.Status != common.StatusPending {
return errors.New("cash application is not pending")
}
now := time.Now()
if targetStatus == "rejected" {
if targetStatus == common.StatusRejected {
result := tx.Model(&models.WalletBasic{}).
Where("id = ? AND withdrawal_balance <= ?", application.WalletBasicID, math.MaxInt64-application.Amount).
Update("withdrawal_balance", gorm.Expr("withdrawal_balance + ?", application.Amount))