Files
platforms/backend/api/cmd/cli/main.go

280 lines
8.9 KiB
Go

// 平台 API 命令行工具。
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"git.apinb.com/bsm-sdk/core/database"
"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"
gaslogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/gas"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
"git.apinb.com/heqiapp/platforms/backend/api/internal/seed"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const serviceKey = "heqi"
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
switch os.Args[1] {
case "version":
fmt.Println("platform-cli 0.1.0")
case "resource-contract":
if err := writeResourceContract(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "gas-resource-contract":
if err := writeGasResourceContract(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "delivery-resource-contract":
if err := writeDeliveryResourceContract(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "mock-data":
if err := writeMockData(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("mock data written successfully")
case "migrate":
if err := migrateDatabase(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("database migrated successfully")
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data>")
}
type route struct {
Method string `json:"method"`
Path string `json:"path"`
}
type contract struct {
Domain string `json:"domain"`
Name string `json:"name"`
Path string `json:"path"`
PageKind string `json:"pageKind"`
Mode string `json:"mode"`
}
type manifest struct {
Resources []contract `json:"resources"`
Routes []route `json:"routes"`
}
func writeResourceContract(output io.Writer) error {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
routers.RegisterPlatform(serviceKey, engine)
routes := make([]route, 0, len(engine.Routes()))
for _, item := range engine.Routes() {
routes = append(routes, route{
Method: item.Method,
Path: strings.TrimPrefix(item.Path, "/"+serviceKey+"/platform/v1"),
})
}
expected := platform.ExpectedResources()
contracts := make([]contract, 0, len(expected))
for _, item := range expected {
contracts = append(contracts, contract{
Domain: item.Domain, Name: item.Name, Path: item.Path,
PageKind: item.PageKind, Mode: string(item.Mode),
})
}
return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes})
}
func writeGasResourceContract(output io.Writer) error {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
routers.RegisterGas(serviceKey, engine)
routes := make([]route, 0, len(engine.Routes()))
for _, item := range engine.Routes() {
routes = append(routes, route{
Method: item.Method,
Path: strings.TrimPrefix(item.Path, "/"+serviceKey+"/gas/v1"),
})
}
expected := gaslogic.ExpectedResources()
contracts := make([]contract, 0, len(expected))
for _, item := range expected {
contracts = append(contracts, contract{
Domain: item.Domain, Name: item.Name, Path: item.Path,
PageKind: item.PageKind, Mode: string(item.Mode),
})
}
return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes})
}
func writeDeliveryResourceContract(output io.Writer) error {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
routers.RegisterDelivery(serviceKey, engine)
routes := make([]route, 0, len(engine.Routes()))
for _, item := range engine.Routes() {
routes = append(routes, route{
Method: item.Method,
Path: strings.TrimPrefix(item.Path, "/"+serviceKey+"/delivery/v1"),
})
}
expected := deliverylogic.ExpectedResources()
contracts := make([]contract, 0, len(expected))
for _, item := range expected {
contracts = append(contracts, contract{
Domain: item.Domain, Name: item.Name, Path: item.Path, PageKind: item.PageKind, Mode: item.Mode,
})
}
return json.NewEncoder(output).Encode(manifest{Resources: contracts, Routes: routes})
}
func writeMockData() error {
config.New("heqi")
if config.Spec.Databases == nil {
return fmt.Errorf("database configuration is required")
}
databaseService, err := database.NewDatabase(
config.Spec.Databases.Driver,
config.Spec.Databases.Source,
nil,
)
if err != nil {
return fmt.Errorf("connect database: %w", err)
}
if err := initdb.New(databaseService); err != nil {
return fmt.Errorf("initialize platform data: %w", err)
}
if err := seed.MockData(databaseService); err != nil {
return fmt.Errorf("write mock data: %w", err)
}
return nil
}
func migrateDatabase() error {
config.New("heqi")
if config.Spec.Databases == nil {
return fmt.Errorf("database configuration is required")
}
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 := resetLegacyPaymentSchema(migrationDatabase, driver); 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(
config.Spec.Databases.Driver,
config.Spec.Databases.Source,
nil,
)
if err != nil {
return fmt.Errorf("migrate database: %w", err)
}
return initdb.New(databaseService)
}
// resetLegacyPaymentSchema 执行经业务明确授权的开发期破坏性支付模型重置,不迁移旧支付或退款历史。
func resetLegacyPaymentSchema(databaseService *gorm.DB, driver string) error {
statements := []string{}
if driver == "postgres" {
statements = append(statements,
`DROP TABLE IF EXISTS "wallet_refund" CASCADE`,
`DROP TABLE IF EXISTS "wallet_payment" CASCADE`,
`DROP TABLE IF EXISTS "gasorder_payment" CASCADE`,
)
} else {
statements = append(statements,
`DROP TABLE IF EXISTS wallet_refund`,
`DROP TABLE IF EXISTS wallet_payment`,
`DROP TABLE IF EXISTS gasorder_payment`,
)
}
for _, statement := range statements {
if err := databaseService.Exec(statement).Error; err != nil {
return fmt.Errorf("reset legacy payment schema: %w", err)
}
}
return nil
}
// prepareAdditiveMigrations 先处理无法由 GORM AutoMigrate 安全完成的新增非空字段。
// 旧轨迹没有服务端接收时间时,以定位发生时间(再退化到创建时间)回填,
// 避免直接 ADD NOT NULL 因历史行存在而中断整库迁移。
func prepareAdditiveMigrations(databaseService *gorm.DB, driver string) error {
if !databaseService.Migrator().HasTable(&models.GasorderTrackPoint{}) {
return nil
}
var statements []string
hasReceivedAt := databaseService.Migrator().HasColumn(&models.GasorderTrackPoint{}, "received_at")
switch driver {
case "postgres":
if !hasReceivedAt {
statements = append(statements, `ALTER TABLE "gasorder_track_point" ADD COLUMN "received_at" timestamptz`)
}
statements = append(statements,
`UPDATE "gasorder_track_point" SET "received_at" = COALESCE("occurred_at", "created_at", CURRENT_TIMESTAMP) WHERE "received_at" IS NULL`,
`ALTER TABLE "gasorder_track_point" ALTER COLUMN "received_at" SET NOT NULL`,
)
case "mysql":
if !hasReceivedAt {
statements = append(statements, "ALTER TABLE `gasorder_track_point` ADD COLUMN `received_at` datetime(3) NULL")
}
statements = append(statements,
"UPDATE `gasorder_track_point` SET `received_at` = COALESCE(`occurred_at`, `created_at`, CURRENT_TIMESTAMP(3)) WHERE `received_at` IS NULL",
"ALTER TABLE `gasorder_track_point` MODIFY COLUMN `received_at` datetime(3) NOT NULL",
)
default:
return fmt.Errorf("unsupported database driver for additive migrations: %s", driver)
}
for _, statement := range statements {
if err := databaseService.Exec(statement).Error; err != nil {
return fmt.Errorf("backfill gasorder_track_point.received_at: %w", err)
}
}
return nil
}