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

@@ -22,11 +22,11 @@ const (
// InitPlatformAccess 幂等初始化 root 角色;菜单定义位于逻辑层静态数据中。
func InitPlatformAccess(database *gorm.DB) error {
rootRole := models.PlatformRole{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
RoleCode: PlatformRootRoleCode,
Name: "系统管理员",
DataScope: "global",
IsSystem: true,
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
RoleCode: PlatformRootRoleCode,
Name: "系统管理员",
LocationScope: "precise",
IsSystem: true,
}
return database.Where("role_code = ?", rootRole.RoleCode).FirstOrCreate(&rootRole).Error
}

View File

@@ -22,8 +22,8 @@ func TestInitPlatformAccessSeedsRootRole(t *testing.T) {
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 ORDER BY "platform_role"."id" LIMIT $2`)).
WithArgs("root", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "role_code", "name", "data_scope", "is_system"}).
AddRow(uint64(1), "root-role", 1, "root", "Root", "global", true))
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "role_code", "name", "location_scope", "is_system"}).
AddRow(uint64(1), "root-role", 1, "root", "Root", "precise", true))
if err := InitPlatformAccess(database); err != nil {
t.Fatal(err)

View File

@@ -105,14 +105,14 @@ var keywordSafeColumns = map[string]bool{
"channel": true, "settlement_no": true, "subject_type": true,
"content_type": true, "publish_status": true, "template_code": true,
"ticket_no": true, "category": true, "priority": true,
"platform_role_code": true, "data_scope": true, "menu_code": true,
"platform_role_code": true, "location_scope": true, "group_code": true,
"path": true, "resource_type": true,
"owner_type": true, "owner_identity": true, "payment_no": true,
"record_no": true, "request_no": true, "refund_no": true,
"cash_no": true, "trade_no": true, "trade_type": true,
"pay_channel": true, "payment_type": true,
"contract_no": true, "creator_type": true, "from_status": true,
"to_status": true, "confirm_type": true, "product_type_name": true,
"contract_no": true, "creator_type": true,
"confirm_type": true, "product_type_name": true,
}
func ApplyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
@@ -191,7 +191,7 @@ func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any,
return
}
result := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Updates(values)
result := ActiveRecords(impl.DBService.Model(model)).Where("identity = ?", ctx.Param("identity")).Updates(values)
if result.Error != nil {
infra.Response.Error(ctx, result.Error)
return
@@ -204,7 +204,7 @@ func UpdateAllowedByIdentity(ctx *gin.Context, model any, values map[string]any,
}
func UpdateByIdentity(ctx *gin.Context, model any, values map[string]any) {
result := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Updates(values)
result := ActiveRecords(impl.DBService.Model(model)).Where("identity = ?", ctx.Param("identity")).Updates(values)
if result.Error != nil {
infra.Response.Error(ctx, result.Error)
return

View File

@@ -0,0 +1,31 @@
package common
import (
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
)
// ValidateOrganizationIDs verifies that an optional delivery point and staff
// member belong to the requested gas organization.
func ValidateOrganizationIDs(gasID, deliveryID, staffID uint64) bool {
var delivery models.DeliveryBasic
if deliveryID != 0 {
if err := impl.DBService.First(&delivery, deliveryID).Error; err != nil {
return false
}
if gasID != 0 && delivery.GasBasicID != 0 && delivery.GasBasicID != gasID {
return false
}
}
if staffID == 0 {
return true
}
var staff models.StaffAccount
if err := impl.DBService.First(&staff, staffID).Error; err != nil {
return false
}
if deliveryID != 0 && staff.DeliveryBasicID != deliveryID {
return false
}
return gasID == 0 || staff.GasBasicID == 0 || staff.GasBasicID == gasID
}

View File

@@ -1,13 +1,16 @@
package common
// 公共实体状态。所有嵌入 models.Entity 的模型统一使用这些整数值
// 通用记录状态。仅用于 models.Entity.Status
const (
StatusDraft = 0 // 草稿
StatusEnable = 1 // 启用
StatusDisable = 2 // 停用
StatusArchived = 3 // 已归档
StatusFrozen = 4 // 已冻结
)
// 领域业务状态。必须写入各模型的专用状态字段,禁止写入 Entity.Status。
const (
StatusPending = 10 // 待处理
StatusActive = 11 // 生效中
StatusExpired = 12 // 已过期

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 {

View File

@@ -5,6 +5,7 @@ import "git.apinb.com/bsm-sdk/core/database"
// CsTicket 对应 cs_ticket保存客服工单。
type CsTicket struct {
Entity // 公共实体字段
TicketStatus int `gorm:"column:ticket_status;not null;default:32;index" json:"ticket_status"` // 工单业务状态
TicketNo string `gorm:"column:ticket_no;type:varchar(64);not null;uniqueIndex" json:"ticket_no"` // ticket_no 业务字段
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段
Category string `gorm:"column:category;type:varchar(64);not null" json:"category"` // category 业务字段

View File

@@ -5,6 +5,7 @@ import "git.apinb.com/bsm-sdk/core/database"
// EcOrder 对应 ec_order保存电商订单与组织快照。
type EcOrder struct {
Entity // 公共实体字段
OrderStatus int `gorm:"column:order_status;not null;default:16;index" json:"order_status"` // 商城订单业务状态
OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` // order_no 业务字段
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段
GasStationID uint64 `gorm:"column:gas_station_id;not null;default:0;index" json:"gas_station_id"` // gas_station_id 业务字段

View File

@@ -7,11 +7,12 @@ import (
// FinPayment 对应 fin_payment保存支付与退款记录。
type FinPayment struct {
Entity // 公共实体字段
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段
Amount int64 `gorm:"column:amount;not null;default:0;check:amount > 0" json:"amount"` // amount 业务字段
PaidAt *time.Time `gorm:"column:paid_at;type:timestamptz" json:"paid_at"` // paid_at 业务字段
Entity // 公共实体字段
PaymentStatus int `gorm:"column:payment_status;not null;default:10;index" json:"payment_status"` // 支付业务状态
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段
Amount int64 `gorm:"column:amount;not null;default:0;check:amount > 0" json:"amount"` // amount 业务字段
PaidAt *time.Time `gorm:"column:paid_at;type:timestamptz" json:"paid_at"` // paid_at 业务字段
}
func init() { database.AppendMigrate(&FinPayment{}) }

View File

@@ -7,10 +7,11 @@ import (
// FinReconciliation 对应 fin_reconciliation保存渠道对账记录。
type FinReconciliation struct {
Entity // 公共实体字段
Channel string `gorm:"column:channel;type:varchar(32);not null;index" json:"channel"` // channel 业务字段
BillDate time.Time `gorm:"column:bill_date;type:date;not null" json:"bill_date"` // bill_date 业务字段
DifferenceAmount int64 `gorm:"column:difference_amount;not null;default:0" json:"difference_amount"` // difference_amount 业务字段
Entity // 公共实体字段
ReconciliationStatus int `gorm:"column:reconciliation_status;not null;default:10;index" json:"reconciliation_status"` // 对账业务状态
Channel string `gorm:"column:channel;type:varchar(32);not null;index" json:"channel"` // channel 业务字段
BillDate time.Time `gorm:"column:bill_date;type:date;not null" json:"bill_date"` // bill_date 业务字段
DifferenceAmount int64 `gorm:"column:difference_amount;not null;default:0" json:"difference_amount"` // difference_amount 业务字段
}
func init() { database.AppendMigrate(&FinReconciliation{}) }

View File

@@ -4,30 +4,31 @@ import "git.apinb.com/bsm-sdk/core/database"
// GasorderBasic 对应 gasorder_basic保存气体配送订单当前快照。
type GasorderBasic struct {
Entity // 公共实体字段
OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` // 订单编号
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 创建幂等
GasorderContractID uint64 `gorm:"column:gasorder_contract_id;not null;index" json:"gasorder_contract_id"` // 合同自增主键
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // 服务用户自增主键
CreatorType string `gorm:"column:creator_type;type:varchar(32);not null" json:"creator_type"` // 业务创建方类型
CreatorID uint64 `gorm:"column:creator_id;not null;default:0;index" json:"creator_id"` // 业务创建方自增主键
CreatorIdentity string `gorm:"column:creator_identity;type:varchar(36);not null;index" json:"creator_identity"` // 业务创建方标识
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 履约气站自增主键
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前配送点自增主键
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // 当前配送人员自增主键
Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` // 配送地址快照
Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // 配送经度快照
Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // 配送度快照
ContactName string `gorm:"column:contact_name;type:varchar(64);not null" json:"contact_name"` // 联系人快照
ContactPhone string `gorm:"column:contact_phone;type:varchar(32);not null" json:"contact_phone"` // 联系电话快照
ProductAmount int64 `gorm:"column:product_amount;not null;check:product_amount >= 0" json:"product_amount"` // 商品金额,单位分
DeliveryFee int64 `gorm:"column:delivery_fee;not null;default:0;check:delivery_fee >= 0" json:"delivery_fee"` // 配送费,单位分
DiscountAmount int64 `gorm:"column:discount_amount;not null;default:0;check:discount_amount >= 0" json:"discount_amount"` // 优惠金额,单位分
PayableAmount int64 `gorm:"column:payable_amount;not null;check:payable_amount > 0" json:"payable_amount"` // 应付金额,单位分
PreviousStatus int `gorm:"column:previous_status;not null;default:0" json:"previous_status"` // 异常前状态
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // 建单操作人标识
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 建单操作人姓名快照
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 订单备注
Entity // 公共实体字段
OrderStatus int `gorm:"column:order_status;not null;default:16;index" json:"order_status"` // 订单业务状态
OrderNo string `gorm:"column:order_no;type:varchar(64);not null;uniqueIndex" json:"order_no"` // 订单编
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 创建幂等号
GasorderContractID uint64 `gorm:"column:gasorder_contract_id;not null;index" json:"gasorder_contract_id"` // 合同自增主键
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // 服务用户自增主键
CreatorType string `gorm:"column:creator_type;type:varchar(32);not null" json:"creator_type"` // 业务创建方类型
CreatorID uint64 `gorm:"column:creator_id;not null;default:0;index" json:"creator_id"` // 业务创建方自增主键
CreatorIdentity string `gorm:"column:creator_identity;type:varchar(36);not null;index" json:"creator_identity"` // 业务创建方标识
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;index" json:"gas_basic_id"` // 履约气站自增主键
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前配送自增主键
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // 当前配送人员自增主键
Address string `gorm:"column:address;type:varchar(255);not null" json:"address"` // 配送地址快照
Longitude string `gorm:"column:longitude;type:varchar(32);not null;default:''" json:"longitude"` // 配送度快照
Latitude string `gorm:"column:latitude;type:varchar(32);not null;default:''" json:"latitude"` // 配送纬度快照
ContactName string `gorm:"column:contact_name;type:varchar(64);not null" json:"contact_name"` // 联系快照
ContactPhone string `gorm:"column:contact_phone;type:varchar(32);not null" json:"contact_phone"` // 联系电话快照
ProductAmount int64 `gorm:"column:product_amount;not null;check:product_amount >= 0" json:"product_amount"` // 商品金额,单位分
DeliveryFee int64 `gorm:"column:delivery_fee;not null;default:0;check:delivery_fee >= 0" json:"delivery_fee"` // 配送费,单位分
DiscountAmount int64 `gorm:"column:discount_amount;not null;default:0;check:discount_amount >= 0" json:"discount_amount"` // 优惠金额,单位分
PayableAmount int64 `gorm:"column:payable_amount;not null;check:payable_amount > 0" json:"payable_amount"` // 应付金额,单位分
PreviousOrderStatus int `gorm:"column:previous_order_status;not null;default:0" json:"previous_order_status"` // 异常前订单状态
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:''" json:"operator_identity"` // 建单操作人标识
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 建单操作人姓名快照
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 订单备注
}
func init() { database.AppendMigrate(&GasorderBasic{}) }

View File

@@ -9,17 +9,18 @@ import (
// GasorderContract 对应 gasorder_contract保存用户与气站唯一供气合同。
type GasorderContract struct {
Entity // 公共实体字段
ContractNo string `gorm:"column:contract_no;type:varchar(64);not null;uniqueIndex" json:"contract_no"` // 合同编号
UserAccountID uint64 `gorm:"column:user_account_id;not null;uniqueIndex:idx_gasorder_contract_party" json:"user_account_id"` // 签约用户自增主键
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;uniqueIndex:idx_gasorder_contract_party" json:"gas_basic_id"` // 签约气站自增主键
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 默认配送点自增主键
Title string `gorm:"column:title;type:varchar(255);not null" json:"title"` // 合同标题
Terms string `gorm:"column:terms;type:text;not null;default:''" json:"terms"` // 合同条款
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // 合同文件地址
DefaultDeliveryFee int64 `gorm:"column:default_delivery_fee;not null;default:0;check:default_delivery_fee >= 0" json:"default_delivery_fee"` // 默认配送费,单位分
SignedAt time.Time `gorm:"column:signed_at;type:timestamptz;not null" json:"signed_at"` // 签订时间
EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // 生效时间
ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // 到期时间
ContractStatus int `gorm:"column:contract_status;not null;default:0;index" json:"contract_status"` // 合同业务状态
ContractNo string `gorm:"column:contract_no;type:varchar(64);not null;uniqueIndex" json:"contract_no"` // 合同编号
UserAccountID uint64 `gorm:"column:user_account_id;not null;uniqueIndex:idx_gasorder_contract_party,where:contract_status <> 13" json:"user_account_id"` // 签约用户自增主键
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;uniqueIndex:idx_gasorder_contract_party,where:contract_status <> 13" json:"gas_basic_id"` // 签约气站自增主键
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 默认配送点自增主键
Title string `gorm:"column:title;type:varchar(255);not null" json:"title"` // 合同标题
Terms string `gorm:"column:terms;type:text;not null;default:''" json:"terms"` // 合同条款
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // 合同文件地址
DefaultDeliveryFee int64 `gorm:"column:default_delivery_fee;not null;default:0;check:default_delivery_fee >= 0" json:"default_delivery_fee"` // 默认配送费,单位分
SignedAt time.Time `gorm:"column:signed_at;type:timestamptz;not null" json:"signed_at"` // 签订时间
EffectiveAt time.Time `gorm:"column:effective_at;type:timestamptz;not null" json:"effective_at"` // 生效时间
ExpiredAt *time.Time `gorm:"column:expired_at;type:timestamptz" json:"expired_at"` // 到期时间
}
func init() { database.AppendMigrate(&GasorderContract{}) }

View File

@@ -4,11 +4,11 @@ import "git.apinb.com/bsm-sdk/core/database"
// PlatformRole 对应 platform_role定义平台总后台的数据范围与菜单权限角色。
type PlatformRole struct {
Entity // 公共实体字段
RoleCode string `gorm:"column:role_code;type:varchar(64);not null;uniqueIndex" json:"role_code"` // 角色编码
Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 角色名称
DataScope string `gorm:"column:data_scope;type:varchar(32);not null;default:'global'" json:"data_scope"` // 数据权限范围
IsSystem bool `gorm:"column:is_system;not null;default:false" json:"is_system"` // 是否系统内置角色
Entity // 公共实体字段
RoleCode string `gorm:"column:role_code;type:varchar(64);not null;uniqueIndex" json:"role_code"` // 角色编码
Name string `gorm:"column:name;type:varchar(64);not null" json:"name"` // 角色名称
LocationScope string `gorm:"column:location_scope;type:varchar(32);not null;default:'standard'" json:"location_scope"` // 坐标展示范围
IsSystem bool `gorm:"column:is_system;not null;default:false" json:"is_system"` // 是否系统内置角色
}
func init() { database.AppendMigrate(&PlatformRole{}) }

View File

@@ -9,6 +9,7 @@ import (
// ProductInfo 对应 product_info保存一物一码的实体产品档案。
type ProductInfo struct {
Entity // 公共实体字段
ProductStatus int `gorm:"column:product_status;not null;default:10;index" json:"product_status"` // 产品业务状态
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 产品唯一标识
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 产品名称
ProductTypeID uint64 `gorm:"column:product_type_id;not null;index" json:"product_type_id"` // 产品类型自增主键
@@ -18,7 +19,6 @@ type ProductInfo struct {
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // 当前归属配送站自增主键
UserAccountID uint64 `gorm:"column:user_account_id;not null;default:0;index" json:"user_account_id"` // 当前归属用户自增主键
ProducedAt time.Time `gorm:"column:produced_at;type:timestamptz;not null" json:"produced_at"` // 生产时间
IsEnabled bool `gorm:"column:is_enabled;not null;default:false" json:"is_enabled"` // 是否启用
EnabledAt *time.Time `gorm:"column:enabled_at;type:timestamptz" json:"enabled_at"` // 首次启用时间
}

View File

@@ -8,17 +8,17 @@ import (
// ProductRepair 对应 product_repair保存产品检修过程与结果。
type ProductRepair struct {
Entity // 公共实体字段
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index;uniqueIndex:idx_pending_product_repair,where:result = 'pending'" json:"product_info_id"` // 产品档案自增主键
RepairNo string `gorm:"column:repair_no;type:varchar(64);not null;uniqueIndex" json:"repair_no"` // 检修单号
RepairType string `gorm:"column:repair_type;type:varchar(32);not null" json:"repair_type"` // 检修类型
StartedAt time.Time `gorm:"column:started_at;type:timestamptz;not null" json:"started_at"` // 开始时间
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
Result string `gorm:"column:result;type:varchar(32);not null;default:'pending'" json:"result"` // 检修结果
TargetStatus int `gorm:"column:target_status;not null;default:0" json:"target_status"` // 完成后的产品状态
Content string `gorm:"column:content;type:text;not null;default:''" json:"content"` // 检修内容
Operator string `gorm:"column:operator;type:varchar(64);not null;default:''" json:"operator"` // 检修人员
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
Entity // 公共实体字段
ProductInfoID uint64 `gorm:"column:product_info_id;not null;index;uniqueIndex:idx_pending_product_repair,where:result = 'pending'" json:"product_info_id"` // 产品档案自增主键
RepairNo string `gorm:"column:repair_no;type:varchar(64);not null;uniqueIndex" json:"repair_no"` // 检修单号
RepairType string `gorm:"column:repair_type;type:varchar(32);not null" json:"repair_type"` // 检修类型
StartedAt time.Time `gorm:"column:started_at;type:timestamptz;not null" json:"started_at"` // 开始时间
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
Result string `gorm:"column:result;type:varchar(32);not null;default:'pending'" json:"result"` // 检修结果
TargetProductStatus int `gorm:"column:target_product_status;not null;default:0" json:"target_product_status"` // 完成后的产品状态
Content string `gorm:"column:content;type:text;not null;default:''" json:"content"` // 检修内容
Operator string `gorm:"column:operator;type:varchar(64);not null;default:''" json:"operator"` // 检修人员
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
}
func init() { database.AppendMigrate(&ProductRepair{}) }

View File

@@ -4,13 +4,12 @@ import "git.apinb.com/bsm-sdk/core/database"
// ProductWarehouse 对应 product_warehouse保存独立库房档案。
type ProductWarehouse struct {
Entity // 公共实体字段
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 库房编码
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 库房名称
Address string `gorm:"column:address;type:varchar(255);not null;default:''" json:"address"` // 库房地址
Manager string `gorm:"column:manager;type:varchar(64);not null;default:''" json:"manager"` // 库房负责人
Phone string `gorm:"column:phone;type:varchar(32);not null;default:''" json:"phone"` // 联系电话
IsEnabled bool `gorm:"column:is_enabled;not null;default:false" json:"is_enabled"` // 是否启用
Entity // 公共实体字段
Code string `gorm:"column:code;type:varchar(64);not null;uniqueIndex" json:"code"` // 库房编码
Name string `gorm:"column:name;type:varchar(128);not null" json:"name"` // 库房名称
Address string `gorm:"column:address;type:varchar(255);not null;default:''" json:"address"` // 库房地址
Manager string `gorm:"column:manager;type:varchar(64);not null;default:''" json:"manager"` // 库房负责人
Phone string `gorm:"column:phone;type:varchar(32);not null;default:''" json:"phone"` // 联系电话
}
func init() { database.AppendMigrate(&ProductWarehouse{}) }

View File

@@ -0,0 +1,37 @@
package models
import (
"reflect"
"testing"
)
func TestBusinessModelsUseDedicatedStatusFields(t *testing.T) {
for _, test := range []struct {
model any
field string
}{
{GasorderContract{}, "ContractStatus"},
{GasorderBasic{}, "OrderStatus"},
{ProductInfo{}, "ProductStatus"},
{EcOrder{}, "OrderStatus"},
{WalletPayment{}, "PaymentStatus"},
{WalletRefund{}, "RefundStatus"},
{WalletApplyCash{}, "ApplyStatus"},
{FinPayment{}, "PaymentStatus"},
{FinReconciliation{}, "ReconciliationStatus"},
{CsTicket{}, "TicketStatus"},
} {
if _, ok := reflect.TypeOf(test.model).FieldByName(test.field); !ok {
t.Fatalf("%T is missing dedicated business status field %s", test.model, test.field)
}
}
}
func TestProductEnablementHasSingleSource(t *testing.T) {
if _, ok := reflect.TypeOf(ProductInfo{}).FieldByName("IsEnabled"); ok {
t.Fatal("ProductInfo duplicates Entity.Status with IsEnabled")
}
if _, ok := reflect.TypeOf(ProductWarehouse{}).FieldByName("IsEnabled"); ok {
t.Fatal("ProductWarehouse duplicates Entity.Status with IsEnabled")
}
}

View File

@@ -5,10 +5,10 @@ import "git.apinb.com/bsm-sdk/core/database"
// UserServiceRelation 对应 user_service_relation保存用户服务归属快照。
type UserServiceRelation struct {
Entity // 公共实体字段
UserAccountID uint64 `gorm:"column:user_account_id;not null;index" json:"user_account_id"` // user_account_id 业务字段
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // gas_basic_id 业务字段
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // delivery_basic_id 业务字段
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // staff_account_id 业务字段
UserAccountID uint64 `gorm:"column:user_account_id;not null;index;uniqueIndex:idx_active_user_service,where:status <> 3" json:"user_account_id"` // 用户账户自增主键
GasBasicID uint64 `gorm:"column:gas_basic_id;not null;default:0;index" json:"gas_basic_id"` // gas_basic_id 业务字段
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id;not null;default:0;index" json:"delivery_basic_id"` // delivery_basic_id 业务字段
StaffAccountID uint64 `gorm:"column:staff_account_id;not null;default:0;index" json:"staff_account_id"` // staff_account_id 业务字段
}
func init() { database.AppendMigrate(&UserServiceRelation{}) }

View File

@@ -9,6 +9,7 @@ import (
// WalletApplyCash 对应 wallet_apply_cash保存提现申请及审核结果。
type WalletApplyCash struct {
Entity // 公共实体字段
ApplyStatus int `gorm:"column:apply_status;not null;default:10;index" json:"apply_status"` // 提现申请业务状态
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
WalletBankID uint64 `gorm:"column:wallet_bank_id;not null;default:0;index" json:"wallet_bank_id"` // 银行卡自增主键
CashNo string `gorm:"column:cash_no;type:varchar(64);not null;uniqueIndex" json:"cash_no"` // 内部提现单号

View File

@@ -5,6 +5,7 @@ import "git.apinb.com/bsm-sdk/core/database"
// WalletPayment 对应 wallet_payment保存第三方或余额支付单。
type WalletPayment struct {
Entity // 公共实体字段
PaymentStatus int `gorm:"column:payment_status;not null;default:10;index" json:"payment_status"` // 支付业务状态
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
PaymentNo string `gorm:"column:payment_no;type:varchar(64);not null;uniqueIndex" json:"payment_no"` // 内部支付单号
OrderNo string `gorm:"column:order_no;type:varchar(128);not null;index" json:"order_no"` // 业务订单号

View File

@@ -9,6 +9,7 @@ import (
// WalletRefund 对应 wallet_refund保存支付退款结果。
type WalletRefund struct {
Entity // 公共实体字段
RefundStatus int `gorm:"column:refund_status;not null;default:10;index" json:"refund_status"` // 退款业务状态
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
WalletPaymentID uint64 `gorm:"column:wallet_payment_id;not null;index" json:"wallet_payment_id"` // 原支付记录自增主键
RefundNo string `gorm:"column:refund_no;type:varchar(64);not null;uniqueIndex" json:"refund_no"` // 内部退款单号

View File

@@ -100,7 +100,7 @@ func registerGasorderRoute(group *gin.RouterGroup) {
func registerProductRoute(group *gin.RouterGroup) {
registerRestrictedNoDeleteResource(group, "/product_type", &models.ProductType{}, []string{"code", "name"})
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone", "is_enabled"})
registerRestrictedNoDeleteResource(group, "/product_warehouse", &models.ProductWarehouse{}, []string{"code", "name", "address", "manager", "phone"})
infoRelations := []common.ResourceRelation{
requiredRelation("product_type_identity", "product_type_id", &models.ProductType{}),
@@ -115,7 +115,8 @@ func registerProductRoute(group *gin.RouterGroup) {
info.POST("", infoCreate)
info.GET("/:identity", infoGet)
info.PUT("/:identity", infoUpdate)
info.PATCH("/:identity/status", product.UpdateProductInfoStatus)
info.PATCH("/:identity/status", product.UpdateProductInfoRecordStatus)
info.PATCH("/:identity/lifecycle", product.UpdateProductInfoLifecycle)
repairList, repairCreate, repairGet, repairUpdate := product.ProductRepairHandlers(
requiredRelation("product_info_identity", "product_info_id", &models.ProductInfo{}),
@@ -164,12 +165,10 @@ func registerCommerceRoute(group *gin.RouterGroup) {
registerRestrictedWritableResource(group, "/ec_product", &models.EcProduct{}, []string{"product_code", "name", "price_amount", "stock_quantity"}, requiredRelation("ec_category_identity", "ec_category_id", &models.EcCategory{}))
registerRestrictedWritableResource(group, "/ec_product_attribute", &models.EcProductAttribute{}, []string{"name", "value", "sort_no"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
registerRestrictedWritableResource(group, "/ec_product_image", &models.EcProductImage{}, []string{"image_uri", "sort_no", "is_cover"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
registerRestrictedWritableResource(group, "/ec_cart", &models.EcCart{}, []string{"quantity", "selected"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
orderRelations := []common.ResourceRelation{requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), optionalRelation("gas_basic_identity", "gas_station_id", &models.GasBasic{}), optionalRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{})}
list, create, _, update := common.ResourceHandlers(&models.EcOrder{}, []string{"order_no", "total_amount"}, []string{"total_amount"}, orderRelations...)
registerWritableResource(group, "/ec_order", list, create, ec.GetEcOrder, update, &models.EcOrder{})
registerRestrictedWritableResource(group, "/ec_order_item", &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
registerRestrictedWritableResource(group, "/ec_review", &models.EcReview{}, []string{"score", "content"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
registerReadOnlyResource(group, "/ec_cart", &models.EcCart{})
registerReadOnlyHandlers(group, "/ec_order", func(ctx *gin.Context) { common.ListResource(ctx, &models.EcOrder{}) }, ec.GetEcOrder)
registerReadOnlyResource(group, "/ec_order_item", &models.EcOrderItem{})
registerReadOnlyResource(group, "/ec_review", &models.EcReview{})
}
func registerStaffRoute(group *gin.RouterGroup) {
@@ -206,10 +205,10 @@ func registerPlatformRoute(group *gin.RouterGroup) {
}
func registerFinanceRoute(group *gin.RouterGroup) {
registerRestrictedWritableResource(group, "/fin_payment", &models.FinPayment{}, []string{"channel", "amount", "paid_at"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}))
registerReadOnlyResource(group, "/fin_payment", &models.FinPayment{})
settlementList, settlementCreate, settlementGet, settlementUpdate := fin.FinSettlementHandlers()
registerWritableResource(group, "/fin_settlement", settlementList, settlementCreate, settlementGet, settlementUpdate, &models.FinSettlement{})
registerRestrictedWritableResource(group, "/fin_reconciliation", &models.FinReconciliation{}, []string{"channel", "bill_date", "difference_amount"})
registerReadOnlyResource(group, "/fin_reconciliation", &models.FinReconciliation{})
}

View File

@@ -115,12 +115,19 @@ func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing
}
for _, resource := range []string{
"/ec_category", "/ec_product", "/ec_product_attribute", "/ec_product_image", "/ec_cart", "/ec_order", "/ec_order_item", "/ec_review",
"/ec_category", "/ec_product", "/ec_product_attribute", "/ec_product_image",
} {
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch)
}
for _, resource := range []string{"/ec_cart", "/ec_order", "/ec_order_item", "/ec_review"} {
path := "/heqi/platform/v1" + resource
assertRouteMethods(t, routes, path, http.MethodGet)
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet)
assertNoRouteMethods(t, routes, path, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
for _, resource := range []string{"/gasorder_item", "/gasorder_assign", "/gasorder_status", "/gasorder_track", "/gasorder_track_point", "/gasorder_confirm", "/gasorder_payment", "/gasorder_contract_revision"} {
path := "/heqi/platform/v1" + resource
@@ -177,13 +184,19 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
}
for _, resource := range []string{
"/fin_payment", "/fin_settlement", "/fin_reconciliation",
"/cms_content", "/cs_ticket",
"/fin_settlement", "/cms_content", "/cs_ticket",
} {
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity/status", http.MethodPatch)
}
for _, resource := range []string{"/fin_payment", "/fin_reconciliation"} {
path := "/heqi/platform/v1" + resource
assertRouteMethods(t, routes, path, http.MethodGet)
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet)
assertNoRouteMethods(t, routes, path, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
for _, resource := range []string{
"/wallet_basic", "/wallet_bank", "/wallet_payment", "/wallet_record", "/wallet_refund", "/wallet_apply_cash",

View File

@@ -113,7 +113,7 @@ func MockData(database *gorm.DB) error {
warehouse := models.ProductWarehouse{
Entity: entity(11, common.StatusEnable), Code: "MOCK-WH-001", Name: "示例中心库房",
Address: gas.Address, Manager: "赵库管", Phone: "13700000001", IsEnabled: true,
Address: gas.Address, Manager: "赵库管", Phone: "13700000001",
}
if err := put(tx, &warehouse); err != nil {
return err
@@ -122,9 +122,9 @@ func MockData(database *gorm.DB) error {
enabledAt := yesterday
productInfo := models.ProductInfo{
Entity: entity(12, common.StatusEnable), Code: "MOCK-CYLINDER-001", Name: "示例液化气钢瓶",
ProductStatus: common.StatusInStock,
ProductTypeID: productType.ID, Params: `{"weight":"15kg","medium":"LPG"}`,
WarehouseID: warehouse.ID, GasBasicID: gas.ID, ProducedAt: now.AddDate(-1, 0, 0),
IsEnabled: true, EnabledAt: &enabledAt,
WarehouseID: warehouse.ID, ProducedAt: now.AddDate(-1, 0, 0), EnabledAt: &enabledAt,
}
if err := put(tx, &productInfo); err != nil {
return err
@@ -132,7 +132,7 @@ func MockData(database *gorm.DB) error {
productOwner := models.ProductOwner{
Entity: entity(13, common.StatusEnable), ProductInfoID: productInfo.ID,
WarehouseID: warehouse.ID, GasBasicID: gas.ID, Action: "stock_in",
WarehouseID: warehouse.ID, Action: "stock_in",
OccurredAt: yesterday, Reason: "模拟数据初始化", OperatorName: "系统",
}
if err := put(tx, &productOwner); err != nil {
@@ -141,9 +141,9 @@ func MockData(database *gorm.DB) error {
completedAt := yesterday.Add(2 * time.Hour)
productRepair := models.ProductRepair{
Entity: entity(14, common.StatusCompleted), ProductInfoID: productInfo.ID,
Entity: entity(14, common.StatusEnable), ProductInfoID: productInfo.ID,
RepairNo: "MOCK-REPAIR-001", RepairType: "inspection", StartedAt: yesterday,
CompletedAt: &completedAt, Result: "passed", TargetStatus: common.StatusEnable,
CompletedAt: &completedAt, Result: "passed", TargetProductStatus: common.StatusInStock,
Content: "外观、阀门与气密性检查", Operator: staff.Name,
}
if err := put(tx, &productRepair); err != nil {
@@ -151,7 +151,7 @@ func MockData(database *gorm.DB) error {
}
contract := models.GasorderContract{
Entity: entity(15, common.StatusActive), ContractNo: "MOCK-CONTRACT-001",
Entity: entity(15, common.StatusEnable), ContractStatus: common.StatusActive, ContractNo: "MOCK-CONTRACT-001",
UserAccountID: user.ID, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID,
Title: "居民瓶装气配送示例合同", Terms: "按需配送,安全使用。",
DefaultDeliveryFee: 500, SignedAt: yesterday, EffectiveAt: yesterday, ExpiredAt: &nextYear,
@@ -161,7 +161,7 @@ func MockData(database *gorm.DB) error {
}
contractRevision := models.GasorderContractRevision{
Entity: entity(16, common.StatusActive), GasorderContractID: contract.ID, Action: "activate",
Entity: entity(16, common.StatusEnable), GasorderContractID: contract.ID, Action: "activate",
ContractStatus: common.StatusActive, EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt,
OperatorIdentity: gasAccount.Identity, OperatorName: gasAccount.DisplayName,
OccurredAt: yesterday, Reason: "模拟合同启用",
@@ -171,7 +171,7 @@ func MockData(database *gorm.DB) error {
}
contractProduct := models.GasorderContractProduct{
Entity: entity(17, common.StatusActive), GasorderContractID: contract.ID,
Entity: entity(17, common.StatusEnable), GasorderContractID: contract.ID,
ProductInfoID: productInfo.ID, ProductCode: productInfo.Code,
ProductTypeName: productType.Name, ProductParams: productInfo.Params,
UnitPrice: 9800, BoundAt: yesterday,
@@ -181,7 +181,7 @@ func MockData(database *gorm.DB) error {
}
gasOrder := models.GasorderBasic{
Entity: entity(18, common.StatusCompleted), OrderNo: "MOCK-GASORDER-001",
Entity: entity(18, common.StatusEnable), OrderStatus: common.StatusCompleted, OrderNo: "MOCK-GASORDER-001",
RequestNo: "MOCK-REQ-GASORDER-001", GasorderContractID: contract.ID,
UserAccountID: user.ID, CreatorType: "user", CreatorID: user.ID,
CreatorIdentity: user.Identity, GasBasicID: gas.ID, DeliveryBasicID: delivery.ID,
@@ -195,7 +195,7 @@ func MockData(database *gorm.DB) error {
}
gasOrderItem := models.GasorderItem{
Entity: entity(19, common.StatusCompleted), GasorderBasicID: gasOrder.ID,
Entity: entity(19, common.StatusEnable), GasorderBasicID: gasOrder.ID,
GasorderContractProductID: contractProduct.ID, ProductInfoID: productInfo.ID,
ProductCode: productInfo.Code, ProductTypeName: productType.Name,
ProductParams: productInfo.Params, UnitPrice: contractProduct.UnitPrice,
@@ -205,7 +205,7 @@ func MockData(database *gorm.DB) error {
}
assignment := models.GasorderAssign{
Entity: entity(20, common.StatusCompleted), GasorderBasicID: gasOrder.ID, GasBasicID: gas.ID,
Entity: entity(20, common.StatusEnable), GasorderBasicID: gasOrder.ID, GasBasicID: gas.ID,
DeliveryBasicID: delivery.ID, StaffAccountID: staff.ID,
AssignerIdentity: deliveryAccount.Identity, AssignerName: deliveryAccount.DisplayName,
AssignedAt: now.Add(-2 * time.Hour), Reason: "系统示例派单",
@@ -215,7 +215,7 @@ func MockData(database *gorm.DB) error {
}
orderStatus := models.GasorderStatus{
Entity: entity(21, common.StatusCompleted), GasorderBasicID: gasOrder.ID,
Entity: entity(21, common.StatusEnable), GasorderBasicID: gasOrder.ID,
FromStatus: common.StatusDelivering, ToStatus: common.StatusCompleted,
OperatorIdentity: staff.Identity, OperatorName: staff.Name,
OccurredAt: now, Reason: "用户已签收",
@@ -226,7 +226,7 @@ func MockData(database *gorm.DB) error {
trackCompletedAt := now.Add(-10 * time.Minute)
track := models.GasorderTrack{
Entity: entity(22, common.StatusCompleted), GasorderBasicID: gasOrder.ID,
Entity: entity(22, common.StatusEnable), GasorderBasicID: gasOrder.ID,
StaffAccountID: staff.ID, AttemptNo: 1, StartedAt: now.Add(-90 * time.Minute),
CompletedAt: &trackCompletedAt,
}
@@ -235,7 +235,7 @@ func MockData(database *gorm.DB) error {
}
trackPoint := models.GasorderTrackPoint{
Entity: entity(23, common.StatusCompleted), GasorderTrackID: track.ID,
Entity: entity(23, common.StatusEnable), GasorderTrackID: track.ID,
Longitude: address.Longitude, Latitude: address.Latitude,
OccurredAt: trackCompletedAt, Source: "gps", Accuracy: "10m",
}
@@ -244,7 +244,7 @@ func MockData(database *gorm.DB) error {
}
confirmation := models.GasorderConfirm{
Entity: entity(24, common.StatusCompleted), GasorderBasicID: gasOrder.ID,
Entity: entity(24, common.StatusEnable), GasorderBasicID: gasOrder.ID,
ConfirmType: "signature", RecipientName: user.Name, RecipientPhone: user.Phone,
ProofURI: "/mock/proofs/gasorder-001.png", ConfirmedAt: now, Remark: "模拟签收",
}
@@ -293,7 +293,7 @@ func MockData(database *gorm.DB) error {
}
ecOrder := models.EcOrder{
Entity: entity(30, common.StatusPaid), OrderNo: "MOCK-ECORDER-001",
Entity: entity(30, common.StatusEnable), OrderStatus: common.StatusPaid, OrderNo: "MOCK-ECORDER-001",
UserAccountID: user.ID, GasStationID: gas.ID, DeliveryPointID: delivery.ID,
TotalAmount: ecProduct.PriceAmount,
}
@@ -302,7 +302,7 @@ func MockData(database *gorm.DB) error {
}
ecOrderItem := models.EcOrderItem{
Entity: entity(31, common.StatusPaid), EcOrderID: ecOrder.ID, EcProductID: ecProduct.ID,
Entity: entity(31, common.StatusEnable), EcOrderID: ecOrder.ID, EcProductID: ecProduct.ID,
ProductSnapshot: `{"code":"MOCK-EC-LPG-001","name":"15kg 液化气配送服务"}`,
Quantity: 1, SaleAmount: ecProduct.PriceAmount,
}
@@ -311,7 +311,7 @@ func MockData(database *gorm.DB) error {
}
review := models.EcReview{
Entity: entity(32, common.StatusPublished), EcOrderID: ecOrder.ID,
Entity: entity(32, common.StatusEnable), EcOrderID: ecOrder.ID,
EcProductID: ecProduct.ID, UserAccountID: user.ID,
Score: 5, Content: "配送及时,服务规范。",
}
@@ -341,7 +341,7 @@ func MockData(database *gorm.DB) error {
}
walletPayment := models.WalletPayment{
Entity: entity(35, common.StatusSuccess), WalletBasicID: wallet.ID,
Entity: entity(35, common.StatusEnable), PaymentStatus: common.StatusSuccess, WalletBasicID: wallet.ID,
PaymentNo: "MOCK-PAYMENT-001", OrderNo: gasOrder.OrderNo,
TradeNo: "MOCK-TRADE-001", PaymentType: "gasorder",
PayChannel: "balance", PayType: "wallet", Amount: gasOrder.PayableAmount,
@@ -352,7 +352,7 @@ func MockData(database *gorm.DB) error {
}
walletRecord := models.WalletRecord{
Entity: entity(36, common.StatusCompleted), WalletBasicID: wallet.ID,
Entity: entity(36, common.StatusEnable), WalletBasicID: wallet.ID,
RecordNo: "MOCK-RECORD-001", RequestNo: "MOCK-REQ-RECORD-001",
Direction: "in", TradeType: "recharge", Amount: 50000,
BalanceAfter: 50000, WithdrawalBalanceAfter: 30000,
@@ -366,7 +366,7 @@ func MockData(database *gorm.DB) error {
refundCompletedAt := now
refund := models.WalletRefund{
Entity: entity(37, common.StatusCompleted), WalletBasicID: wallet.ID,
Entity: entity(37, common.StatusEnable), RefundStatus: common.StatusCompleted, WalletBasicID: wallet.ID,
WalletPaymentID: walletPayment.ID, RefundNo: "MOCK-REFUND-001",
OrderIdentity: gasOrder.Identity, Amount: 1000, Reason: "模拟部分退款",
OrderInfo: `{"order_no":"MOCK-GASORDER-001"}`, Result: `{"status":"success"}`,
@@ -377,7 +377,7 @@ func MockData(database *gorm.DB) error {
}
applyCash := models.WalletApplyCash{
Entity: entity(38, common.StatusApproved), WalletBasicID: wallet.ID, WalletBankID: bank.ID,
Entity: entity(38, common.StatusEnable), ApplyStatus: common.StatusApproved, WalletBasicID: wallet.ID, WalletBankID: bank.ID,
CashNo: "MOCK-CASH-001", RequestNo: "MOCK-REQ-CASH-001", Amount: 5000,
Channel: "bank", TradeNo: "MOCK-CASH-TRADE-001", Remark: "模拟提现",
ReviewerIdentity: gasAccount.Identity, ReviewerName: gasAccount.DisplayName,
@@ -388,7 +388,7 @@ func MockData(database *gorm.DB) error {
}
gasOrderPayment := models.GasorderPayment{
Entity: entity(39, common.StatusSuccess), GasorderBasicID: gasOrder.ID,
Entity: entity(39, common.StatusEnable), GasorderBasicID: gasOrder.ID,
WalletPaymentID: walletPayment.ID, AttemptNo: 1, Amount: gasOrder.PayableAmount,
}
if err := put(tx, &gasOrderPayment); err != nil {
@@ -396,7 +396,7 @@ func MockData(database *gorm.DB) error {
}
finPayment := models.FinPayment{
Entity: entity(40, common.StatusPaid), EcOrderID: ecOrder.ID,
Entity: entity(40, common.StatusEnable), PaymentStatus: common.StatusPaid, EcOrderID: ecOrder.ID,
Channel: "wallet", Amount: ecOrder.TotalAmount, PaidAt: &now,
}
if err := put(tx, &finPayment); err != nil {
@@ -404,7 +404,7 @@ func MockData(database *gorm.DB) error {
}
settlement := models.FinSettlement{
Entity: entity(41, common.StatusCompleted), SettlementNo: "MOCK-SETTLEMENT-001",
Entity: entity(41, common.StatusEnable), SettlementNo: "MOCK-SETTLEMENT-001",
SubjectType: "gas", SubjectID: gas.ID,
PeriodStart: now.AddDate(0, 0, -30), PeriodEnd: now,
}
@@ -413,7 +413,7 @@ func MockData(database *gorm.DB) error {
}
reconciliation := models.FinReconciliation{
Entity: entity(42, common.StatusMatched), Channel: "wallet",
Entity: entity(42, common.StatusEnable), ReconciliationStatus: common.StatusMatched, Channel: "wallet",
BillDate: now, DifferenceAmount: 0,
}
if err := put(tx, &reconciliation); err != nil {
@@ -430,7 +430,7 @@ func MockData(database *gorm.DB) error {
}
ticket := models.CsTicket{
Entity: entity(44, common.StatusOpen), TicketNo: "MOCK-TICKET-001",
Entity: entity(44, common.StatusEnable), TicketStatus: common.StatusOpen, TicketNo: "MOCK-TICKET-001",
UserAccountID: user.ID, Category: "delivery", Priority: "normal",
}
if err := put(tx, &ticket); err != nil {
@@ -439,7 +439,7 @@ func MockData(database *gorm.DB) error {
role := models.PlatformRole{
Entity: entity(45, common.StatusEnable), RoleCode: "mock_operator",
Name: "模拟运营人员", DataScope: "global", IsSystem: false,
Name: "模拟运营人员", LocationScope: "standard", IsSystem: false,
}
if err := put(tx, &role); err != nil {
return err