251 lines
7.9 KiB
Go
251 lines
7.9 KiB
Go
// 平台 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
|
||
}
|