refactor(platform): remove deprecated reporting and audit modules

This commit is contained in:
2026-07-27 15:31:22 +08:00
parent e88734c0fd
commit a98fdd0fca
42 changed files with 86 additions and 674 deletions

View File

@@ -42,11 +42,8 @@ func InitPlatformAccess(database *gorm.DB) error {
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "ec", Name: "电商管理", Icon: "icon-shopping", Path: "/ec/product", SortNo: 80},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "finance", Name: "财务管理", Icon: "icon-safe", Path: "/finance/payment", SortNo: 90},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "wallet", Name: "钱包中心", Icon: "icon-wallet", Path: "/wallet/list", SortNo: 100},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "report", Name: "统计报表", Icon: "icon-bar-chart", Path: "/report/list", SortNo: 110},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "content", Name: "内容管理", Icon: "icon-file", Path: "/content", SortNo: 120},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "notification", Name: "通知管理", Icon: "icon-notification", Path: "/notification", SortNo: 130},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "customer_service", Name: "客户服务", Icon: "icon-customer-service", Path: "/customer_service", SortNo: 140},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "audit", Name: "审计合规", Icon: "icon-history", Path: "/audit", SortNo: 150},
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "platform", Name: "平台配置", Icon: "icon-settings", Path: "/platform/account", SortNo: 160},
}
for index := range menus {

View File

@@ -27,8 +27,7 @@ func TestInitPlatformAccessSeedsEveryProtectedFrontendDomain(t *testing.T) {
domains := []string{
"dashboard", "gas", "delivery", "staff", "user", "device", "safety",
"ec", "finance", "wallet", "report", "content", "notification",
"customer_service", "audit", "platform",
"ec", "finance", "wallet", "content", "customer_service", "platform",
}
for index, domain := range domains {
menuID := uint64(index + 10)

View File

@@ -1,103 +0,0 @@
package platform
import (
"encoding/json"
"errors"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/bsm-sdk/core/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var errApprovalNotProcessable = errors.New("approval is not processable")
func approvalValues(status, opinion, operatorIdentity string) map[string]any {
return map[string]any{
"status": status,
"opinion": opinion,
"handler_identity": operatorIdentity,
"handled_at": time.Now().UTC(),
}
}
// ApproveAudit records the reviewer and decision without allowing an approval
// to mutate any business fields. The operation audit is created atomically with
// the approval update.
func ApproveAudit(ctx *gin.Context) {
claims, err := middleware.ParseAuth(ctx)
if err != nil {
infra.Response.Error(ctx, err)
return
}
var request struct {
Status string `json:"status" binding:"required,max=32"`
Opinion string `json:"opinion" binding:"max=2000"`
}
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if request.Status != "approved" && request.Status != "rejected" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
values := approvalValues(request.Status, request.Opinion, claims.Identity)
var approval models.AuditApproval
err = impl.DBService.Transaction(func(transaction *gorm.DB) error {
if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&approval).Error; err != nil {
return err
}
if approval.Status != "pending" || approval.ApplicantIdentity == claims.Identity {
return errApprovalNotProcessable
}
before, err := json.Marshal(gin.H{
"status": approval.Status, "opinion": approval.Opinion,
"handler_identity": approval.HandlerIdentity, "handled_at": approval.HandledAt,
})
if err != nil {
return err
}
if result := transaction.Model(&models.AuditApproval{}).Where("identity = ? AND status = ?", approval.Identity, "pending").Updates(values); result.Error != nil {
return result.Error
} else if result.RowsAffected == 0 {
return errApprovalNotProcessable
}
after, err := json.Marshal(values)
if err != nil {
return err
}
return transaction.Create(&models.AuditOperationLog{
Entity: newEntity("enabled"),
OperatorIdentity: claims.Identity,
Action: "approve",
ObjectType: "audit_approval",
ObjectIdentity: approval.Identity,
BeforeData: string(before),
AfterData: string(after),
}).Error
})
if err != nil {
if errors.Is(err, errApprovalNotProcessable) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if errors.Is(err, gorm.ErrRecordNotFound) {
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
return
}
infra.Response.Error(ctx, err)
return
}
approval.Status = request.Status
approval.Opinion = request.Opinion
approval.HandlerIdentity = claims.Identity
handledAt := values["handled_at"].(time.Time)
approval.HandledAt = &handledAt
infra.Response.Success(ctx, resourceResponse(approval))
}

View File

@@ -1,135 +0,0 @@
package platform
import (
"database/sql/driver"
"net/http"
"regexp"
"strings"
"testing"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/types"
"github.com/DATA-DOG/go-sqlmock"
"google.golang.org/grpc/status"
)
func TestApprovalValuesOnlyChangesApprovalFieldsAndRecordsOperator(t *testing.T) {
values := approvalValues("approved", "accepted", "operator-a")
if values["status"] != "approved" || values["opinion"] != "accepted" || values["handler_identity"] != "operator-a" {
t.Fatalf("approval values do not retain the approved state, opinion, and operator: %#v", values)
}
if _, ok := values["handled_at"]; !ok {
t.Fatalf("approval values do not record handling time: %#v", values)
}
if len(values) != 4 {
t.Fatalf("approval update includes fields outside its whitelist: %#v", values)
}
}
func TestApproveAuditOnlyUpdatesApprovalFieldsAndAppendsOperationAudit(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
now := time.Now().UTC()
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "business_type", "business_identity", "applicant_identity", "opinion", "handler_identity", "handled_at"}).
AddRow(uint64(1), "approval-a", now, now, "pending", 1, "refund", "payment-a", "applicant-a", "", "", nil))
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
WithArgs(sqlmock.AnyArg(), "operator-a", "accepted", "approved", sqlmock.AnyArg(), "approval-a", "pending").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "audit_operation_log" ("identity","created_at","updated_at","status","version","operator_identity","action","object_type","object_identity","before_data","after_data") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING "id"`)).
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "operator-a", "approve", "audit_approval", "approval-a", jsonContaining(`"status":"pending"`), jsonContaining(`"handler_identity":"operator-a"`)).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
mock.ExpectCommit()
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved","opinion":"accepted","business_identity":"payment-b"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
assertMockExpectations(t, mock)
assertResponseCode(t, recorder, 0)
if !strings.Contains(recorder.Body.String(), `"handler_identity":"operator-a"`) {
t.Fatalf("approval response omitted its handler: %s", recorder.Body.String())
}
}
func TestApproveAuditRejectsStatusesOutsideApprovedAndRejected(t *testing.T) {
for _, decision := range []string{"pending", "archived"} {
t.Run(decision, func(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"`+decision+`"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
assertMockExpectations(t, mock)
})
}
}
func TestApproveAuditRejectsAlreadyHandledApproval(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "approved", "applicant-a"))
mock.ExpectRollback()
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
assertMockExpectations(t, mock)
}
func TestApproveAuditRejectsTheApplicant(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "pending", "operator-a"))
mock.ExpectRollback()
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
assertMockExpectations(t, mock)
}
func TestApproveAuditRejectsAConcurrentSecondDecision(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
WithArgs("approval-a", 1).
WillReturnRows(approvalRows("approval-a", "pending", "applicant-a"))
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
WithArgs(sqlmock.AnyArg(), "operator-a", "", "approved", sqlmock.AnyArg(), "approval-a", "pending").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectRollback()
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
ApproveAudit(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
assertMockExpectations(t, mock)
}
func approvalRows(identity, approvalStatus, applicant string) *sqlmock.Rows {
now := time.Now().UTC()
return sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "business_type", "business_identity", "applicant_identity", "opinion", "handler_identity", "handled_at"}).
AddRow(uint64(1), identity, now, now, approvalStatus, 1, "refund", "payment-a", applicant, "", "", nil)
}
type jsonContaining string
func (expected jsonContaining) Match(value driver.Value) bool {
actual, ok := value.(string)
return ok && strings.Contains(actual, string(expected))
}

View File

@@ -69,9 +69,7 @@ var keywordSafeColumns = map[string]bool{
"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,
"path": true, "report_code": true, "report_type": true,
"stat_period": true, "dimension": true, "metric_code": true,
"scope_type": true, "business_type": true, "resource_type": true,
"path": true, "resource_type": true,
}
func applyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
@@ -100,7 +98,7 @@ func keywordColumns(model any) []string {
columns := make([]string, 0)
for index := 0; index < modelType.NumField(); index++ {
field := modelType.Field(index)
if field.Anonymous || field.Type.Kind() != reflect.String || strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
if field.Anonymous || field.Type.Kind() != reflect.String {
continue
}
column := gormColumn(field.Tag.Get("gorm"))

View File

@@ -79,11 +79,9 @@ func ExpectedResources() []ResourceContract {
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("delivery", "delivery_task", Writable, "list"), resourceContract("delivery", "delivery_track", Writable, "list"), resourceContract("delivery", "delivery_track_point", ReadOnly, "list"),
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
resourceContract("content", "cnt_content", Writable, "list"), resourceContract("notification", "ntf_template", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
resourceContract("wallet", "wallet", ReadOnly, "list"), resourceContract("wallet", "wallet_ledger", ReadOnly, "list"), resourceContract("wallet", "wallet_recharge", ReadOnly, "list"), resourceContract("wallet", "wallet_withdrawal", ReadOnly, "list"),
resourceContract("report", "report", ReadOnly, "list"), resourceContract("report", "report_item", ReadOnly, "list"), resourceContract("report", "report_metric_snapshot", ReadOnly, "list"),
resourceContract("audit", "audit_operation_log", ReadOnly, "list"), resourceContract("audit", "audit_export_log", ReadOnly, "list"), resourceContract("audit", "audit_approval", ReadOnly, "list"),
}
}

View File

@@ -30,21 +30,27 @@ func TestExpectedResources(t *testing.T) {
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
}
func TestSafeAndAuditResourceContracts(t *testing.T) {
func TestContentUsesCMSResourceAndRemovedDomainsAreAbsent(t *testing.T) {
resources := ExpectedResources()
assertContract(t, resources, "content", "cms_content", Writable, "list")
for _, removed := range []string{"ntf_template", "report", "report_item", "report_metric_snapshot", "audit_operation_log", "audit_export_log", "audit_approval"} {
for _, resource := range resources {
if resource.Name == removed {
t.Fatalf("removed resource %s remains registered", removed)
}
}
}
}
func TestSafeResourceContracts(t *testing.T) {
legacySafetyPrefix := "sa" + "f_"
legacyAuditPrefix := "au" + "d_"
assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list")
assertContract(t, ExpectedResources(), "safety", "safe_event_disposal", AppendOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_operation_log", ReadOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_export_log", ReadOnly, "list")
assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list")
for _, contract := range ExpectedResources() {
if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix)) ||
(contract.Domain == "audit" && strings.HasPrefix(contract.Name, legacyAuditPrefix)) {
if contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix) {
t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name)
}
}
@@ -251,15 +257,15 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid
}
}
func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
func TestPrepareResourceValuesAcceptsArbitraryTextFields(t *testing.T) {
t.Run("safety rule", func(t *testing.T) {
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve","gray_scope":"north region"}`))
values, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
if err != nil {
t.Fatal(err)
}
if values["threshold"] != `{"max":10}` || values["gray_scope"] != `["north"]` {
t.Fatalf("jsonb values were not normalized to strings: %#v", values)
if values["threshold"] != `{invalid}` || values["gray_scope"] != `north region` {
t.Fatalf("text values were not preserved: %#v", values)
}
})
@@ -272,7 +278,7 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
WithArgs("product-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","product_snapshot":{"name":"液化气"},"quantity":2,"sale_amount":500}`))
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","product_snapshot":"arbitrary product snapshot text","quantity":2,"sale_amount":500}`))
values, err := prepareResourceValues(ctx, &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, []ResourceRelation{
{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true},
{Input: "ec_product_identity", Column: "ec_product_id", Model: &models.EcProduct{}, Required: true},
@@ -280,16 +286,16 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if values["product_snapshot"] != `{"name":"液化气"}` {
if values["product_snapshot"] != `arbitrary product snapshot text` {
t.Fatalf("product snapshot was not normalized to a string: %#v", values)
}
assertMockExpectations(t, mock)
})
t.Run("invalid json string", func(t *testing.T) {
t.Run("arbitrary text", func(t *testing.T) {
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
t.Fatal("invalid JSON string was accepted for a string/jsonb field")
if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err != nil {
t.Fatalf("arbitrary text was rejected: %v", err)
}
})
}
@@ -344,7 +350,7 @@ func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) {
model any
want []string
}{
{"safety rule excludes jsonb", &models.SafeRule{}, []string{"rule_code", "action"}},
{"safety rule excludes arbitrary text fields", &models.SafeRule{}, []string{"rule_code", "action"}},
{"gas basic excludes sensitive fields", &models.GasBasic{}, []string{"code", "name"}},
{"user address has no searchable safe text", &models.UserAddress{}, []string{}},
}

View File

@@ -266,7 +266,7 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
return
}
values, err := resolveResourceRelations(input, allowedFields, relations, false)
if err != nil || normalizeStringJSONBFields(model, values) != nil {
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -283,42 +283,12 @@ func prepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
return nil, errors.New("invalid resource payload")
}
values, err := resolveResourceRelations(input, allowedFields, relations, true)
if err != nil || normalizeStringJSONBFields(model, values) != nil || len(values) == 0 {
if err != nil || len(values) == 0 {
return nil, errors.New("invalid resource payload")
}
return values, nil
}
func normalizeStringJSONBFields(model any, values map[string]any) error {
modelType := reflect.TypeOf(model)
for modelType.Kind() == reflect.Pointer {
modelType = modelType.Elem()
}
for index := 0; index < modelType.NumField(); index++ {
field := modelType.Field(index)
if field.Type.Kind() != reflect.String || !strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
continue
}
column := gormColumn(field.Tag.Get("gorm"))
value, exists := values[column]
if !exists {
continue
}
if text, ok := value.(string); ok {
if !json.Valid([]byte(text)) {
return errors.New("invalid jsonb string")
}
continue
}
encoded, err := json.Marshal(value)
if err != nil || !json.Valid(encoded) {
return errors.New("invalid jsonb value")
}
values[column] = string(encoded)
}
return nil
}
func resolveResourceRelations(input map[string]any, allowedFields []string, relations []ResourceRelation, requireRelations bool) (map[string]any, error) {
values := filterFields(input, allowedFields)
for _, relation := range relations {
@@ -411,7 +381,6 @@ var relationIdentityModels = map[string]any{
"delivery_track_id": &models.DeliveryTrack{},
"platform_role_id": &models.PlatformRole{},
"platform_menu_id": &models.PlatformMenu{},
"report_id": &models.Report{},
"wallet_id": &models.Wallet{},
}

View File

@@ -1,21 +0,0 @@
package models
import (
"time"
"git.apinb.com/bsm-sdk/core/database"
)
// AuditApproval 对应 audit_approval保存审批流与复核意见。
type AuditApproval struct {
Entity // 公共实体字段
BusinessType string `gorm:"column:business_type;type:varchar(64);not null" json:"business_type"` // business_type 业务字段
BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` // business_identity 业务字段
ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段
Opinion string `gorm:"column:opinion;type:text;not null;default:''" json:"opinion"` // opinion 业务字段
HandlerIdentity string `gorm:"column:handler_identity;type:varchar(36);not null;default:'';index" json:"handler_identity"` // handler_identity 业务字段
HandledAt *time.Time `gorm:"column:handled_at;type:timestamptz" json:"handled_at"` // handled_at 业务字段
}
func init() { database.AppendMigrate(&AuditApproval{}) }
func (table *AuditApproval) TableName() string { return "audit_approval" }

View File

@@ -1,19 +0,0 @@
package models
import (
"git.apinb.com/bsm-sdk/core/database"
"time"
)
// AuditExportLog 对应 audit_export_log保存敏感导出审计。
type AuditExportLog struct {
Entity // 公共实体字段
ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段
Purpose string `gorm:"column:purpose;type:varchar(255);not null" json:"purpose"` // purpose 业务字段
FieldScope string `gorm:"column:field_scope;type:jsonb;not null;default:'{}'" json:"field_scope"` // field_scope 业务字段
ApprovedAt *time.Time `gorm:"column:approved_at;type:timestamptz" json:"approved_at"` // approved_at 业务字段
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // file_uri 业务字段
}
func init() { database.AppendMigrate(&AuditExportLog{}) }
func (table *AuditExportLog) TableName() string { return "audit_export_log" }

View File

@@ -1,17 +0,0 @@
package models
import "git.apinb.com/bsm-sdk/core/database"
// AuditOperationLog 对应 audit_operation_log保存不可变操作审计。
type AuditOperationLog struct {
Entity // 公共实体字段
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;index" json:"operator_identity"` // operator_identity 业务字段
Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
ObjectType string `gorm:"column:object_type;type:varchar(64);not null" json:"object_type"` // object_type 业务字段
ObjectIdentity string `gorm:"column:object_identity;type:varchar(36);not null;index" json:"object_identity"` // object_identity 业务字段
BeforeData string `gorm:"column:before_data;type:jsonb;not null;default:'{}'" json:"before_data"` // before_data 业务字段
AfterData string `gorm:"column:after_data;type:jsonb;not null;default:'{}'" json:"after_data"` // after_data 业务字段
}
func init() { database.AppendMigrate(&AuditOperationLog{}) }
func (table *AuditOperationLog) TableName() string { return "audit_operation_log" }

View File

@@ -2,8 +2,8 @@ package models
import "git.apinb.com/bsm-sdk/core/database"
// CntContent 对应 cnt_content保存公告与协议内容。
type CntContent struct {
// CmsContent 对应 cms_content保存公告与协议内容。
type CmsContent struct {
Entity // 公共实体字段
ContentType string `gorm:"column:content_type;type:varchar(32);not null" json:"content_type"` // content_type 业务字段
Title string `gorm:"column:title;type:varchar(256);not null" json:"title"` // title 业务字段
@@ -12,5 +12,5 @@ type CntContent struct {
PublishStatus string `gorm:"column:publish_status;type:varchar(32);not null;default:'draft'" json:"publish_status"` // publish_status 业务字段
}
func init() { database.AppendMigrate(&CntContent{}) }
func (table *CntContent) TableName() string { return "cnt_content" }
func init() { database.AppendMigrate(&CmsContent{}) }
func (table *CmsContent) TableName() string { return "cms_content" }

View File

@@ -10,7 +10,7 @@ type DevTelemetry struct {
Entity // 公共实体字段
SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;index" json:"smart_cylinder_valve_identity"` // smart_cylinder_valve_identity 业务字段
ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null;index" json:"reported_at"` // reported_at 业务字段
Payload string `gorm:"column:payload;type:jsonb;not null;default:'{}'" json:"payload"` // payload 业务字段
Payload string `gorm:"column:payload;type:text;not null;default:''" json:"payload"` // payload 业务字段
QualityFlag string `gorm:"column:quality_flag;type:varchar(32);not null;default:'normal'" json:"quality_flag"` // quality_flag 业务字段
}

View File

@@ -5,11 +5,11 @@ import "git.apinb.com/bsm-sdk/core/database"
// EcOrderItem 对应 ec_order_item保存订单商品快照。
type EcOrderItem struct {
Entity // 公共实体字段
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段
ProductSnapshot string `gorm:"column:product_snapshot;type:jsonb;not null;default:'{}'" json:"product_snapshot"` // product_snapshot 业务字段
Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段
SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` // sale_amount 业务字段
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段
ProductSnapshot string `gorm:"column:product_snapshot;type:text;not null;default:''" json:"product_snapshot"` // product_snapshot 业务字段
Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段
SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` // sale_amount 业务字段
}
func init() { database.AppendMigrate(&EcOrderItem{}) }

View File

@@ -1,14 +0,0 @@
package models
import "git.apinb.com/bsm-sdk/core/database"
// NtfTemplate 对应 ntf_template保存通知模板。
type NtfTemplate struct {
Entity // 公共实体字段
TemplateCode string `gorm:"column:template_code;type:varchar(64);not null;uniqueIndex" json:"template_code"` // template_code 业务字段
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段
Content string `gorm:"column:content;type:text;not null" json:"content"` // content 业务字段
}
func init() { database.AppendMigrate(&NtfTemplate{}) }
func (table *NtfTemplate) TableName() string { return "ntf_template" }

View File

@@ -1,18 +0,0 @@
package models
import (
"git.apinb.com/bsm-sdk/core/database"
"time"
)
// Report 对应 report保存统计报表档案。
type Report struct {
Entity // 公共实体字段
ReportCode string `gorm:"column:report_code;type:varchar(64);not null;uniqueIndex" json:"report_code"` // report_code 业务字段
ReportType string `gorm:"column:report_type;type:varchar(32);not null" json:"report_type"` // report_type 业务字段
StatPeriod string `gorm:"column:stat_period;type:varchar(64);not null" json:"stat_period"` // stat_period 业务字段
GeneratedAt time.Time `gorm:"column:generated_at;type:timestamptz;not null" json:"generated_at"` // generated_at 业务字段
}
func init() { database.AppendMigrate(&Report{}) }
func (table *Report) TableName() string { return "report" }

View File

@@ -1,15 +0,0 @@
package models
import "git.apinb.com/bsm-sdk/core/database"
// ReportItem 对应 report_item保存报表维度明细。
type ReportItem struct {
Entity // 公共实体字段
ReportID uint64 `gorm:"column:report_id;not null;index" json:"report_id"` // report_id 业务字段
Dimension string `gorm:"column:dimension;type:varchar(128);not null" json:"dimension"` // dimension 业务字段
MetricCode string `gorm:"column:metric_code;type:varchar(64);not null" json:"metric_code"` // metric_code 业务字段
MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段
}
func init() { database.AppendMigrate(&ReportItem{}) }
func (table *ReportItem) TableName() string { return "report_item" }

View File

@@ -1,19 +0,0 @@
package models
import (
"git.apinb.com/bsm-sdk/core/database"
"time"
)
// ReportMetricSnapshot 对应 report_metric_snapshot保存指标快照。
type ReportMetricSnapshot struct {
Entity // 公共实体字段
MetricCode string `gorm:"column:metric_code;type:varchar(64);not null;index" json:"metric_code"` // metric_code 业务字段
ScopeType string `gorm:"column:scope_type;type:varchar(32);not null" json:"scope_type"` // scope_type 业务字段
ScopeID uint64 `gorm:"column:scope_id;not null;default:0;index" json:"scope_id"` // scope_id 业务字段
StatAt time.Time `gorm:"column:stat_at;type:timestamptz;not null;index" json:"stat_at"` // stat_at 业务字段
MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段
}
func init() { database.AppendMigrate(&ReportMetricSnapshot{}) }
func (table *ReportMetricSnapshot) TableName() string { return "report_metric_snapshot" }

View File

@@ -7,9 +7,9 @@ type SafeRule struct {
Entity // 公共实体字段
RuleCode string `gorm:"column:rule_code;type:varchar(64);not null;uniqueIndex" json:"rule_code"` // rule_code 业务字段
VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` // version_no 业务字段
Threshold string `gorm:"column:threshold;type:jsonb;not null;default:'{}'" json:"threshold"` // threshold 业务字段
Threshold string `gorm:"column:threshold;type:text;not null;default:''" json:"threshold"` // threshold 业务字段
Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
GrayScope string `gorm:"column:gray_scope;type:jsonb;not null;default:'{}'" json:"gray_scope"` // gray_scope 业务字段
GrayScope string `gorm:"column:gray_scope;type:text;not null;default:''" json:"gray_scope"` // gray_scope 业务字段
}
func init() { database.AppendMigrate(&SafeRule{}) }

View File

@@ -32,7 +32,6 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
registerCommerceRoute(protected)
registerFinanceRoute(protected)
registerContentRoute(protected)
registerAuditRoute(protected)
registerPlatformRoute(protected)
}
@@ -126,24 +125,13 @@ func registerFinanceRoute(group *gin.RouterGroup) {
registerReadOnlyResource(group, "/wallet/wallet_ledger", &models.WalletLedger{})
registerReadOnlyResource(group, "/wallet/wallet_recharge", &models.WalletRecharge{})
registerReadOnlyResource(group, "/wallet/wallet_withdrawal", &models.WalletWithdrawal{})
registerReadOnlyResource(group, "/report/report", &models.Report{})
registerReadOnlyResource(group, "/report/report_item", &models.ReportItem{})
registerReadOnlyResource(group, "/report/report_metric_snapshot", &models.ReportMetricSnapshot{})
}
func registerContentRoute(group *gin.RouterGroup) {
registerRestrictedWritableResource(group, "/content/cnt_content", &models.CntContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
registerRestrictedWritableResource(group, "/notification/ntf_template", &models.NtfTemplate{}, []string{"template_code", "channel", "content"})
registerRestrictedWritableResource(group, "/content/cms_content", &models.CmsContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
registerRestrictedWritableResource(group, "/customer_service/cs_ticket", &models.CsTicket{}, []string{"ticket_no", "category", "priority"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
}
func registerAuditRoute(group *gin.RouterGroup) {
registerReadOnlyResource(group, "/audit/audit_operation_log", &models.AuditOperationLog{})
registerReadOnlyResource(group, "/audit/audit_export_log", &models.AuditExportLog{})
registerReadOnlyResource(group, "/audit/audit_approval", &models.AuditApproval{})
group.POST("/audit/audit_approval/:identity/approve", platform.ApproveAudit)
}
func registerWritableResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) {
resource := group.Group(path)
resource.GET("", list)

View File

@@ -142,7 +142,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacySafetyPrefix+"event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) {
func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
engine := gin.New()
RegisterPlatform("heqi", engine)
@@ -156,7 +156,7 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
for _, resource := range []string{
"/finance/fin_payment", "/finance/fin_settlement", "/finance/fin_reconciliation",
"/content/cnt_content", "/notification/ntf_template", "/customer_service/cs_ticket",
"/content/cms_content", "/customer_service/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)
@@ -165,8 +165,6 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
for _, resource := range []string{
"/wallet/wallet", "/wallet/wallet_ledger", "/wallet/wallet_recharge", "/wallet/wallet_withdrawal",
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
"/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
} {
path := "/heqi/platform/v1" + resource
assertRouteMethods(t, routes, path, http.MethodGet)
@@ -178,15 +176,16 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
}
}
assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost)
legacyAuditPrefix := "/audit/" + "au" + "d_"
for _, resource := range []string{legacyAuditPrefix + "operation_log", legacyAuditPrefix + "export_log", legacyAuditPrefix + "approval"} {
for _, resource := range []string{
"/content/cnt_content", "/notification/ntf_template",
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
"/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
} {
path := "/heqi/platform/v1" + resource
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacyAuditPrefix+"approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
}
func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) {