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

251 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 平台 API 命令行工具。
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"git.apinb.com/bsm-sdk/core/database"
dbsql "git.apinb.com/bsm-sdk/core/database/sql"
"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/impl"
"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,
dbsql.SetOptions(nil),
)
if err != nil {
return fmt.Errorf("connect database: %w", err)
}
impl.DBService = databaseService
if err := initdb.New(); 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(serviceKey)
options := &types.SqlOptions{
MaxIdleConns: vars.SqlOptionMaxIdleConns,
MaxOpenConns: vars.SqlOptionMaxOpenConns,
ConnMaxLifetime: vars.SqlOptionConnMaxLifetime,
IsAutoMigrate: true,
LogStdout: false,
Debug: true,
}
_, err := database.NewDatabase(
config.Spec.Databases.Driver,
config.Spec.Databases.Source,
options,
)
if err != nil {
return fmt.Errorf("migrate database: %w", err)
}
return initdb.New()
}
// rejectLegacyPaymentSchema 阻止通用迁移物理删除历史资金表。
// 旧支付事实必须通过单独评审的数据迁移保留和对账,不能由 migrate 命令静默重置。
func rejectLegacyPaymentSchema(databaseService *gorm.DB) error {
for _, table := range []string{"wallet_refund", "wallet_payment", "gasorder_payment"} {
if databaseService.Migrator().HasTable(table) {
return fmt.Errorf("检测到历史资金表 %s请先执行经财务与审计批准的保留式迁移", table)
}
}
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
}