refactor platform domain states and permissions

This commit is contained in:
david
2026-07-29 14:25:38 +08:00
parent d4799cd320
commit 35a52c4b06
42 changed files with 507 additions and 364 deletions

View File

@@ -63,8 +63,8 @@ func Login(ctx *gin.Context) {
extend["location_scope"] = "precise"
} else {
var role models.PlatformRole
if impl.DBService.Select("data_scope").Where("role_code = ? AND status = ?", account.PlatformRoleCode, common.StatusEnable).First(&role).Error == nil &&
role.DataScope == "precise" {
if impl.DBService.Select("location_scope").Where("role_code = ? AND status = ?", account.PlatformRoleCode, common.StatusEnable).First(&role).Error == nil &&
role.LocationScope == "precise" {
extend["location_scope"] = "precise"
}
}
@@ -109,15 +109,9 @@ func CurrentProfile(ctx *gin.Context) {
menuCodes := make([]string, 0, len(menus))
seenMenuCodes := make(map[string]bool, len(menus))
for _, menu := range menus {
codes := []string{menu.Identity}
if path := strings.Trim(menu.Path, "/"); path != "" {
codes = append(codes, strings.Split(path, "/")[0])
}
for _, code := range codes {
if code != "" && !seenMenuCodes[code] {
menuCodes = append(menuCodes, code)
seenMenuCodes[code] = true
}
if menu.Identity != "" && !seenMenuCodes[menu.Identity] {
menuCodes = append(menuCodes, menu.Identity)
seenMenuCodes[menu.Identity] = true
}
}
infra.Response.Success(ctx, gin.H{

View File

@@ -152,7 +152,7 @@ func CreateGasorderContract(ctx *gin.Context) {
return
}
contract := models.GasorderContract{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusDraft},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, ContractStatus: common.StatusDraft,
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,
@@ -192,7 +192,7 @@ func UpdateGasorderContract(ctx *gin.Context) {
return
}
result := impl.DBService.Model(&models.GasorderContract{}).
Where("identity = ? AND status = ?", ctx.Param("identity"), common.StatusDraft).
Where("identity = ? AND contract_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})
@@ -228,13 +228,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 != common.StatusActive && contract.Status != common.StatusExpired && contract.Status != common.StatusTerminated {
if contract.ContractStatus != common.StatusActive && contract.ContractStatus != common.StatusExpired && contract.ContractStatus != common.StatusTerminated {
return errors.New("contract cannot be renewed")
}
if err := tx.Model(&contract).Updates(map[string]any{"status": common.StatusActive, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
if err := tx.Model(&contract).Updates(map[string]any{"contract_status": common.StatusActive, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
return err
}
contract.Status, contract.EffectiveAt, contract.ExpiredAt = common.StatusActive, request.EffectiveAt, request.ExpiredAt
contract.ContractStatus, contract.EffectiveAt, contract.ExpiredAt = common.StatusActive, request.EffectiveAt, request.ExpiredAt
return tx.Create(contractRevision(contract, "renew", request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
@@ -258,10 +258,10 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
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 != common.StatusDraft && contract.Status != common.StatusTerminated {
if action == "activate" && contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusTerminated {
return errors.New("contract cannot be activated")
}
if action == "terminate" && contract.Status != common.StatusActive {
if action == "terminate" && contract.ContractStatus != common.StatusActive {
return errors.New("contract cannot be terminated")
}
if target == common.StatusActive {
@@ -275,23 +275,23 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
return errors.New("contract has no active product")
}
}
if err := tx.Model(&contract).Update("status", target).Error; err != nil {
if err := tx.Model(&contract).Update("contract_status", target).Error; err != nil {
return err
}
contract.Status = target
contract.ContractStatus = target
return tx.Create(contractRevision(contract, action, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
infra.Response.Success(ctx, gin.H{"updated": true, "contract_status": target})
}
func contractRevision(contract models.GasorderContract, action, reason, operatorIdentity, operatorName string) *models.GasorderContractRevision {
return &models.GasorderContractRevision{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.Status,
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.ContractStatus,
EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
}
@@ -312,13 +312,13 @@ func BindGasorderContractProduct(ctx *gin.Context) {
common.RespondRecordError(ctx, err)
return
}
if contract.Status != common.StatusDraft && contract.Status != common.StatusActive {
if contract.ContractStatus != common.StatusDraft && contract.ContractStatus != 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 == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -328,7 +328,7 @@ func BindGasorderContractProduct(ctx *gin.Context) {
return
}
binding := models.GasorderContractProduct{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusBound},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderContractID: contract.ID, ProductInfoID: product.ID, ProductCode: product.Code,
ProductTypeName: productType.Name, ProductParams: product.Params, UnitPrice: request.UnitPrice, BoundAt: time.Now(),
}
@@ -350,6 +350,7 @@ func UnbindGasorderContractProduct(ctx *gin.Context) {
now := time.Now()
result := impl.DBService.Model(&models.GasorderContractProduct{}).
Where("identity = ? AND unbound_at IS NULL", ctx.Param("identity")).
Where("NOT EXISTS (SELECT 1 FROM gasorder_item WHERE gasorder_item.gasorder_contract_product_id = gasorder_contract_product.id AND gasorder_item.active = true)").
Updates(gasorderUnbindUpdates(request.Reason, now))
if result.Error != nil || result.RowsAffected != 1 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -389,11 +390,11 @@ func CreateGasorderBasic(ctx *gin.Context) {
return err
}
var contract models.GasorderContract
if err := tx.Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
return err
}
now := time.Now()
if contract.Status != common.StatusActive || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
if contract.ContractStatus != 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 {
@@ -417,8 +418,8 @@ func CreateGasorderBasic(ctx *gin.Context) {
var productAmount int64
for _, binding := range bindings {
var product models.ProductInfo
if err := tx.Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
!product.IsEnabled || product.Status == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
return errors.New("contract product is no longer eligible")
}
productAmount += binding.UnitPrice
@@ -428,7 +429,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
return errors.New("invalid payable amount")
}
order = models.GasorderBasic{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusCreated},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OrderStatus: common.StatusCreated,
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,
@@ -443,7 +444,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
}
for _, binding := range bindings {
item := models.GasorderItem{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusOrdered},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, GasorderContractProductID: binding.ID, ProductInfoID: binding.ProductInfoID,
Active: true,
ProductCode: binding.ProductCode, ProductTypeName: binding.ProductTypeName,
@@ -493,20 +494,20 @@ 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 != common.StatusCreated && order.Status != common.StatusAssigned {
if order.OrderStatus != common.StatusCreated && order.OrderStatus != common.StatusAssigned {
return errors.New("order cannot be assigned")
}
if !validGasorderAssignment(order, delivery, staff) {
return errors.New("delivery or staff does not belong to the order organization")
}
previous := order.Status
previous := order.OrderStatus
if err := tx.Model(&order).Updates(map[string]any{
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "status": common.StatusAssigned,
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "order_status": common.StatusAssigned,
}).Error; err != nil {
return err
}
assignment := models.GasorderAssign{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, GasBasicID: order.GasBasicID, DeliveryBasicID: delivery.ID,
StaffAccountID: staff.ID, AssignerIdentity: operatorIdentity, AssignerName: operatorName,
AssignedAt: time.Now(), Reason: request.Reason,
@@ -523,7 +524,7 @@ func AssignGasorderBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": common.StatusAssigned})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusAssigned})
}
func GasorderStartFilling(ctx *gin.Context) {
@@ -561,14 +562,14 @@ func GasorderComplete(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 != common.StatusAwaitingConfirmation {
if order.OrderStatus != common.StatusAwaitingConfirmation {
return errors.New("order cannot be completed")
}
if err := tx.Model(&order).Update("status", common.StatusCompleted).Error; err != nil {
if err := tx.Model(&order).Update("order_status", common.StatusCompleted).Error; err != nil {
return err
}
confirm := models.GasorderConfirm{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, ConfirmType: request.ConfirmType, RecipientName: request.RecipientName,
RecipientPhone: request.RecipientPhone, ProofURI: request.ProofURI, ConfirmedAt: time.Now(), Remark: request.Remark,
}
@@ -578,13 +579,13 @@ func GasorderComplete(ctx *gin.Context) {
if err := releaseGasorderProducts(tx, order.ID); err != nil {
return err
}
return tx.Create(gasorderStatusRecord(order.ID, order.Status, common.StatusCompleted, request.Reason, operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, order.OrderStatus, common.StatusCompleted, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": common.StatusCompleted})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusCompleted})
}
func GasorderException(ctx *gin.Context) {
@@ -605,11 +606,11 @@ 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 != common.StatusException || order.PreviousStatus == common.StatusDraft {
if order.OrderStatus != common.StatusException || order.PreviousOrderStatus == common.StatusDraft {
return errors.New("order cannot recover")
}
target := order.PreviousStatus
if err := tx.Model(&order).Updates(map[string]any{"status": target, "previous_status": common.StatusDraft}).Error; err != nil {
target := order.PreviousOrderStatus
if err := tx.Model(&order).Updates(map[string]any{"order_status": target, "previous_order_status": common.StatusDraft}).Error; err != nil {
return err
}
return tx.Create(gasorderStatusRecord(order.ID, common.StatusException, target, request.Reason, operatorIdentity, operatorName)).Error
@@ -635,12 +636,12 @@ func transitionGasorder(ctx *gin.Context, target int) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if !gasorderTransitionAllowed(order.Status, target) || (target == common.StatusFilling && order.DeliveryBasicID == 0) {
if !gasorderTransitionAllowed(order.OrderStatus, target) || (target == common.StatusFilling && order.DeliveryBasicID == 0) {
return errors.New("invalid order transition")
}
updates := map[string]any{"status": target}
updates := map[string]any{"order_status": target}
if target == common.StatusException {
updates["previous_status"] = order.Status
updates["previous_order_status"] = order.OrderStatus
}
if err := tx.Model(&order).Updates(updates).Error; err != nil {
return err
@@ -652,7 +653,7 @@ func transitionGasorder(ctx *gin.Context, target int) {
return err
}
track := models.GasorderTrack{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusActive},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, StaffAccountID: order.StaffAccountID,
AttemptNo: attempt + 1, StartedAt: time.Now(),
}
@@ -674,13 +675,13 @@ func transitionGasorder(ctx *gin.Context, target int) {
return err
}
}
return tx.Create(gasorderStatusRecord(order.ID, order.Status, target, request.Reason, operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, order.OrderStatus, target, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": target})
}
func gasorderTransitionAllowed(from, target int) bool {
@@ -734,7 +735,7 @@ func deliveryBelongsToGas(deliveryID, gasID uint64) bool {
func gasorderStatusRecord(orderID uint64, from, to int, reason, operatorIdentity, operatorName string) *models.GasorderStatus {
return &models.GasorderStatus{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: orderID, FromStatus: from, ToStatus: to,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
}

View File

@@ -91,14 +91,14 @@ func TestGasorderStatusRecordIsImmutableSnapshot(t *testing.T) {
if record.GasorderBasicID != 7 || record.FromStatus != common.StatusAssigned || record.ToStatus != common.StatusFilling {
t.Fatalf("unexpected status record: %#v", record)
}
if record.Status != common.StatusRecorded || record.OccurredAt.IsZero() {
if record.Status != common.StatusEnable || 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: common.StatusActive},
Entity: models.Entity{ID: 9, Status: common.StatusEnable}, ContractStatus: common.StatusActive,
}
revision := contractRevision(contract, "renew", "annual renewal", "operator-a", "Operator")
if revision.GasorderContractID != 9 || revision.Action != "renew" || revision.ContractStatus != common.StatusActive {

View File

@@ -10,7 +10,7 @@ import (
type Menu struct {
Identity string `json:"identity"`
ParentIdentity string `json:"parent_identity,omitempty"`
MenuCode string `json:"menu_code"`
GroupCode string `json:"group_code"`
Name string `json:"name"`
Icon string `json:"icon"`
Path string `json:"path"`
@@ -21,79 +21,79 @@ type Menu struct {
// 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: "dashboard", GroupCode: "dashboard", Name: "工作台", Icon: "icon-dashboard", Path: "/dashboard", SortNo: 10, Status: common.StatusEnable},
{Identity: "dashboard_overview", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "数据概览", Path: "/dashboard/overview", SortNo: 1, Status: common.StatusEnable},
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "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: "gas", GroupCode: "gas", Name: "气站管理", Icon: "icon-storage", Path: "/gas", SortNo: 20, Status: common.StatusEnable},
{Identity: "gas_basic", ParentIdentity: "gas", GroupCode: "gas", Name: "气站", Path: "/gas/gas-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "gas_account", ParentIdentity: "gas", GroupCode: "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: "delivery", GroupCode: "delivery", Name: "配送站管理", Icon: "icon-send", Path: "/delivery", SortNo: 30, Status: common.StatusEnable},
{Identity: "delivery_basic", ParentIdentity: "delivery", GroupCode: "delivery", Name: "配送站", Path: "/delivery/delivery-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "delivery_account", ParentIdentity: "delivery", GroupCode: "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: "staff", GroupCode: "staff", Name: "工作人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 40, Status: common.StatusEnable},
{Identity: "staff_account", ParentIdentity: "staff", GroupCode: "staff", Name: "工作人员", Path: "/staff/staff-account", SortNo: 1, Status: common.StatusEnable},
{Identity: "staff_credential", ParentIdentity: "staff", GroupCode: "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: "user", GroupCode: "user", Name: "用户管理", Icon: "icon-user", Path: "/user", SortNo: 50, Status: common.StatusEnable},
{Identity: "user_account", ParentIdentity: "user", GroupCode: "user", Name: "用户账户", Path: "/user/user-account", SortNo: 1, Status: common.StatusEnable},
{Identity: "user_address", ParentIdentity: "user", GroupCode: "user", Name: "用户地址", Path: "/user/user-address", SortNo: 2, Status: common.StatusEnable},
{Identity: "user_service_relation", ParentIdentity: "user", GroupCode: "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: "device", GroupCode: "device", Name: "产品管理", Icon: "icon-common", Path: "/product", SortNo: 60, Status: common.StatusEnable},
{Identity: "product_type", ParentIdentity: "device", GroupCode: "device", Name: "产品类型", Path: "/product/product-type", SortNo: 1, Status: common.StatusEnable},
{Identity: "product_warehouse", ParentIdentity: "device", GroupCode: "device", Name: "库房", Path: "/product/warehouse", SortNo: 2, Status: common.StatusEnable},
{Identity: "product_info", ParentIdentity: "device", GroupCode: "device", Name: "产品信息", Path: "/product/product-info", SortNo: 3, Status: common.StatusEnable},
},
{
{Identity: "gasorder", MenuCode: "gasorder", Name: "气体配送订单管理", Icon: "icon-list", Path: "/gasorder", SortNo: 70, Status: common.StatusEnable},
{Identity: "gasorder_contract", ParentIdentity: "gasorder", MenuCode: "gasorder_contract", Name: "合同管理", Path: "/gasorder/contracts", SortNo: 1, Status: common.StatusEnable},
{Identity: "gasorder_basic", ParentIdentity: "gasorder", MenuCode: "gasorder_basic", Name: "配送订单", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
{Identity: "gasorder_track", ParentIdentity: "gasorder", MenuCode: "gasorder_track", Name: "运行轨迹", Path: "/gasorder/tracks", SortNo: 3, Status: common.StatusEnable},
{Identity: "gasorder", GroupCode: "gasorder", Name: "气体配送订单管理", Icon: "icon-list", Path: "/gasorder", SortNo: 70, Status: common.StatusEnable},
{Identity: "gasorder_contract", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "合同管理", Path: "/gasorder/contracts", SortNo: 1, Status: common.StatusEnable},
{Identity: "gasorder_basic", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "配送订单", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
{Identity: "gasorder_track", ParentIdentity: "gasorder", GroupCode: "gasorder", 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: "ec", GroupCode: "ec", Name: "商城管理", Icon: "icon-gift", Path: "/ec", SortNo: 80, Status: common.StatusEnable},
{Identity: "ec_category", ParentIdentity: "ec", GroupCode: "ec", Name: "商品分类", Path: "/ec/categories", SortNo: 1, Status: common.StatusEnable},
{Identity: "ec_product", ParentIdentity: "ec", GroupCode: "ec", Name: "商品", Path: "/ec/products", SortNo: 2, Status: common.StatusEnable},
{Identity: "ec_cart", ParentIdentity: "ec", GroupCode: "ec", Name: "购物车", Path: "/ec/carts", SortNo: 3, Status: common.StatusEnable},
{Identity: "ec_order", ParentIdentity: "ec", GroupCode: "ec", Name: "商城订单", Path: "/ec/orders", SortNo: 4, Status: common.StatusEnable},
{Identity: "ec_review", ParentIdentity: "ec", GroupCode: "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: "wallet", GroupCode: "wallet", Name: "钱包管理", Icon: "icon-safe", Path: "/wallet", SortNo: 90, Status: common.StatusEnable},
{Identity: "wallet_basic", ParentIdentity: "wallet", GroupCode: "wallet", Name: "钱包", Path: "/wallet/wallet-basic", SortNo: 1, Status: common.StatusEnable},
{Identity: "wallet_bank", ParentIdentity: "wallet", GroupCode: "wallet", Name: "银行卡", Path: "/wallet/banks", SortNo: 2, Status: common.StatusEnable},
{Identity: "wallet_payment", ParentIdentity: "wallet", GroupCode: "wallet", Name: "支付记录", Path: "/wallet/payments", SortNo: 3, Status: common.StatusEnable},
{Identity: "wallet_record", ParentIdentity: "wallet", GroupCode: "wallet", Name: "钱包流水", Path: "/wallet/records", SortNo: 4, Status: common.StatusEnable},
{Identity: "wallet_refund", ParentIdentity: "wallet", GroupCode: "wallet", Name: "退款记录", Path: "/wallet/refunds", SortNo: 5, Status: common.StatusEnable},
{Identity: "wallet_apply_cash", ParentIdentity: "wallet", GroupCode: "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: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 100, Status: common.StatusEnable},
{Identity: "fin_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 1, Status: common.StatusEnable},
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 2, Status: common.StatusEnable},
{Identity: "fin_reconciliation", ParentIdentity: "finance", GroupCode: "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: "content", GroupCode: "content", Name: "内容管理", Icon: "icon-file", Path: "/content", SortNo: 110, Status: common.StatusEnable},
{Identity: "cms_content", ParentIdentity: "content", GroupCode: "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: "customer_service", GroupCode: "customer_service", Name: "客服管理", Icon: "icon-customer-service", Path: "/customer-service", SortNo: 120, Status: common.StatusEnable},
{Identity: "cs_ticket", ParentIdentity: "customer_service", GroupCode: "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},
{Identity: "platform", GroupCode: "platform", Name: "平台管理", Icon: "icon-settings", Path: "/platform", SortNo: 130, Status: common.StatusEnable},
{Identity: "platform_account", ParentIdentity: "platform", GroupCode: "platform", Name: "平台账户", Path: "/platform/accounts", SortNo: 1, Status: common.StatusEnable},
{Identity: "platform_role", ParentIdentity: "platform", GroupCode: "platform", Name: "平台角色", Path: "/platform/roles", SortNo: 2, Status: common.StatusEnable},
{Identity: "platform_menu", ParentIdentity: "platform", GroupCode: "platform", Name: "平台菜单", Path: "/platform/menus", SortNo: 3, Status: common.StatusEnable},
},
}

View File

@@ -16,7 +16,7 @@ func TestPlatformMenusAreStaticTwoDimensionalData(t *testing.T) {
t.Fatalf("first menu in group must be first-level: %#v", parent)
}
for index, menu := range group {
if menu.Identity == "" || menu.MenuCode == "" {
if menu.Identity == "" || menu.GroupCode == "" {
t.Fatalf("menu identity and code are required: %#v", menu)
}
if index > 0 && menu.ParentIdentity != parent.Identity {

View File

@@ -28,3 +28,12 @@ func TestHiddenGasorderResourcesFollowOwningSecondLevelMenu(t *testing.T) {
t.Fatal("order menu granted contract management")
}
}
func TestLocationScopeValuesAreExplicit(t *testing.T) {
if !validLocationScope("standard") || !validLocationScope("precise") {
t.Fatal("supported location scopes were rejected")
}
if validLocationScope("global") || validLocationScope("anything") {
t.Fatal("ambiguous location scope was accepted")
}
}

View File

@@ -27,8 +27,12 @@ func CreatePlatformRole(ctx *gin.Context) {
}
request.Entity = common.NewEntity(common.StatusEnable)
request.IsSystem = false
if request.DataScope == "" {
request.DataScope = "global"
if request.LocationScope == "" {
request.LocationScope = "standard"
}
if !validLocationScope(request.LocationScope) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Create(&request).Error; err != nil {
infra.Response.Error(ctx, err)
@@ -43,10 +47,10 @@ func UpdatePlatformRole(ctx *gin.Context) {
return
}
var request struct {
Name string `json:"name" binding:"required,max=64"`
DataScope string `json:"data_scope" binding:"required,max=32"`
Name string `json:"name" binding:"required,max=64"`
LocationScope string `json:"location_scope" binding:"required,max=32"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
if err := ctx.ShouldBindJSON(&request); err != nil || !validLocationScope(request.LocationScope) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -59,9 +63,11 @@ func UpdatePlatformRole(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "data_scope": request.DataScope}, []string{"name", "data_scope"})
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "location_scope": request.LocationScope}, []string{"name", "location_scope"})
}
func validLocationScope(scope string) bool { return scope == "standard" || scope == "precise" }
// UpdatePlatformRoleStatus 更新非内置平台角色状态,系统角色始终受保护。
func UpdatePlatformRoleStatus(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
@@ -70,7 +76,7 @@ func UpdatePlatformRoleStatus(ctx *gin.Context) {
var request struct {
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsGenericRecordStatus(request.Status) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}

View File

@@ -27,7 +27,7 @@ var productLifecycleStatuses = map[int]bool{
}
func ProductInfoHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
fields := []string{"code", "name", "params", "produced_at", "is_enabled", "status", "action", "reason", "remark"}
fields := []string{"code", "name", "params", "produced_at", "action", "reason", "remark"}
return func(ctx *gin.Context) { common.ListResource(ctx, &models.ProductInfo{}) },
func(ctx *gin.Context) { createProductInfo(ctx, fields, relations) },
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductInfo{}) },
@@ -43,17 +43,11 @@ 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(common.StatusPending), Params: "{}", IsEnabled: false}
data := models.ProductInfo{Entity: common.NewEntity(common.StatusDisable), ProductStatus: common.StatusPending, Params: "{}"}
if err := decodeValues(values, &data); err != nil || data.Code == "" || data.Name == "" || data.ProducedAt.IsZero() || !validProductOwnership(data) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data.Status = common.StatusPending
if data.IsEnabled {
now := time.Now()
data.EnabledAt = &now
data.Status = initialProductStatus(data)
}
operatorIdentity, operatorName := productOperator(ctx)
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&data).Error; err != nil {
@@ -90,36 +84,12 @@ func updateProductInfo(ctx *gin.Context, fields []string, relations []common.Res
operatorIdentity, operatorName := productOperator(ctx)
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
var current models.ProductInfo
if err := tx.Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
return err
}
if _, changingCode := values["code"]; changingCode {
delete(values, "code")
}
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 == common.StatusScrapped {
return errors.New("scrapped product cannot be enabled")
}
if current.EnabledAt == nil {
now := time.Now()
values["enabled_at"] = &now
if _, supplied := values["status"]; !supplied {
preview := current
_ = decodeValues(values, &preview)
values["status"] = initialProductStatus(preview)
}
}
}
if enabled, ok := values["is_enabled"].(bool); ok && !enabled {
values["status"] = common.StatusPending
}
preview := current
if err := decodeValues(values, &preview); err != nil || !validProductOwnership(preview) {
return errors.New("product can have at most one current owner")
@@ -171,23 +141,48 @@ func listProductOwners(ctx *gin.Context) {
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
}
func UpdateProductInfoStatus(ctx *gin.Context) {
func UpdateProductInfoLifecycle(ctx *gin.Context) {
var request struct {
ProductStatus int `json:"product_status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !productLifecycleStatuses[request.ProductStatus] {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
values := gin.H{"product_status": request.ProductStatus}
if request.ProductStatus == common.StatusScrapped {
values["status"] = common.StatusDisable
}
common.UpdateAllowedByIdentity(ctx, &models.ProductInfo{}, values, []string{"product_status", "status"})
}
func UpdateProductInfoRecordStatus(ctx *gin.Context) {
var request struct {
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !productLifecycleStatuses[request.Status] {
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsGenericRecordStatus(request.Status) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var product models.ProductInfo
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&product).Error; err != nil ||
(request.Status == common.StatusEnable && product.ProductStatus == common.StatusScrapped) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
values := gin.H{"status": request.Status}
if request.Status == common.StatusScrapped {
values["is_enabled"] = false
if request.Status == common.StatusEnable && product.EnabledAt == nil {
now := time.Now()
values["enabled_at"] = &now
if product.ProductStatus == common.StatusPending {
values["product_status"] = initialProductStatus(product)
}
}
common.UpdateAllowedByIdentity(ctx, &models.ProductInfo{}, values, []string{"status", "is_enabled"})
common.UpdateAllowedByIdentity(ctx, &models.ProductInfo{}, values, []string{"status", "enabled_at", "product_status"})
}
func ProductRepairHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
fields := []string{"repair_no", "repair_type", "started_at", "completed_at", "result", "target_status", "content", "operator", "remark"}
fields := []string{"repair_no", "repair_type", "started_at", "completed_at", "result", "target_product_status", "content", "operator", "remark"}
return func(ctx *gin.Context) { common.ListResource(ctx, &models.ProductRepair{}) },
func(ctx *gin.Context) { createProductRepair(ctx, fields, relations) },
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductRepair{}) },
@@ -200,7 +195,7 @@ func createProductRepair(ctx *gin.Context, fields []string, relations []common.R
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
data := models.ProductRepair{Entity: common.NewEntity(common.StatusActive), Result: "pending"}
data := models.ProductRepair{Entity: common.NewEntity(common.StatusEnable), 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
@@ -217,7 +212,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", common.StatusRepairing).Error
return tx.Model(&models.ProductInfo{}).Where("id = ?", data.ProductInfoID).Update("product_status", common.StatusRepairing).Error
})
if err != nil {
infra.Response.Error(ctx, err)
@@ -254,6 +249,9 @@ func updateProductRepair(ctx *gin.Context, fields []string, relations []common.R
if err := decodeValues(values, &preview); err != nil || !validRepair(preview) {
return errors.New("invalid repair")
}
if preview.ProductInfoID != current.ProductInfoID {
return errors.New("repair product cannot be changed")
}
if err := tx.Model(&current).Updates(values).Error; err != nil {
return err
}
@@ -271,7 +269,7 @@ func updateProductRepair(ctx *gin.Context, fields []string, relations []common.R
if remaining != 0 {
return nil
}
return tx.Model(&models.ProductInfo{}).Where("id = ?", preview.ProductInfoID).Update("status", preview.TargetStatus).Error
return tx.Model(&models.ProductInfo{}).Where("id = ?", preview.ProductInfoID).Update("product_status", preview.TargetProductStatus).Error
}
return nil
})
@@ -291,7 +289,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 != common.StatusRepairing
productLifecycleStatuses[repair.TargetProductStatus] && repair.TargetProductStatus != common.StatusRepairing
default:
return false
}
@@ -375,7 +373,7 @@ func intValue(value any) (int, bool) {
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: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
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

@@ -56,13 +56,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: common.StatusInStock}) {
if !validRepair(models.ProductRepair{StartedAt: started, CompletedAt: &completed, Result: "passed", TargetProductStatus: common.StatusInStock}) {
t.Fatal("completed repair was rejected")
}
if validRepair(models.ProductRepair{StartedAt: started, Result: "passed", TargetStatus: common.StatusInStock}) {
if validRepair(models.ProductRepair{StartedAt: started, Result: "passed", TargetProductStatus: common.StatusInStock}) {
t.Fatal("completed result without completion time was accepted")
}
if validRepair(models.ProductRepair{StartedAt: completed, CompletedAt: &started, Result: "failed", TargetStatus: common.StatusInStock}) {
if validRepair(models.ProductRepair{StartedAt: completed, CompletedAt: &started, Result: "failed", TargetProductStatus: common.StatusInStock}) {
t.Fatal("completion before start was accepted")
}
}

View File

@@ -82,11 +82,11 @@ func ExpectedResources() []ResourceContract {
resourceContract("staff", "staff_account", Writable, "list"), resourceContract("staff", "staff_credential", Writable, "list"),
resourceContract("user", "user_account", Writable, "list"), resourceContract("user", "user_address", Writable, "list"), resourceContract("user", "user_service_relation", Writable, "list"),
resourceContract("product", "product_type", Editable, "list"), resourceContract("product", "product_warehouse", Editable, "list"), resourceContract("product", "product_info", Editable, "list"), resourceContract("product", "product_repair", Editable, "list"), resourceContract("product", "product_owner", ReadOnly, "list"),
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", Writable, "list"), resourceContract("ec", "ec_order", Writable, "list"), resourceContract("ec", "ec_order_item", Writable, "list"), resourceContract("ec", "ec_review", Writable, "list"),
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", ReadOnly, "list"), resourceContract("ec", "ec_order", ReadOnly, "list"), resourceContract("ec", "ec_order_item", ReadOnly, "list"), resourceContract("ec", "ec_review", ReadOnly, "list"),
resourceContract("gasorder", "gasorder_contract", Managed, "list"), resourceContract("gasorder", "gasorder_contract_product", AppendOnly, "list"), resourceContract("gasorder", "gasorder_contract_revision", ReadOnly, "list"),
resourceContract("gasorder", "gasorder_basic", AppendOnly, "list"), resourceContract("gasorder", "gasorder_item", ReadOnly, "list"), resourceContract("gasorder", "gasorder_assign", ReadOnly, "list"), resourceContract("gasorder", "gasorder_status", ReadOnly, "list"),
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("finance", "fin_payment", ReadOnly, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", ReadOnly, "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", 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

@@ -47,10 +47,18 @@ func CreateStaff(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if !common.ValidateOrganizationIDs(gasBasicID, deliveryBasicID, 0) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
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"
}
if !validWorkStatus(staff.WorkStatus) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Create(&staff).Error; err != nil {
infra.Response.Error(ctx, err)
return
@@ -69,7 +77,7 @@ func UpdateStaff(ctx *gin.Context) {
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
WorkStatus string `json:"work_status" binding:"max=32"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
if err := ctx.ShouldBindJSON(&request); err != nil || !validWorkStatus(request.WorkStatus) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -83,5 +91,11 @@ func UpdateStaff(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if !common.ValidateOrganizationIDs(gasBasicID, deliveryBasicID, 0) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": gasBasicID, "delivery_basic_id": deliveryBasicID, "work_status": request.WorkStatus}, []string{"name", "phone", "avatar", "role_code", "gas_basic_id", "delivery_basic_id", "work_status"})
}
func validWorkStatus(status string) bool { return status == "on_duty" || status == "off_duty" }

View File

@@ -0,0 +1,12 @@
package staff
import "testing"
func TestWorkStatusIsClosedEnumeration(t *testing.T) {
if !validWorkStatus("on_duty") || !validWorkStatus("off_duty") {
t.Fatal("supported work status was rejected")
}
if validWorkStatus("") || validWorkStatus("available") {
t.Fatal("unknown work status was accepted as available")
}
}

View File

@@ -1,6 +1,8 @@
package user
import (
"errors"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
@@ -61,6 +63,9 @@ func UpdateUserAddress(ctx *gin.Context) {
if err := tx.Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
return err
}
if current.UserAccountID != userAccountID {
return errors.New("address owner cannot be changed")
}
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).
Where("user_account_id = ? AND id <> ?", userAccountID, current.ID).
@@ -137,5 +142,10 @@ func resolveServiceRelation(ctx *gin.Context, request serviceRelationRequest) (u
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
if gasBasicID == 0 && deliveryBasicID == 0 && staffAccountID == 0 ||
!common.ValidateOrganizationIDs(gasBasicID, deliveryBasicID, staffAccountID) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
return userAccountID, gasBasicID, deliveryBasicID, staffAccountID, true
}

View File

@@ -227,7 +227,7 @@ func RechargeWalletBasic(ctx *gin.Context) {
}
now := time.Now()
record = models.WalletRecord{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusPosted},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: request.RequestNo,
Direction: "income", TradeType: "recharge", Amount: request.Amount,
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
@@ -285,10 +285,10 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
Where("identity = ?", ctx.Param("identity")).First(&application).Error; err != nil {
return err
}
if application.Status == targetStatus {
if application.ApplyStatus == targetStatus {
return nil
}
if application.Status != common.StatusPending {
if application.ApplyStatus != common.StatusPending {
return errors.New("cash application is not pending")
}
now := time.Now()
@@ -304,7 +304,7 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
}
}
return tx.Model(&application).Updates(map[string]any{
"status": targetStatus, "reviewer_identity": operatorIdentity, "reviewer_name": operatorName,
"apply_status": targetStatus, "reviewer_identity": operatorIdentity, "reviewer_name": operatorName,
"reviewed_at": &now, "review_reason": request.Reason,
}).Error
})
@@ -312,7 +312,7 @@ func reviewWalletApplyCash(ctx *gin.Context, targetStatus int) {
common.RespondRecordError(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": targetStatus})
infra.Response.Success(ctx, gin.H{"updated": true, "apply_status": targetStatus})
}
func dateNumber(value time.Time, layout string) int32 {