refactor(platform): remove deprecated reporting and audit modules
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}},
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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 业务字段
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}) }
|
||||
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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" }
|
||||
@@ -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{}) }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
| 配送轨迹 | `delivery_track` / `DeliveryTrack` / `delivery_track.go` | `id`、`identity`、`delivery_task_id`、`status`、`started_at`、`completed_at` | 用户可见简化配送轨迹 |
|
||||
| 配送轨迹点 | `delivery_track_point` / `DeliveryTrackPoint` / `delivery_track_point.go` | `id`、`identity`、`delivery_track_id`、`point_type`、`occurred_at`、`longitude`、`latitude` | 精确位置与任务节点;访问受授权控制 |
|
||||
|
||||
### 4.5 财务、钱包、统计报表、内容与审计
|
||||
### 4.5 财务、钱包与内容
|
||||
|
||||
| 实体 | 表 / 模型 / 文件 | 关键字段 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -98,15 +98,8 @@
|
||||
| 钱包流水 | `wallet_ledger` / `WalletLedger` / `wallet_ledger.go` | `id`、`identity`、`wallet_id`、`amount`、`direction`、`balance_after`、`reference_identity` | 钱包唯一资金事实流水 |
|
||||
| 钱包充值 | `wallet_recharge` / `WalletRecharge` / `wallet_recharge.go` | `id`、`identity`、`wallet_id`、`amount`、`channel`、`status` | 钱包充值申请和支付结果 |
|
||||
| 钱包提现 | `wallet_withdrawal` / `WalletWithdrawal` / `wallet_withdrawal.go` | `id`、`identity`、`wallet_id`、`amount`、`bank_account_masked`、`status` | 提现申请、审核与付款凭证 |
|
||||
| 统计报表 | `report` / `Report` / `report.go` | `id`、`identity`、`report_code`、`report_type`、`stat_period`、`generated_at` | 可下载或在线查看的统计报表主档案 |
|
||||
| 统计报表明细 | `report_item` / `ReportItem` / `report_item.go` | `id`、`identity`、`report_id`、`dimension`、`metric_code`、`metric_value` | 按组织、区域、商品、时间拆分的报表明细 |
|
||||
| 内容 | `cnt_content` / `CntContent` / `cnt_content.go` | `content_type`、`title`、`version_no`、`publish_status` | 公告、协议、安全宣教内容 |
|
||||
| 消息模板 | `ntf_template` / `NtfTemplate` / `ntf_template.go` | `template_code`、`channel`、`content`、`status` | 短信、推送、站内信模板 |
|
||||
| 内容 | `cms_content` / `CmsContent` / `cms_content.go` | `content_type`、`title`、`version_no`、`publish_status` | 公告、协议、安全宣教内容 |
|
||||
| 客服工单 | `cs_ticket` / `CsTicket` / `cs_ticket.go` | `id`、`identity`、`ticket_no`、`user_id`、`category`、`status`、`priority` | 咨询、投诉、回访与升级 |
|
||||
| 运营指标快照 | `report_metric_snapshot` / `ReportMetricSnapshot` / `report_metric_snapshot.go` | `id`、`identity`、`metric_code`、`scope_type`、`scope_id`、`stat_at`、`metric_value` | 看板与预警计算结果 |
|
||||
| 操作审计 | `aud_operation_log` / `AudOperationLog` / `aud_operation_log.go` | `operator_identity`、`action`、`object_type`、`object_identity`、`before_data`、`after_data` | 不可由普通管理员改写或删除 |
|
||||
| 导出审计 | `aud_export_log` / `AudExportLog` / `aud_export_log.go` | `applicant_identity`、`purpose`、`field_scope`、`approved_at`、`file_uri` | 敏感导出、下载与水印记录 |
|
||||
| 审批单 | `aud_approval` / `AudApproval` / `aud_approval.go` | `business_type`、`business_identity`、`applicant_identity`、`status` | 双人复核、审批流与意见 |
|
||||
|
||||
## 5. API 路由规则
|
||||
|
||||
@@ -193,16 +186,8 @@
|
||||
| `GET` | `/wallet/wallet_ledger/:identity` | 钱包流水详情查看 |
|
||||
| `GET` | `/wallet/wallet_recharge` | 钱包充值记录列表查询 |
|
||||
| `GET` | `/wallet/wallet_withdrawal` | 钱包提现记录列表查询 |
|
||||
| `GET` | `/report/report` | 统计报表列表查询 |
|
||||
| `GET` | `/report/report/:identity` | 统计报表详情查看 |
|
||||
| `GET` | `/report/report_item` | 统计报表明细列表查询 |
|
||||
| `GET` | `/report/report_item/:identity` | 统计报表明细详情查看 |
|
||||
| `GET/POST/PUT/DELETE` | `/content/cnt_content` | 内容 CRUD |
|
||||
| `GET/POST/PUT/DELETE` | `/content/cms_content` | 内容 CRUD |
|
||||
| `GET/POST/PUT/DELETE` | `/customer_service/cs_ticket` | 客服工单 CRUD |
|
||||
| `GET` | `/report/report_metric_snapshot` | 运营指标看板查询 |
|
||||
| `GET` | `/audit/aud_operation_log` | 操作审计查询 |
|
||||
| `GET` | `/audit/aud_export_log` | 导出审计查询 |
|
||||
| `POST` | `/audit/aud_approval/:identity/approve` | 审批通过或驳回 |
|
||||
|
||||
## 6. 前端页面规划
|
||||
|
||||
@@ -269,14 +254,8 @@
|
||||
| 钱包中心 `WalletCards` | 钱包流水 `ListOrdered` | `/wallet/ledger` | `views/wallet/ledger` | `ListPage.vue` | `wallet_ledger` 只读列表、方向、金额和余额快照 |
|
||||
| 钱包中心 `WalletCards` | 充值记录 `CirclePlus` | `/wallet/recharge` | `views/wallet/recharge` | `ListPage.vue` | `wallet_recharge` 只读列表和详情 |
|
||||
| 钱包中心 `WalletCards` | 提现记录 `CircleMinus` | `/wallet/withdrawal` | `views/wallet/withdrawal` | `ListPage.vue` | `wallet_withdrawal` 只读列表和详情 |
|
||||
| 统计报表 `ChartNoAxesCombined` | 报表列表 `FileBarChart` | `/report/list` | `views/report/list` | `ListPage.vue` | `report` 只读列表、周期、生成时间和下载入口 |
|
||||
| 统计报表 `ChartNoAxesCombined` | 报表明细 `TableProperties` | `/report/item` | `views/report/item` | `ListPage.vue` | `report_item` 只读列表、维度和指标值 |
|
||||
| 统计报表 `ChartNoAxesCombined` | 指标快照 `ChartLine` | `/report/metric-snapshot` | `views/report/metric-snapshot` | `DashboardPage.vue` | `report_metric_snapshot` 只读图表、同比和环比 |
|
||||
| 内容客服 `MessagesSquare` | 内容管理 `FileText` | `/content/list` | `views/content/list` | `ListPage.vue` | `cnt_content` CRUD、类型、标题和发布状态 |
|
||||
| 内容客服 `MessagesSquare` | 消息模板 `Send` | `/content/template` | `views/content/template` | `ListPage.vue` | `ntf_template` CRUD、渠道、模板编码和状态 |
|
||||
| 内容客服 `MessagesSquare` | 内容管理 `FileText` | `/content/cms-content` | `views/content/cms_content` | `ListPage.vue` | `cms_content` CRUD、类型、标题和发布状态 |
|
||||
| 内容客服 `MessagesSquare` | 客服工单 `Headset` | `/content/ticket` | `views/content/ticket` | `ListPage.vue` | `cs_ticket` CRUD、客户、分类、优先级和状态 |
|
||||
| 审计合规 `ScrollText` | 操作审计 `History` | `/audit/operation-log` | `views/audit/operation-log` | `ListPage.vue` | `aud_operation_log` 只读检索、对象、动作和前后值摘要 |
|
||||
| 审计合规 `ScrollText` | 导出审计 `FileOutput` | `/audit/export-log` | `views/audit/export-log` | `ListPage.vue` | `aud_export_log` 只读检索、用途、字段范围和导出文件 |
|
||||
| 平台配置 `ShieldCheck` | 平台账户 `ContactRound` | `/platform/account` | `views/platform/account` | `ListPage.vue` | `platfrom_account` 列表、头像、角色、状态;新增/编辑抽屉 |
|
||||
| 平台配置 `ShieldCheck` | 角色管理 `BadgeCheck` | `/platform/role` | `views/platform/role` | `ListPage.vue` | `platform_role` CRUD,`root` 角色只读保护 |
|
||||
| 平台配置 `ShieldCheck` | 菜单管理 `MenuSquare` | `/platform/menu` | `views/platform/menu` | `TreePage.vue` | `platform_menu` 树、图标、路由、排序和角色菜单授权 |
|
||||
|
||||
@@ -141,10 +141,10 @@ platforms/
|
||||
| 订单与履约 | `ord_` | `ord_order`、`ord_payment`、`ord_service_task`、`ord_delivery_track` |
|
||||
| 调度与配送 | `dsp_` | `dsp_assignment`、`dsp_shift`、`dsp_delivery_track_point` |
|
||||
| 钱包与结算 | `wal_` | `wal_wallet_ledger`、`wal_settlement`、`wal_withdrawal` |
|
||||
| 内容与通知 | `cnt_`、`ntf_` | `cnt_article`、`cnt_banner`、`ntf_message`、`ntf_template` |
|
||||
| 内容管理 | `cms_` | `cms_content` |
|
||||
| 生产与质量 | `mfg_` | `mfg_work_order`、`mfg_batch`、`mfg_quality_check` |
|
||||
| 开放接口 | `api_` | `api_product`、`api_client`、`api_subscription` |
|
||||
| 审计与平台任务 | `aud_`、`sys_` | `aud_operation_log`、`sys_outbox_event`、`sys_dead_letter_event` |
|
||||
| 平台任务 | `sys_` | `sys_outbox_event`、`sys_dead_letter_event` |
|
||||
|
||||
- 所有主表必须包含 `identity` 字段,类型为 UUID V7,并作为该表的主键。UUID V7 由应用服务生成,保证时间有序性;禁止使用数据库自增主键、随机 UUID V4 或将业务编号作为主键。
|
||||
- 引用主表时,外键字段命名为 `<实体名>_identity`,例如 `order_identity`、`service_person_identity`。业务展示编号(订单号、设备编码、站点编码等)应使用独立字段并设置唯一约束,不能替代 `identity`。
|
||||
|
||||
@@ -7,19 +7,16 @@ const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
|
||||
const legacySafetyPrefix = ['sa', 'f_'].join('');
|
||||
const legacyAuditPrefix = ['au', 'd_'].join('');
|
||||
|
||||
test('资源定义使用 safe 和 audit 前缀', () => {
|
||||
test('资源定义使用 safe 前缀且不保留已删除审计资源', () => {
|
||||
assert.match(resourcesSource, /define\(\s*'safe_rule',\s*'\/safety\/safe_rule'/);
|
||||
assert.match(resourcesSource, /define\(\s*'safe_event',\s*'\/safety\/safe_event'/);
|
||||
assert.match(resourcesSource, /define\(\s*'safe_inspection',\s*'\/safety\/safe_inspection'/);
|
||||
assert.match(resourcesSource, /define\(\s*'safe_event_disposal',\s*'\/safety\/safe_event\/:identity\/disposals'/);
|
||||
assert.match(resourcesSource, /define\(\s*'audit_operation_log',\s*'\/audit\/audit_operation_log'/);
|
||||
assert.match(resourcesSource, /define\(\s*'audit_export_log',\s*'\/audit\/audit_export_log'/);
|
||||
assert.match(resourcesSource, /define\(\s*'audit_approval',\s*'\/audit\/audit_approval'/);
|
||||
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacyAuditPrefix}(?:operation_log|export_log|approval)',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`/audit/${legacyAuditPrefix}approval/:identity/approve`));
|
||||
assert.doesNotMatch(resourcesSource, /audit_(?:operation_log|export_log|approval)/);
|
||||
});
|
||||
|
||||
test('只读页面将状态变更视为违规写操作', () => {
|
||||
|
||||
@@ -28,13 +28,13 @@ function loadResourceForm() {
|
||||
return resourceModule.exports;
|
||||
}
|
||||
|
||||
test('资源字段声明保留数字、布尔、时间与 JSON 类型', () => {
|
||||
test('资源字段声明保留数字、布尔、时间与文本类型', () => {
|
||||
const resources = loadResources();
|
||||
const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key);
|
||||
assert.equal(field('ec_product', 'price_amount').type, 'number');
|
||||
assert.equal(field('ec_product_image', 'is_cover').type, 'boolean');
|
||||
assert.equal(field('delivery_track', 'started_at').type, 'datetime');
|
||||
assert.equal(field('dev_telemetry', 'payload').type, 'json');
|
||||
assert.equal(field('dev_telemetry', 'payload').type, 'textarea');
|
||||
});
|
||||
|
||||
test('表单载荷构造器省略空的可选关系并转换字段类型', async () => {
|
||||
@@ -56,7 +56,7 @@ test('表单载荷构造器省略空的可选关系并转换字段类型', async
|
||||
);
|
||||
});
|
||||
|
||||
test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提交', () => {
|
||||
test('安全规则与订单明细的文本字段允许任意文本提交', () => {
|
||||
const resources = loadResources();
|
||||
const { buildResourcePayload } = loadResourceForm();
|
||||
const fields = (name) => resources.find((item) => item.name === name).fields;
|
||||
@@ -64,29 +64,29 @@ test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields('safe_rule'), {
|
||||
rule_code: 'pressure-limit',
|
||||
threshold: '{"max":10}',
|
||||
threshold: '{invalid}',
|
||||
action: 'close-valve',
|
||||
gray_scope: '["north"]',
|
||||
gray_scope: 'north region only',
|
||||
}))),
|
||||
{
|
||||
rule_code: 'pressure-limit',
|
||||
threshold: '{"max":10}',
|
||||
threshold: '{invalid}',
|
||||
action: 'close-valve',
|
||||
gray_scope: '["north"]',
|
||||
gray_scope: 'north region only',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields('ec_order_item'), {
|
||||
ec_order_identity: 'order-a',
|
||||
ec_product_identity: 'product-a',
|
||||
product_snapshot: '{"name":"液化气"}',
|
||||
product_snapshot: 'arbitrary product snapshot text',
|
||||
quantity: 2,
|
||||
sale_amount: 500,
|
||||
}))),
|
||||
{
|
||||
ec_order_identity: 'order-a',
|
||||
ec_product_identity: 'product-a',
|
||||
product_snapshot: '{"name":"液化气"}',
|
||||
product_snapshot: 'arbitrary product snapshot text',
|
||||
quantity: 2,
|
||||
sale_amount: 500,
|
||||
},
|
||||
@@ -131,11 +131,11 @@ test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('审批只读页提供同意和驳回操作', () => {
|
||||
test('已删除的审批模块不再保留前端资源或页面', () => {
|
||||
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
|
||||
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
||||
assert.match(resources, /audit_approval[\s\S]*\/audit\/audit_approval\/:identity\/approve/);
|
||||
assert.match(source, /submitDetailAction/);
|
||||
assert.doesNotMatch(resources, /audit_approval|audit_operation_log|audit_export_log/);
|
||||
assert.equal(fs.existsSync(fromProjectRoot('src/views/audit')), false);
|
||||
});
|
||||
|
||||
test('树页面通过资源归档接口归档节点', () => {
|
||||
|
||||
@@ -43,12 +43,6 @@ export function buildResourcePayload(
|
||||
case 'boolean':
|
||||
payload[field.key] = value === true || value === 'true';
|
||||
break;
|
||||
case 'json':
|
||||
if (typeof value !== 'string')
|
||||
throw new Error(`${field.label}必须是有效 JSON`);
|
||||
JSON.parse(value);
|
||||
payload[field.key] = value;
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime': {
|
||||
const date = new Date(String(value));
|
||||
|
||||
@@ -10,7 +10,6 @@ export type ResourceFieldType =
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'json'
|
||||
| 'textarea';
|
||||
export type ResourceField = {
|
||||
key: string;
|
||||
@@ -175,14 +174,17 @@ const datetimeFields = new Set([
|
||||
'handled_at',
|
||||
'created_at',
|
||||
]);
|
||||
const jsonFields = new Set([
|
||||
// 大文本字段按普通文本处理,不再要求或解析 JSON。
|
||||
const textareaFields = new Set([
|
||||
'body',
|
||||
'content',
|
||||
'reason',
|
||||
'opinion',
|
||||
'payload',
|
||||
'threshold',
|
||||
'gray_scope',
|
||||
'product_snapshot',
|
||||
'field_scope',
|
||||
]);
|
||||
const textareaFields = new Set(['body', 'content', 'reason', 'opinion']);
|
||||
const fieldType = (key: string): ResourceFieldType => {
|
||||
if (key === 'platform_role_code') return 'role-code';
|
||||
if (key === 'menu_identities') return 'menu-identities';
|
||||
@@ -192,7 +194,6 @@ const fieldType = (key: string): ResourceFieldType => {
|
||||
if (booleanFields.has(key)) return 'boolean';
|
||||
if (dateFields.has(key)) return 'date';
|
||||
if (datetimeFields.has(key)) return 'datetime';
|
||||
if (jsonFields.has(key)) return 'json';
|
||||
if (textareaFields.has(key)) return 'textarea';
|
||||
return 'text';
|
||||
};
|
||||
@@ -237,8 +238,7 @@ const titles: Record<string, string> = {
|
||||
fin_payment: '支付记录',
|
||||
fin_settlement: '财务结算',
|
||||
fin_reconciliation: '财务对账',
|
||||
cnt_content: '内容管理',
|
||||
ntf_template: '通知模板',
|
||||
cms_content: '内容管理',
|
||||
cs_ticket: '客服工单',
|
||||
platfrom_account: '平台账户',
|
||||
platform_role: '平台角色',
|
||||
@@ -247,12 +247,6 @@ const titles: Record<string, string> = {
|
||||
wallet_ledger: '钱包流水',
|
||||
wallet_recharge: '钱包充值',
|
||||
wallet_withdrawal: '钱包提现',
|
||||
report: '报表',
|
||||
report_item: '报表项目',
|
||||
report_metric_snapshot: '指标快照',
|
||||
audit_operation_log: '操作审计',
|
||||
audit_export_log: '导出审计',
|
||||
audit_approval: '审批审计',
|
||||
};
|
||||
const define = (
|
||||
name: string,
|
||||
@@ -523,18 +517,13 @@ export const resources: ResourceUiDefinition[] = [
|
||||
'list',
|
||||
['channel!', 'bill_date!', 'difference_amount!'],
|
||||
),
|
||||
define('cnt_content', '/content/cnt_content', 'writable', 'list', [
|
||||
define('cms_content', '/content/cms_content', 'writable', 'list', [
|
||||
'content_type!',
|
||||
'title!',
|
||||
'body!',
|
||||
'version_no',
|
||||
'publish_status',
|
||||
]),
|
||||
define('ntf_template', '/notification/ntf_template', 'writable', 'list', [
|
||||
'template_code!',
|
||||
'channel!',
|
||||
'content!',
|
||||
]),
|
||||
define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'ticket_no!',
|
||||
@@ -592,62 +581,6 @@ export const resources: ResourceUiDefinition[] = [
|
||||
'amount',
|
||||
'status',
|
||||
]),
|
||||
define('report', '/report/report', 'readonly', 'list', [
|
||||
'report_code',
|
||||
'report_type',
|
||||
'stat_period',
|
||||
'generated_at',
|
||||
'status',
|
||||
]),
|
||||
define('report_item', '/report/report_item', 'readonly', 'list', [
|
||||
'report_identity',
|
||||
'dimension',
|
||||
'metric_code',
|
||||
'metric_value',
|
||||
]),
|
||||
define(
|
||||
'report_metric_snapshot',
|
||||
'/report/report_metric_snapshot',
|
||||
'readonly',
|
||||
'list',
|
||||
['metric_code', 'scope_type', 'stat_at', 'metric_value'],
|
||||
),
|
||||
define('audit_operation_log', '/audit/audit_operation_log', 'readonly', 'list', [
|
||||
'operator_identity',
|
||||
'action',
|
||||
'object_identity',
|
||||
'created_at',
|
||||
]),
|
||||
define('audit_export_log', '/audit/audit_export_log', 'readonly', 'list', [
|
||||
'applicant_identity',
|
||||
'purpose',
|
||||
'field_scope',
|
||||
'approved_at',
|
||||
'file_uri',
|
||||
]),
|
||||
define(
|
||||
'audit_approval',
|
||||
'/audit/audit_approval',
|
||||
'readonly',
|
||||
'list',
|
||||
[
|
||||
'business_type',
|
||||
'business_identity',
|
||||
'applicant_identity',
|
||||
'opinion',
|
||||
'handler_identity',
|
||||
'status',
|
||||
'handled_at',
|
||||
],
|
||||
[
|
||||
action('同意', '/audit/audit_approval/:identity/approve', ['opinion'], {
|
||||
status: 'approved',
|
||||
}),
|
||||
action('驳回', '/audit/audit_approval/:identity/approve', ['opinion!'], {
|
||||
status: 'rejected',
|
||||
}),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
export const resourceByName = Object.fromEntries(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/audit', name: 'audit', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.audit', requiresAuth: true, icon: 'icon-apps', order: 22 },
|
||||
children: [
|
||||
{ path: 'audit-operation-log', name: 'audit-audit-operation-log', component: () => import('@/views/audit/audit_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_operation_log', requiresAuth: true, menuCode: 'audit' } },
|
||||
{ path: 'audit-export-log', name: 'audit-audit-export-log', component: () => import('@/views/audit/audit_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_export_log', requiresAuth: true, menuCode: 'audit' } },
|
||||
{ path: 'audit-approval', name: 'audit-audit-approval', component: () => import('@/views/audit/audit_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_approval', requiresAuth: true, menuCode: 'audit' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -4,7 +4,7 @@ const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/content', name: 'content', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.content', requiresAuth: true, icon: 'icon-apps', order: 17 },
|
||||
children: [
|
||||
{ path: 'cnt-content', name: 'content-cnt-content', component: () => import('@/views/content/cnt_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cnt_content', requiresAuth: true, menuCode: 'content' } }
|
||||
{ path: 'cms-content', name: 'content-cms-content', component: () => import('@/views/content/cms_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cms_content', requiresAuth: true, menuCode: 'content' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/notification', name: 'notification', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.notification', requiresAuth: true, icon: 'icon-apps', order: 18 },
|
||||
children: [
|
||||
{ path: 'ntf-template', name: 'notification-ntf-template', component: () => import('@/views/notification/ntf_template/ListPage.vue'), meta: { locale: 'menu.platform.notification.ntf_template', requiresAuth: true, menuCode: 'notification' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -9,11 +9,8 @@ import safetyRoutes from './safety';
|
||||
import ecRoutes from './ec';
|
||||
import financeRoutes from './finance';
|
||||
import contentRoutes from './content';
|
||||
import notificationRoutes from './notification';
|
||||
import customerServiceRoutes from './customer_service';
|
||||
import walletRoutes from './wallet';
|
||||
import reportRoutes from './report';
|
||||
import auditRoutes from './audit';
|
||||
const platformRoutes: AppRouteRecordRaw[] = [
|
||||
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, meta: { locale: 'menu.dashboard', requiresAuth: true, icon: 'icon-dashboard', order: 0 }, children: [{ path: 'overview', name: 'DashboardOverview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { locale: 'menu.platform.dashboard', requiresAuth: true, menuCode: 'dashboard' } }] },
|
||||
{ path: '/platform', name: 'platform', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.config', requiresAuth: true, icon: 'icon-settings', order: 30 }, children: [
|
||||
@@ -22,4 +19,4 @@ const platformRoutes: AppRouteRecordRaw[] = [
|
||||
{ path: 'platform-menu', name: 'platform-platform-menu', component: () => import('@/views/platform/platform_menu/TreePage.vue'), meta: { locale: 'menu.platform.platform.platform_menu', requiresAuth: true, menuCode: 'platform' } },
|
||||
] },
|
||||
];
|
||||
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...safetyRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...notificationRoutes, ...customerServiceRoutes, ...walletRoutes, ...reportRoutes, ...auditRoutes];
|
||||
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...safetyRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...customerServiceRoutes, ...walletRoutes];
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{
|
||||
path: '/report', name: 'report', component: DEFAULT_LAYOUT,
|
||||
meta: { locale: 'menu.platform.report', requiresAuth: true, icon: 'icon-apps', order: 21 },
|
||||
children: [
|
||||
{ path: 'report', name: 'report-report', component: () => import('@/views/report/report/ListPage.vue'), meta: { locale: 'menu.platform.report.report', requiresAuth: true, menuCode: 'report' } },
|
||||
{ path: 'report-item', name: 'report-report-item', component: () => import('@/views/report/report_item/ListPage.vue'), meta: { locale: 'menu.platform.report.report_item', requiresAuth: true, menuCode: 'report' } },
|
||||
{ path: 'report-metric-snapshot', name: 'report-report-metric-snapshot', component: () => import('@/views/report/report_metric_snapshot/ListPage.vue'), meta: { locale: 'menu.platform.report.report_metric_snapshot', requiresAuth: true, menuCode: 'report' } }
|
||||
],
|
||||
}];
|
||||
export default routes;
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/audit/audit_approval');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/audit/audit_export_log');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/audit/audit_operation_log');
|
||||
</script>
|
||||
@@ -2,5 +2,5 @@
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/content/cnt_content');
|
||||
const definition = getResource('/content/cms_content');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><CrudListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import CrudListPage from '@/views/shared/CrudListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/notification/ntf_template');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/report/report');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/report/report_item');
|
||||
</script>
|
||||
@@ -1,6 +0,0 @@
|
||||
<template><ReadOnlyListPage :definition="definition" /></template>
|
||||
<script setup lang="ts">
|
||||
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
|
||||
import { getResource } from '@/api/resources';
|
||||
const definition = getResource('/report/report_metric_snapshot');
|
||||
</script>
|
||||
@@ -37,7 +37,7 @@
|
||||
<a-input-number v-else-if="field.type === 'number'" v-model="form[field.key]" />
|
||||
<a-date-picker v-else-if="field.type === 'date'" v-model="form[field.key]" value-format="YYYY-MM-DD" />
|
||||
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
|
||||
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
|
||||
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
|
||||
<a-select v-else-if="field.type === 'role-code'" v-model="form[field.key]">
|
||||
<a-option v-for="role in roleOptions" :key="role.role_code" :value="role.role_code">{{ role.name }}</a-option>
|
||||
@@ -61,7 +61,7 @@
|
||||
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
|
||||
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
|
||||
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
|
||||
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="actionForm[field.key]" />
|
||||
<a-textarea v-else-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
|
||||
<a-select v-else-if="field.type === 'menu-identities'" v-model="actionForm[field.key]" multiple allow-search>
|
||||
<a-option v-for="menu in menuOptions" :key="menu.identity" :value="menu.identity">{{ menu.name }}</a-option>
|
||||
</a-select>
|
||||
@@ -124,12 +124,7 @@ const detailEntries = computed(() =>
|
||||
function resetForm(data?: Row) {
|
||||
for (const field of props.definition.fields) {
|
||||
const value = data?.[field.key];
|
||||
form[field.key] =
|
||||
field.type === 'json' && value != null && typeof value !== 'string'
|
||||
? JSON.stringify(value, null, 2)
|
||||
: value == null
|
||||
? undefined
|
||||
: value;
|
||||
form[field.key] = value == null ? undefined : value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
|
||||
<a-form :model="actionForm" layout="vertical">
|
||||
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
|
||||
<a-textarea v-if="field.type === 'textarea' || field.type === 'json'" v-model="actionForm[field.key]" />
|
||||
<a-textarea v-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
|
||||
<a-input v-else v-model="actionForm[field.key]" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
Reference in New Issue
Block a user