fix(admin): standardize resource initialization and lists

This commit is contained in:
2026-08-05 12:19:13 +08:00
parent d88e18bd09
commit 2162fe5e11
36 changed files with 419 additions and 267 deletions

View File

@@ -9,6 +9,8 @@ import (
"strings"
"git.apinb.com/bsm-sdk/core/database"
"git.apinb.com/bsm-sdk/core/types"
"git.apinb.com/bsm-sdk/core/vars"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
"git.apinb.com/heqiapp/platforms/backend/api/internal/initdb"
deliverylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/delivery"
@@ -165,7 +167,7 @@ func writeMockData() error {
if err != nil {
return fmt.Errorf("connect database: %w", err)
}
if err := initdb.New(databaseService); err != nil {
if err := initdb.New(); err != nil {
return fmt.Errorf("initialize platform data: %w", err)
}
if err := seed.MockData(databaseService); err != nil {
@@ -175,45 +177,24 @@ func writeMockData() error {
}
func migrateDatabase() error {
config.New("heqi")
if config.Spec.Databases == nil {
return fmt.Errorf("database configuration is required")
config.New(serviceKey)
options := &types.SqlOptions{
MaxIdleConns: vars.SqlOptionMaxIdleConns,
MaxOpenConns: vars.SqlOptionMaxOpenConns,
ConnMaxLifetime: vars.SqlOptionConnMaxLifetime,
IsAutoMigrate: true,
LogStdout: false,
Debug: true,
}
var migrationDatabase *gorm.DB
var err error
driver := strings.ToLower(config.Spec.Databases.Driver)
switch driver {
case "postgres":
migrationDatabase, err = database.NewPostgres(config.Spec.Databases.Source, nil)
case "mysql":
migrationDatabase, err = database.NewMysql(config.Spec.Databases.Source, nil)
default:
return fmt.Errorf("unsupported database driver: %s", config.Spec.Databases.Driver)
}
if err != nil {
return fmt.Errorf("connect database before migration: %w", err)
}
if err := prepareAdditiveMigrations(migrationDatabase, driver); err != nil {
return err
}
if err := rejectLegacyPaymentSchema(migrationDatabase); err != nil {
return err
}
const legacyPhoneIndex = "idx_platform_account_phone"
if migrationDatabase.Migrator().HasIndex(&models.PlatformAccount{}, legacyPhoneIndex) {
if err := migrationDatabase.Migrator().DropIndex(&models.PlatformAccount{}, legacyPhoneIndex); err != nil {
return fmt.Errorf("drop legacy platform account phone index: %w", err)
}
}
databaseService, err := database.NewDatabase(
_, err := database.NewDatabase(
config.Spec.Databases.Driver,
config.Spec.Databases.Source,
nil,
options,
)
if err != nil {
return fmt.Errorf("migrate database: %w", err)
}
return initdb.New(databaseService)
return initdb.New()
}
// rejectLegacyPaymentSchema 阻止通用迁移物理删除历史资金表。

View File

@@ -15,6 +15,7 @@ import (
"git.apinb.com/bsm-sdk/core/printer"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/initdb"
appmiddleware "git.apinb.com/heqiapp/platforms/backend/api/internal/middleware"
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
"github.com/gin-gonic/gin"
@@ -27,9 +28,9 @@ func main() {
impl.NewImpl()
// 初始化先注释
// if err := initdb.New(impl.DBService); err != nil {
// panic(err)
// }
if err := initdb.New(); err != nil {
panic(err)
}
app := gin.Default()
sdkmiddleware.Mode(app)

View File

@@ -2,6 +2,8 @@ module git.apinb.com/heqiapp/platforms/backend/api
go 1.26.1
replace git.apinb.com/bsm-sdk/core => D:/work/bsm-sdk/core
require (
git.apinb.com/bsm-sdk/core v0.2.0
github.com/DATA-DOG/go-sqlmock v1.5.2

View File

@@ -3,7 +3,6 @@ package impl
import (
"git.apinb.com/bsm-sdk/core/cache/redis"
"git.apinb.com/bsm-sdk/core/database"
"git.apinb.com/bsm-sdk/core/logger"
"git.apinb.com/bsm-sdk/core/with"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
@@ -22,10 +21,6 @@ func NewImpl() {
MemoryService = with.Memory(nil)
RedisService = with.RedisCache(config.Spec.Cache)
// HTTP 服务启动只建立连接。表结构迁移由 platform-cli migrate 显式执行,
// 避免每次重启都对全部远程表执行耗时的元数据扫描。
migrateTables := database.MigrateTables
database.MigrateTables = nil
defer func() { database.MigrateTables = migrateTables }()
DBService = with.Databases(config.Spec.Databases, nil)
logger.New(nil)
}

View File

@@ -1,14 +1,15 @@
// Package initdb 提供应用启动后的基础数据初始化。
package initdb
import "gorm.io/gorm"
// New 是初始化入口,按依赖顺序在同一事务中编排所有必需的幂等初始化任务。
func New() error {
if err := InitPlatformAccess(); err != nil {
return err
}
// New 在同一事务中初始化平台基础数据。
func New(database *gorm.DB) error {
return database.Transaction(func(tx *gorm.DB) error {
if err := InitPlatformAccess(tx); err != nil {
return err
}
return InitPlatformRoot(tx)
})
if err := InitPlatformRoot(); err != nil {
return err
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"os"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"golang.org/x/crypto/bcrypt"
@@ -11,16 +12,13 @@ import (
)
const (
// PlatformRootUsername 是平台总后台的内置根账号名称。
PlatformRootUsername = "root"
// PlatformRootPassword 是仅用于首次启动的初始密码,首次登录后必须修改。
PlatformRootPassword = "Heqi@Root2026"
// PlatformRootRoleCode 表示根账号的平台角色。
PlatformRootRoleCode = "root"
)
// InitPlatformAccess 幂等初始化 root 角色;菜单定义位于逻辑层静态数据中。
func InitPlatformAccess(database *gorm.DB) error {
// InitPlatformAccess 幂等初始化平台根角色;菜单定义位于逻辑层静态数据中。
func InitPlatformAccess() error {
rootRole := models.PlatformRole{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
RoleCode: PlatformRootRoleCode,
@@ -28,13 +26,13 @@ func InitPlatformAccess(database *gorm.DB) error {
LocationScope: "precise",
IsSystem: true,
}
return database.Where("role_code = ?", rootRole.RoleCode).FirstOrCreate(&rootRole).Error
return impl.DBService.Where("role_code = ?", rootRole.RoleCode).FirstOrCreate(&rootRole).Error
}
// InitPlatformRoot 幂等创建平台总后台 root 账
func InitPlatformRoot(database *gorm.DB) error {
// InitPlatformRoot 幂等创建平台总后台 root 账
func InitPlatformRoot() error {
var account models.PlatformAccount
err := database.Where("username = ?", PlatformRootUsername).First(&account).Error
err := impl.DBService.Where("username = ?", PlatformRootUsername).First(&account).Error
if err == nil {
return nil
}
@@ -55,7 +53,7 @@ func InitPlatformRoot(database *gorm.DB) error {
PlatformRoleCode: PlatformRootRoleCode,
Phone: "",
}
return database.Create(&account).Error
return impl.DBService.Create(&account).Error
}
// platformRootPassword 优先读取部署环境传入的 root 初始密码。

View File

@@ -1,50 +0,0 @@
package initdb
import (
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func TestInitPlatformAccessSeedsRootRole(t *testing.T) {
sqlDatabase, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDatabase.Close() })
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 AND "platform_role"."deleted_at" IS NULL ORDER BY "platform_role"."id" LIMIT $2`)).
WithArgs("root", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "role_code", "name", "location_scope", "is_system"}).
AddRow(uint64(1), "root-role", 1, "root", "Root", "precise", true))
if err := InitPlatformAccess(database); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestPlatformRootPassword(t *testing.T) {
t.Run("accepts six character environment password", func(t *testing.T) {
t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "123456")
if got := platformRootPassword(); got != "123456" {
t.Fatalf("platformRootPassword() = %q, want environment password", got)
}
})
t.Run("falls back when environment password is too short", func(t *testing.T) {
t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "12345")
if got := platformRootPassword(); got != PlatformRootPassword {
t.Fatalf("platformRootPassword() = %q, want default password", got)
}
})
}

View File

@@ -242,6 +242,7 @@ func PrepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
if err := ctx.ShouldBindJSON(&input); err != nil || len(input) == 0 {
return nil, errors.New("invalid resource payload")
}
stripClientManagedCreateFields(input)
values, err := ResolveResourceRelations(input, allowedFields, relations, true)
if err != nil || len(values) == 0 {
return nil, errors.New("invalid resource payload")
@@ -249,6 +250,13 @@ func PrepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
return values, nil
}
// stripClientManagedCreateFields ensures database IDs and public identities are
// always generated by the logic layer for newly created records.
func stripClientManagedCreateFields(input map[string]any) {
delete(input, "id")
delete(input, "identity")
}
// ValidateResourceValues enforces invariants that database nullability and
// frontend form metadata cannot express.
func ValidateResourceValues(model any, values map[string]any, creating bool) error {

View File

@@ -7,6 +7,7 @@ import (
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/google/uuid"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@@ -18,6 +19,32 @@ func TestFilterFieldsKeepsOnlyAllowedKeys(t *testing.T) {
}
}
func TestStripClientManagedCreateFields(t *testing.T) {
input := map[string]any{"id": float64(99), "identity": "client-value", "name": "气站"}
stripClientManagedCreateFields(input)
if _, exists := input["id"]; exists {
t.Fatal("client supplied database ID was retained")
}
if _, exists := input["identity"]; exists {
t.Fatal("client supplied identity was retained")
}
if input["name"] != "气站" {
t.Fatalf("business fields changed: %#v", input)
}
}
func TestNewEntityGeneratesUUIDV7Identity(t *testing.T) {
first := NewEntity(StatusDraft)
second := NewEntity(StatusDraft)
if first.Identity == second.Identity {
t.Fatal("generated identities must be unique")
}
parsed, err := uuid.Parse(first.Identity)
if err != nil || parsed.Version() != 7 {
t.Fatalf("identity = %q, want UUID V7", first.Identity)
}
}
func TestOperationalQueriesExcludeArchivedRecords(t *testing.T) {
sqlDatabase, _, err := sqlmock.New()
if err != nil {

View File

@@ -1,17 +1,30 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.31.0-20230802163732-1c33ebd9ecfa.1/go.mod h1:xafc+XIsTxTy76GJQ1TKgvJWsSugFBqMaN27WhUblew=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw=
codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/protovalidate-go v0.2.1/go.mod h1:e7XXDtlxj5vlEyAgsrxpzayp4cEMKCSSb8ZCkin+MVA=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
@@ -19,6 +32,8 @@ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9D
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -39,33 +54,52 @@ github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDD
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/cel-go v0.17.1/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e h1:a+PGEeXb+exwBS3NboqXHyxarD9kaboBbrSp+7GuBuc=
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lyft/protoc-gen-star/v2 v2.0.4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
@@ -73,23 +107,31 @@ github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZd
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
@@ -101,28 +143,83 @@ github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd/api/v3 v3.6.12 h1:OLOZUKEuAA36TR48F0cIaa8FdzrWygjyfrJxXg4iDgs=
go.etcd.io/etcd/api/v3 v3.6.12/go.mod h1:p14EIQXHbuOQbVvL/WEes5uqKnxP9AgKJgpjbMVvzvE=
go.etcd.io/etcd/client/pkg/v3 v3.6.12 h1:36zzB+pQOdHbhN+kH2iJz/K8bJn0ZLtLfPPO7jozTDo=
go.etcd.io/etcd/client/pkg/v3 v3.6.12/go.mod h1:hh2+ZXtfLzs3o6mn92ntgNPBrTJJOvXqICM5g3L3DMY=
go.etcd.io/etcd/client/v3 v3.6.12 h1:kMSP6JcPZMqSJiX+TXdUIBU/4eXEZWBAaui4VihMbIc=
go.etcd.io/etcd/client/v3 v3.6.12/go.mod h1:CMs6fJWYiZQk4ytFjd4lE1diOvvRMmtbbn/alZXd3dQ=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=
gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3 h1:ctPmKL12ZsoKAlmPUsoW70zEDiYF+/H6aLieXxgAU0k=
google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3 h1:phvBWCAQMGN1945mp5fjCXP6jEF0+a0+4TjokS4sxNY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260618152121-87f3d3e198d3/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=

View File

@@ -153,7 +153,7 @@ platforms/
| 开放接口 | `api_` | `api_product``api_client``api_subscription` |
| 平台任务 | `sys_` | `sys_outbox_event``sys_dead_letter_event` |
- 每张表必须包含数据库内部使用的 `id bigint` 自增主键;主表还必须包含由应用生成、带唯一索引的 UUID V7 `identity varchar(36)`HTTP、消息、审计日志、Flutter/Vue 模型和跨服务引用只使用 `identity`,不得暴露或接受内部 `id`
- 每张表必须包含数据库内部使用的 `id bigint` 自增主键;主表还必须包含由应用生成、带唯一索引的 UUID V7 `identity varchar(36)`写接口、消息、审计日志、Flutter 模型和跨服务引用只使用 `identity`,不得接受内部 `id`三个受控管理后台的列表查询可以只读展示当前记录的主键 `id`,但详情、更新、删除、状态动作和关联选择仍必须使用 `identity`,不得把 `id` 作为写入条件或跨服务标识。
- 数据库内部关联优先使用 `<实体词根>_id` 指向自增主键;跨服务契约、异步事件和审计关联使用 `<实体词根>_identity`。业务展示编号另设唯一字段,不能替代 `id``identity`
- 每个主表还应按需要包含 `created_at``updated_at``created_by_identity``updated_by_identity``status``version` 等审计/并发字段;资金流水、安全事件、审计日志等不可变记录不得被物理删除。
- 数据库表、字段、索引、约束和枚举必须编写中文注释;注释说明业务含义、取值/单位、脱敏或留存要求。模型注释与接口契约必须同步维护,禁止只在设计文档中说明。

View File

@@ -91,6 +91,8 @@
## 4. 安全、隐私与合规
- 平台、气站和配送点三个受控管理后台的列表可以只读展示记录主键 `ID`,用于数据库问题定位;任何写操作、详情定位、关联选择、日志和跨服务传递仍统一使用 `identity`。服务端不得接受客户端指定新记录的 `id``identity`,创建时必须在逻辑层生成 UUID V7 `identity`
- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略。平台后台管理的平台、气站、配送、员工和业主账号密码按当前实施口径仅要求不少于 6 个字符,不附加复杂度校验。
- 权限校验在服务端执行,前端菜单隐藏不构成权限控制。按角色、站点、区域、对象归属联合鉴权。
- 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、显示脱敏、访问留痕、最小化留存。

View File

@@ -199,6 +199,28 @@ const fieldLabels: Record<string, string> = {
path: '路由',
menu_identities: '菜单权限',
status: '状态',
confirm_type: '确认方式',
delivery_basic_identity: '配送点唯一标识',
description: '说明',
ec_category_identity: '商品分类唯一标识',
ec_product_identity: '商品唯一标识',
gas_basic_identity: '气站唯一标识',
gasorder_contract_identity: '配送合同唯一标识',
gasorder_contract_product_identities: '合同气瓶唯一标识',
producer_account_identity: '生产商唯一标识',
product_info_identity: '智能气阀唯一标识',
product_type_identity: '智能气阀类型唯一标识',
proof_uri: '凭证地址',
recipient_name: '签收人姓名',
recipient_phone: '签收人电话',
staff_account_identity: '工作人员唯一标识',
subject_identity: '结算主体唯一标识',
user_account_identity: '用户唯一标识',
user_address_identity: '用户地址唯一标识',
wallet_bank_identity: '银行卡唯一标识',
wallet_basic_identity: '钱包唯一标识',
warehouse_identity: '库房唯一标识',
withdrawable: '计入可提现余额',
};
const numbers = new Set([
@@ -224,6 +246,8 @@ const textareas = new Set([
]);
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
const label = fieldLabels[key];
if (!label) throw new Error(`资源字段缺少中文名称:${key}`);
const type: ResourceFieldType = key.endsWith('_identity')
? 'identity'
: money.has(key)
@@ -241,7 +265,7 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
: key === 'password'
? 'password'
: 'text';
return { key, label: fieldLabels[key] ?? key, type, ...options };
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {

View File

@@ -1,16 +0,0 @@
<template>
<a-layout-footer class="footer">和气配送点管理系统</a-layout-footer>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
color: var(--color-text-2);
text-align: center;
}
</style>

View File

@@ -157,5 +157,11 @@ export default defineComponent({
font-size: 18px;
}
}
.arco-menu-item.arco-menu-selected {
color: rgb(var(--primary-6));
background-color: var(--color-primary-light-1);
box-shadow: inset 3px 0 0 rgb(var(--primary-6));
font-weight: 600;
}
}
</style>

View File

@@ -37,7 +37,6 @@
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
</a-layout>
@@ -47,7 +46,6 @@
<script lang="ts" setup>
import { computed, onMounted, provide, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Footer from '@/components/footer/index.vue';
import Menu from '@/components/menu/index.vue';
import NavBar from '@/components/navbar/index.vue';
import TabBar from '@/components/tab-bar/index.vue';
@@ -67,7 +65,6 @@ const navbarHeight = `60px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
const hideMenu = computed(() => appStore.hideMenu);
const footer = computed(() => appStore.footer);
const menuWidth = computed(() => {
return appStore.menuCollapse ? 48 : appStore.menuWidth;
});

View File

@@ -9,7 +9,6 @@
<a-grid-item v-for="item in cards" :key="item.key">
<a-card :bordered="false" class="metric-card">
<a-statistic :title="item.label" :value="overview[item.key]" show-group-separator />
<div class="metric-hint">{{ item.hint }}</div>
</a-card>
</a-grid-item>
</a-grid>
@@ -25,9 +24,9 @@ const errorMessage = ref('');
const updatedAt = ref('');
const updatedAtLabel = computed(() => updatedAt.value || (errorMessage.value ? '加载失败' : '加载中…'));
const overview = reactive<Overview>({ staff_count: 0, user_count: 0, contract_count: 0, order_count: 0 });
const cards: Array<{ key: keyof Overview; label: string; hint: string }> = [
{ key: 'staff_count', label: '配送人员', hint: '本站关联配送人员' }, { key: 'user_count', label: '服务用户', hint: '本站有效服务关系' },
{ key: 'contract_count', label: '配送合同', hint: '本站合同总量' }, { key: 'order_count', label: '配送订单', hint: '本站订单总量' },
const cards: Array<{ key: keyof Overview; label: string }> = [
{ key: 'staff_count', label: '配送人员' }, { key: 'user_count', label: '服务用户' },
{ key: 'contract_count', label: '配送合同' }, { key: 'order_count', label: '配送订单' },
];
async function loadOverview() {
loading.value = true;
@@ -45,6 +44,5 @@ onMounted(loadOverview);
<style scoped lang="less">
.dashboard-alert { margin-bottom: 16px; }
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
.metric-card { min-height: 132px; }
.metric-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--color-border-2); color: var(--color-text-2); font-size: 13px; line-height: 20px; }
.metric-card { min-height: 112px; }
</style>

View File

@@ -6,7 +6,6 @@
<main class="login-card" aria-label="和气配送点管理系统登录">
<img class="login-card-logo" :src="logoUrl" alt="和气" />
<LoginForm />
<div class="login-footer">© 2026 和气 · 安全连接每一程</div>
</main>
</div>
</template>
@@ -61,16 +60,6 @@ import LoginForm from './components/login-form.vue';
margin: 0 auto 14px;
}
.login-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(17, 77, 73, 0.09);
color: #839694;
font-size: 12px;
line-height: 20px;
text-align: center;
}
@media (max-width: @screen-sm) {
.login-page {
padding: 82px 16px 20px;

View File

@@ -31,8 +31,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="ID" data-index="id" :width="100" />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" ellipsis tooltip>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
<template #cell="{ record }">
{{ displayFieldValue(field, record) }}
</template>
@@ -95,9 +96,11 @@
</div>
<a-table :data="accountList" :loading="accountLoading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="用户名" data-index="username" />
<a-table-column title="显示名称" data-index="display_name" />
<a-table-column title="角色编码" data-index="role_code" />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column title="用户名" data-index="username" :width="160" />
<a-table-column title="显示名称" data-index="display_name" :width="180" />
<a-table-column title="角色编码" data-index="role_code" :width="140" />
<a-table-column title="状态" :width="90">
<template #cell="{ record }">
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
@@ -282,7 +285,7 @@ const loading = ref(false);
const saving = ref(false);
const actionSubmitting = ref(false);
const page = ref(Math.max(1, Number(route.query.page) || 1));
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
@@ -292,7 +295,7 @@ const accountLoading = ref(false);
const accountList = ref<Row[]>([]);
const accountOwner = ref<Row>({});
const accountPage = ref(1);
const accountPageSize = 10;
const accountPageSize = 50;
const accountTotal = ref(0);
const accountFormVisible = ref(false);
const accountSaving = ref(false);
@@ -362,6 +365,7 @@ const formMode = computed<'create' | 'edit'>(() =>
const formFields = computed(() =>
props.definition.fields.filter(
(field) =>
field.key !== 'identity' &&
(formMode.value === 'create' || field.type !== 'password') &&
!(
formMode.value === 'edit' &&
@@ -375,6 +379,7 @@ const displayFields = computed(() =>
props.definition.fields
.filter(
(field) =>
field.key !== 'identity' &&
field.key !== 'password' &&
!(
props.definition.name === 'gas_basic' &&
@@ -1128,12 +1133,26 @@ watch(
function optionLabel(option: Row) {
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
}
function columnWidth(field: ResourceField) {
if (field.key === 'identity') return 280;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
</script>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.workflow-alert,
.action-alert {
margin-bottom: 16px;

View File

@@ -7,8 +7,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" :width="columnWidth(field)" ellipsis tooltip />
<a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column>
</template>
</a-table>
@@ -35,13 +36,13 @@ import { Message } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
import type { DetailAction, ResourceField, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
@@ -51,8 +52,20 @@ const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
props.definition.fields.filter((field) => field.key !== 'identity' && field.key !== 'status'),
);
function columnWidth(field: ResourceField) {
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),

View File

@@ -199,6 +199,28 @@ const fieldLabels: Record<string, string> = {
path: '路由',
menu_identities: '菜单权限',
status: '状态',
confirm_type: '确认方式',
delivery_basic_identity: '配送点唯一标识',
description: '说明',
ec_category_identity: '商品分类唯一标识',
ec_product_identity: '商品唯一标识',
gas_basic_identity: '气站唯一标识',
gasorder_contract_identity: '配送合同唯一标识',
gasorder_contract_product_identities: '合同气瓶唯一标识',
producer_account_identity: '生产商唯一标识',
product_info_identity: '智能气阀唯一标识',
product_type_identity: '智能气阀类型唯一标识',
proof_uri: '凭证地址',
recipient_name: '签收人姓名',
recipient_phone: '签收人电话',
staff_account_identity: '工作人员唯一标识',
subject_identity: '结算主体唯一标识',
user_account_identity: '用户唯一标识',
user_address_identity: '用户地址唯一标识',
wallet_bank_identity: '银行卡唯一标识',
wallet_basic_identity: '钱包唯一标识',
warehouse_identity: '库房唯一标识',
withdrawable: '计入可提现余额',
};
const numbers = new Set([
@@ -224,6 +246,8 @@ const textareas = new Set([
]);
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
const label = fieldLabels[key];
if (!label) throw new Error(`资源字段缺少中文名称:${key}`);
const type: ResourceFieldType = key.endsWith('_identity')
? 'identity'
: money.has(key)
@@ -241,7 +265,7 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
: key === 'password'
? 'password'
: 'text';
return { key, label: fieldLabels[key] ?? key, type, ...options };
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {

View File

@@ -1,16 +0,0 @@
<template>
<a-layout-footer class="footer">和气气站管理系统</a-layout-footer>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
color: var(--color-text-2);
text-align: center;
}
</style>

View File

@@ -157,5 +157,11 @@ export default defineComponent({
font-size: 18px;
}
}
.arco-menu-item.arco-menu-selected {
color: rgb(var(--primary-6));
background-color: var(--color-primary-light-1);
box-shadow: inset 3px 0 0 rgb(var(--primary-6));
font-weight: 600;
}
}
</style>

View File

@@ -37,7 +37,6 @@
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
</a-layout>
@@ -47,7 +46,6 @@
<script lang="ts" setup>
import { computed, onMounted, provide, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Footer from '@/components/footer/index.vue';
import Menu from '@/components/menu/index.vue';
import NavBar from '@/components/navbar/index.vue';
import TabBar from '@/components/tab-bar/index.vue';
@@ -67,7 +65,6 @@ const navbarHeight = `60px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
const hideMenu = computed(() => appStore.hideMenu);
const footer = computed(() => appStore.footer);
const menuWidth = computed(() => {
return appStore.menuCollapse ? 48 : appStore.menuWidth;
});

View File

@@ -9,7 +9,6 @@
<a-grid-item v-for="item in cards" :key="item.key">
<a-card :bordered="false" class="metric-card">
<a-statistic :title="item.label" :value="overview[item.key]" show-group-separator />
<div class="metric-hint">{{ item.hint }}</div>
</a-card>
</a-grid-item>
</a-grid>
@@ -27,12 +26,12 @@ const errorMessage = ref('');
const updatedAt = ref('');
const updatedAtLabel = computed(() => updatedAt.value || (errorMessage.value ? '加载失败' : '加载中…'));
const overview = reactive<Overview>({ delivery_count: 0, staff_count: 0, user_count: 0, contract_count: 0, order_count: 0 });
const cards: Array<{ key: keyof Overview; label: string; hint: string }> = [
{ key: 'delivery_count', label: '配送点', hint: '当前气站服务范围' },
{ key: 'staff_count', label: '工作人员', hint: '本站关联人员' },
{ key: 'user_count', label: '服务用户', hint: '本站有效服务关系' },
{ key: 'contract_count', label: '配送合同', hint: '本站合同总量' },
{ key: 'order_count', label: '燃气配送订单', hint: '本站订单总量' },
const cards: Array<{ key: keyof Overview; label: string }> = [
{ key: 'delivery_count', label: '配送点' },
{ key: 'staff_count', label: '工作人员' },
{ key: 'user_count', label: '服务用户' },
{ key: 'contract_count', label: '配送合同' },
{ key: 'order_count', label: '燃气配送订单' },
];
async function loadOverview() {
loading.value = true;
@@ -53,6 +52,5 @@ onMounted(loadOverview);
<style scoped lang="less">
.dashboard-alert { margin-bottom: 16px; }
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
.metric-card { min-height: 132px; }
.metric-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--color-border-2); color: var(--color-text-2); font-size: 13px; line-height: 20px; }
.metric-card { min-height: 112px; }
</style>

View File

@@ -6,7 +6,6 @@
<main class="login-card" aria-label="和气气站管理系统登录">
<img class="login-card-logo" :src="logoUrl" alt="和气" />
<LoginForm />
<div class="login-footer">© 2026 和气 · 安全连接每一程</div>
</main>
</div>
</template>
@@ -61,16 +60,6 @@ import LoginForm from './components/login-form.vue';
margin: 0 auto 14px;
}
.login-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(74, 43, 21, 0.09);
color: #978b82;
font-size: 12px;
line-height: 20px;
text-align: center;
}
@media (max-width: @screen-sm) {
.login-page {
padding: 82px 16px 20px;

View File

@@ -31,8 +31,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="ID" data-index="id" :width="100" />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" ellipsis tooltip>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
<template #cell="{ record }">
{{ displayFieldValue(field, record) }}
</template>
@@ -95,9 +96,11 @@
</div>
<a-table :data="accountList" :loading="accountLoading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="用户名" data-index="username" />
<a-table-column title="显示名称" data-index="display_name" />
<a-table-column title="角色编码" data-index="role_code" />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column title="用户名" data-index="username" :width="160" />
<a-table-column title="显示名称" data-index="display_name" :width="180" />
<a-table-column title="角色编码" data-index="role_code" :width="140" />
<a-table-column title="状态" :width="90">
<template #cell="{ record }">
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
@@ -282,7 +285,7 @@ const loading = ref(false);
const saving = ref(false);
const actionSubmitting = ref(false);
const page = ref(Math.max(1, Number(route.query.page) || 1));
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
@@ -292,7 +295,7 @@ const accountLoading = ref(false);
const accountList = ref<Row[]>([]);
const accountOwner = ref<Row>({});
const accountPage = ref(1);
const accountPageSize = 10;
const accountPageSize = 50;
const accountTotal = ref(0);
const accountFormVisible = ref(false);
const accountSaving = ref(false);
@@ -362,6 +365,7 @@ const formMode = computed<'create' | 'edit'>(() =>
const formFields = computed(() =>
props.definition.fields.filter(
(field) =>
field.key !== 'identity' &&
(formMode.value === 'create' || field.type !== 'password') &&
!(
formMode.value === 'edit' &&
@@ -375,6 +379,7 @@ const displayFields = computed(() =>
props.definition.fields
.filter(
(field) =>
field.key !== 'identity' &&
field.key !== 'password' &&
!(
props.definition.name === 'gas_basic' &&
@@ -1128,12 +1133,26 @@ watch(
function optionLabel(option: Row) {
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
}
function columnWidth(field: ResourceField) {
if (field.key === 'identity') return 280;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
</script>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.workflow-alert,
.action-alert {
margin-bottom: 16px;

View File

@@ -7,8 +7,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" :width="columnWidth(field)" ellipsis tooltip />
<a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column>
</template>
</a-table>
@@ -35,13 +36,13 @@ import { Message } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
import type { DetailAction, ResourceField, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
@@ -51,8 +52,20 @@ const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
props.definition.fields.filter((field) => field.key !== 'identity' && field.key !== 'status'),
);
function columnWidth(field: ResourceField) {
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),

View File

@@ -204,6 +204,28 @@ const fieldLabels: Record<string, string> = {
path: '路由',
menu_identities: '菜单权限',
status: '状态',
confirm_type: '确认方式',
delivery_basic_identity: '配送点唯一标识',
description: '说明',
ec_category_identity: '商品分类唯一标识',
ec_product_identity: '商品唯一标识',
gas_basic_identity: '气站唯一标识',
gasorder_contract_identity: '配送合同唯一标识',
gasorder_contract_product_identities: '合同气瓶唯一标识',
producer_account_identity: '生产商唯一标识',
product_info_identity: '智能气阀唯一标识',
product_type_identity: '智能气阀类型唯一标识',
proof_uri: '凭证地址',
recipient_name: '签收人姓名',
recipient_phone: '签收人电话',
staff_account_identity: '工作人员唯一标识',
subject_identity: '结算主体唯一标识',
user_account_identity: '用户唯一标识',
user_address_identity: '用户地址唯一标识',
wallet_bank_identity: '银行卡唯一标识',
wallet_basic_identity: '钱包唯一标识',
warehouse_identity: '库房唯一标识',
withdrawable: '计入可提现余额',
};
const numbers = new Set([
@@ -229,6 +251,8 @@ const textareas = new Set([
]);
function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
const label = fieldLabels[key];
if (!label) throw new Error(`资源字段缺少中文名称:${key}`);
const type: ResourceFieldType = key.endsWith('_identity')
? 'identity'
: money.has(key)
@@ -246,7 +270,7 @@ function f(key: string, options: Partial<ResourceField> = {}): ResourceField {
: key === 'password'
? 'password'
: 'text';
return { key, label: fieldLabels[key] ?? key, type, ...options };
return { key, label, type, ...options };
}
function relation(key: string, resource: string, required = false): ResourceField {

View File

@@ -1,16 +0,0 @@
<template>
<a-layout-footer class="footer">和气平台总后台</a-layout-footer>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
color: var(--color-text-2);
text-align: center;
}
</style>

View File

@@ -157,5 +157,11 @@ export default defineComponent({
font-size: 18px;
}
}
.arco-menu-item.arco-menu-selected {
color: rgb(var(--primary-6));
background-color: var(--color-primary-light-1);
box-shadow: inset 3px 0 0 rgb(var(--primary-6));
font-weight: 600;
}
}
</style>

View File

@@ -37,7 +37,6 @@
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
</a-layout>
@@ -47,7 +46,6 @@
<script lang="ts" setup>
import { computed, onMounted, provide, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Footer from '@/components/footer/index.vue';
import Menu from '@/components/menu/index.vue';
import NavBar from '@/components/navbar/index.vue';
import TabBar from '@/components/tab-bar/index.vue';
@@ -67,7 +65,6 @@ const navbarHeight = `60px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
const hideMenu = computed(() => appStore.hideMenu);
const footer = computed(() => appStore.footer);
const menuWidth = computed(() => {
return appStore.menuCollapse ? 48 : appStore.menuWidth;
});

View File

@@ -17,7 +17,6 @@
>
<template v-if="item.money" #prefix>¥</template>
</a-statistic>
<div class="metric-hint">{{ item.hint }}</div>
</a-card>
</a-grid-item>
</a-grid>
@@ -69,15 +68,15 @@ const updatedAtLabel = computed(() => updatedAt.value || (errorMessage.value ? '
const overview = ref<DashboardOverview>(emptyOverview());
const router = useRouter();
const userStore = useUserStore();
const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] = [
{ key: 'gas_basic_count', label: '气站总数', hint: '正常运营气站总数' },
{ key: 'delivery_basic_count', label: '配送点总数', hint: '正常运营配送点总数' },
{ key: 'user_count', label: '客户总数', hint: '有效客户账户总数' },
{ key: 'product_count', label: '智能气阀', hint: '平台智能气阀总数' },
{ key: 'today_order_count', label: '今日订单', hint: '自然日新增' },
{ key: 'today_order_amount', label: '今日应付金额', hint: '订单口径', money: true },
{ key: 'pending_ticket_count', label: '待受理工单', hint: '客服待办' },
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
const cards: { key: CountKey; label: string; money?: boolean }[] = [
{ key: 'gas_basic_count', label: '气站总数' },
{ key: 'delivery_basic_count', label: '配送点总数' },
{ key: 'user_count', label: '客户总数' },
{ key: 'product_count', label: '智能气阀' },
{ key: 'today_order_count', label: '今日订单' },
{ key: 'today_order_amount', label: '今日应付金额', money: true },
{ key: 'pending_ticket_count', label: '待受理工单' },
{ key: 'paid_amount', label: '累计实收金额', money: true },
];
const actions = [
{ label: '新建气站', route: 'organization-gas-basic', menu: 'gas_basic', icon: IconPlus },
@@ -148,8 +147,7 @@ onMounted(loadOverview);
.dashboard-spin { width: 100%; }
.dashboard-alert { margin-bottom: 16px; }
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
.metric-card { min-height: 132px; }
.metric-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--color-border-2); color: var(--color-text-2); font-size: 13px; line-height: 20px; }
.metric-card { min-height: 112px; }
.section-card { margin-top: 16px; }
.quick-action { height: 52px; justify-content: flex-start; padding: 0 18px; }
</style>

View File

@@ -6,7 +6,6 @@
<main class="login-card" aria-label="和气平台总后台登录">
<img class="login-card-logo" :src="logoUrl" alt="和气" />
<LoginForm />
<div class="login-footer">© 2026 和气 · 安全连接每一程</div>
</main>
</div>
</template>
@@ -61,16 +60,6 @@ import LoginForm from './components/login-form.vue';
margin: 0 auto 14px;
}
.login-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(29, 44, 75, 0.08);
color: #8a94a6;
font-size: 12px;
line-height: 20px;
text-align: center;
}
@media (max-width: @screen-sm) {
.login-page {
padding: 82px 16px 20px;

View File

@@ -31,8 +31,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="ID" data-index="id" :width="100" />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" ellipsis tooltip>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :width="columnWidth(field)" ellipsis tooltip>
<template #cell="{ record }">
{{ displayFieldValue(field, record) }}
</template>
@@ -95,9 +96,11 @@
</div>
<a-table :data="accountList" :loading="accountLoading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="用户名" data-index="username" />
<a-table-column title="显示名称" data-index="display_name" />
<a-table-column title="角色编码" data-index="role_code" />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column title="用户名" data-index="username" :width="160" />
<a-table-column title="显示名称" data-index="display_name" :width="180" />
<a-table-column title="角色编码" data-index="role_code" :width="140" />
<a-table-column title="状态" :width="90">
<template #cell="{ record }">
<a-tag :color="Number(record.status) === 1 ? 'green' : 'red'">
@@ -282,7 +285,7 @@ const loading = ref(false);
const saving = ref(false);
const actionSubmitting = ref(false);
const page = ref(Math.max(1, Number(route.query.page) || 1));
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
@@ -292,7 +295,7 @@ const accountLoading = ref(false);
const accountList = ref<Row[]>([]);
const accountOwner = ref<Row>({});
const accountPage = ref(1);
const accountPageSize = 10;
const accountPageSize = 50;
const accountTotal = ref(0);
const accountFormVisible = ref(false);
const accountSaving = ref(false);
@@ -362,6 +365,7 @@ const formMode = computed<'create' | 'edit'>(() =>
const formFields = computed(() =>
props.definition.fields.filter(
(field) =>
field.key !== 'identity' &&
(formMode.value === 'create' || field.type !== 'password') &&
!(
formMode.value === 'edit' &&
@@ -375,6 +379,7 @@ const displayFields = computed(() =>
props.definition.fields
.filter(
(field) =>
field.key !== 'identity' &&
field.key !== 'password' &&
!(
props.definition.name === 'gas_basic' &&
@@ -1128,12 +1133,26 @@ watch(
function optionLabel(option: Row) {
return String(option.name ?? option.title ?? option.code ?? option.username ?? option.contract_no ?? option.order_no ?? option.identity);
}
function columnWidth(field: ResourceField) {
if (field.key === 'identity') return 280;
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
</script>
<style scoped lang="less">
.filters {
margin-bottom: 16px;
}
.workflow-alert,
.action-alert {
margin-bottom: 16px;

View File

@@ -7,8 +7,9 @@
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" ellipsis tooltip />
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="唯一标识" data-index="identity" :width="280" ellipsis tooltip />
<a-table-column v-for="field in displayFields" :key="field.key" :title="field.label" :data-index="field.key" :width="columnWidth(field)" ellipsis tooltip />
<a-table-column title="操作" :width="90"><template #cell="{ record }"><a-button size="mini" @click="openDetail(record)">详情</a-button></template></a-table-column>
</template>
</a-table>
@@ -35,13 +36,13 @@ import { Message } from '@arco-design/web-vue';
import { computed, onMounted, reactive, ref } from 'vue';
import { resourceApi } from '@/api/resource';
import { buildResourcePayload, isMissingField } from '@/api/resource-form';
import type { DetailAction, ResourceUiDefinition } from '@/api/resources';
import type { DetailAction, ResourceField, ResourceUiDefinition } from '@/api/resources';
type Row = Record<string, unknown>;
const props = defineProps<{ definition: ResourceUiDefinition }>();
const loading = ref(false);
const page = ref(1);
const pageSize = 20;
const pageSize = 50;
const total = ref(0);
const list = ref<Row[]>([]);
const filters = reactive({ keyword: '' });
@@ -51,8 +52,20 @@ const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const actionForm = reactive<Record<string, any>>({});
const displayFields = computed(() =>
props.definition.fields.filter((field) => field.key !== 'status'),
props.definition.fields.filter((field) => field.key !== 'identity' && field.key !== 'status'),
);
function columnWidth(field: ResourceField) {
if (field.type === 'datetime' || field.type === 'date') return 180;
if (field.type === 'money' || field.type === 'number') return 140;
if (field.type === 'boolean') return 110;
if (field.type === 'select') return 140;
if (field.type === 'identity' || field.type === 'identity-list') return 240;
if (field.type === 'textarea') return 280;
if (field.key.includes('phone')) return 150;
if (/(name|title|address|content|remark|reason|terms)$/.test(field.key)) return 220;
return 180;
}
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
([key]) => key !== 'id' && !key.endsWith('_id'),