2026-07-28 10:42:26 +08:00
|
|
|
// 平台 API 命令行工具。
|
2026-07-26 18:24:13 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-29 16:25:03 +08:00
|
|
|
"encoding/json"
|
2026-07-26 18:24:13 +08:00
|
|
|
"fmt"
|
2026-07-29 16:25:03 +08:00
|
|
|
"io"
|
2026-07-26 18:24:13 +08:00
|
|
|
"os"
|
2026-07-29 16:25:03 +08:00
|
|
|
"strings"
|
2026-07-28 22:42:21 +08:00
|
|
|
|
|
|
|
|
"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"
|
2026-07-30 14:16:58 +08:00
|
|
|
deliverylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/delivery"
|
|
|
|
|
gaslogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/gas"
|
2026-07-29 16:25:03 +08:00
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
|
2026-07-29 18:38:01 +08:00
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
2026-07-29 16:25:03 +08:00
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
|
2026-07-28 22:42:21 +08:00
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/seed"
|
2026-07-29 16:25:03 +08:00
|
|
|
"github.com/gin-gonic/gin"
|
2026-07-29 18:38:01 +08:00
|
|
|
"gorm.io/gorm"
|
2026-07-26 18:24:13 +08:00
|
|
|
)
|
|
|
|
|
|
2026-07-28 22:42:21 +08:00
|
|
|
const serviceKey = "heqi"
|
|
|
|
|
|
2026-07-26 18:24:13 +08:00
|
|
|
func main() {
|
|
|
|
|
if len(os.Args) < 2 {
|
2026-07-28 10:42:26 +08:00
|
|
|
printUsage()
|
|
|
|
|
os.Exit(1)
|
2026-07-26 18:24:13 +08:00
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-07-30 14:16:58 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-07-28 22:42:21 +08:00
|
|
|
case "mock-data":
|
|
|
|
|
if err := writeMockData(); err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println("mock data written successfully")
|
2026-07-29 18:38:01 +08:00
|
|
|
case "migrate":
|
|
|
|
|
if err := migrateDatabase(); err != nil {
|
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println("database migrated successfully")
|
2026-07-28 10:42:26 +08:00
|
|
|
default:
|
2026-07-26 18:24:13 +08:00
|
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
2026-07-28 10:42:26 +08:00
|
|
|
printUsage()
|
2026-07-26 18:24:13 +08:00
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func printUsage() {
|
2026-07-30 14:16:58 +08:00
|
|
|
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data>")
|
2026-07-28 22:42:21 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-29 16:25:03 +08:00
|
|
|
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})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 14:16:58 +08:00
|
|
|
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})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 22:42:21 +08:00
|
|
|
func writeMockData() error {
|
2026-07-28 23:27:56 +08:00
|
|
|
config.New("platform")
|
2026-07-28 22:42:21 +08:00
|
|
|
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
|
2026-07-26 18:24:13 +08:00
|
|
|
}
|
2026-07-29 18:38:01 +08:00
|
|
|
|
|
|
|
|
func migrateDatabase() error {
|
|
|
|
|
config.New("platform")
|
|
|
|
|
if config.Spec.Databases == nil {
|
|
|
|
|
return fmt.Errorf("database configuration is required")
|
|
|
|
|
}
|
|
|
|
|
var migrationDatabase interface {
|
|
|
|
|
Migrator() gorm.Migrator
|
|
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
switch strings.ToLower(config.Spec.Databases.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)
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|