完善后端联调链路与模拟器多通道支持
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"cmr-backend/internal/httpapi"
|
||||
"cmr-backend/internal/platform/assets"
|
||||
"cmr-backend/internal/platform/jwtx"
|
||||
"cmr-backend/internal/platform/wechatmini"
|
||||
"cmr-backend/internal/service"
|
||||
@@ -38,7 +39,8 @@ func New(ctx context.Context, cfg Config) (*App, error) {
|
||||
entryHomeService := service.NewEntryHomeService(store)
|
||||
eventService := service.NewEventService(store)
|
||||
eventPlayService := service.NewEventPlayService(store)
|
||||
configService := service.NewConfigService(store, cfg.LocalEventDir, cfg.AssetBaseURL)
|
||||
assetPublisher := assets.NewOSSUtilPublisher(cfg.OSSUtilPath, cfg.OSSUtilConfigFile, cfg.AssetBucketRoot, cfg.AssetPublicBaseURL)
|
||||
configService := service.NewConfigService(store, cfg.LocalEventDir, cfg.AssetBaseURL, assetPublisher)
|
||||
homeService := service.NewHomeService(store)
|
||||
profileService := service.NewProfileService(store)
|
||||
resultService := service.NewResultService(store)
|
||||
|
||||
@@ -24,6 +24,10 @@ type Config struct {
|
||||
WechatMiniDevPrefix string
|
||||
LocalEventDir string
|
||||
AssetBaseURL string
|
||||
AssetPublicBaseURL string
|
||||
AssetBucketRoot string
|
||||
OSSUtilPath string
|
||||
OSSUtilConfigFile string
|
||||
}
|
||||
|
||||
func LoadConfigFromEnv() (Config, error) {
|
||||
@@ -44,6 +48,10 @@ func LoadConfigFromEnv() (Config, error) {
|
||||
WechatMiniDevPrefix: getEnv("WECHAT_MINI_DEV_PREFIX", "dev-"),
|
||||
LocalEventDir: getEnv("LOCAL_EVENT_DIR", filepath.Clean("..\\event")),
|
||||
AssetBaseURL: getEnv("ASSET_BASE_URL", "https://oss-mbh5.colormaprun.com/gotomars"),
|
||||
AssetPublicBaseURL: getEnv("ASSET_PUBLIC_BASE_URL", "https://oss-mbh5.colormaprun.com"),
|
||||
AssetBucketRoot: getEnv("ASSET_BUCKET_ROOT", "oss://color-map-html"),
|
||||
OSSUtilPath: getEnv("OSSUTIL_PATH", filepath.Clean("..\\tools\\ossutil.exe")),
|
||||
OSSUtilConfigFile: getEnv("OSSUTIL_CONFIG_FILE", filepath.Join(mustUserHomeDir(), ".ossutilconfig")),
|
||||
}
|
||||
|
||||
if cfg.DatabaseURL == "" {
|
||||
@@ -71,3 +79,11 @@ func getDurationEnv(key string, fallback time.Duration) time.Duration {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func mustUserHomeDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
96
backend/internal/platform/assets/publisher.go
Normal file
96
backend/internal/platform/assets/publisher.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OSSUtilPublisher struct {
|
||||
ossutilPath string
|
||||
configFile string
|
||||
bucketRoot string
|
||||
publicBaseURL string
|
||||
}
|
||||
|
||||
func NewOSSUtilPublisher(ossutilPath, configFile, bucketRoot, publicBaseURL string) *OSSUtilPublisher {
|
||||
return &OSSUtilPublisher{
|
||||
ossutilPath: strings.TrimSpace(ossutilPath),
|
||||
configFile: strings.TrimSpace(configFile),
|
||||
bucketRoot: strings.TrimRight(strings.TrimSpace(bucketRoot), "/"),
|
||||
publicBaseURL: strings.TrimRight(strings.TrimSpace(publicBaseURL), "/"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OSSUtilPublisher) Enabled() bool {
|
||||
return p != nil &&
|
||||
p.ossutilPath != "" &&
|
||||
p.configFile != "" &&
|
||||
p.bucketRoot != "" &&
|
||||
p.publicBaseURL != ""
|
||||
}
|
||||
|
||||
func (p *OSSUtilPublisher) UploadJSON(ctx context.Context, publicURL string, payload []byte) error {
|
||||
if !p.Enabled() {
|
||||
return fmt.Errorf("asset publisher is not configured")
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return fmt.Errorf("payload is empty")
|
||||
}
|
||||
|
||||
objectKey, err := p.objectKeyFromPublicURL(publicURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(p.ossutilPath); err != nil {
|
||||
return fmt.Errorf("ossutil not found: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(p.configFile); err != nil {
|
||||
return fmt.Errorf("ossutil config not found: %w", err)
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "cmr-manifest-*.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
if _, err := tmpFile.Write(payload); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("write temp file: %w", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("close temp file: %w", err)
|
||||
}
|
||||
|
||||
target := p.bucketRoot + "/" + objectKey
|
||||
cmd := exec.CommandContext(ctx, p.ossutilPath, "cp", "-f", tmpPath, target, "--config-file", p.configFile)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("upload object %s failed: %w: %s", objectKey, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OSSUtilPublisher) objectKeyFromPublicURL(publicURL string) (string, error) {
|
||||
publicURL = strings.TrimSpace(publicURL)
|
||||
if publicURL == "" {
|
||||
return "", fmt.Errorf("public url is required")
|
||||
}
|
||||
if !strings.HasPrefix(publicURL, p.publicBaseURL+"/") {
|
||||
return "", fmt.Errorf("public url %s does not match public base %s", publicURL, p.publicBaseURL)
|
||||
}
|
||||
relative := strings.TrimPrefix(publicURL, p.publicBaseURL+"/")
|
||||
relative = strings.ReplaceAll(relative, "\\", "/")
|
||||
relative = strings.TrimLeft(relative, "/")
|
||||
if relative == "" {
|
||||
return "", fmt.Errorf("public url %s resolved to empty object key", publicURL)
|
||||
}
|
||||
return filepath.ToSlash(relative), nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
"cmr-backend/internal/platform/assets"
|
||||
"cmr-backend/internal/platform/security"
|
||||
"cmr-backend/internal/store/postgres"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ type ConfigService struct {
|
||||
store *postgres.Store
|
||||
localEventDir string
|
||||
assetBaseURL string
|
||||
publisher *assets.OSSUtilPublisher
|
||||
}
|
||||
|
||||
type ConfigPipelineSummary struct {
|
||||
@@ -76,11 +78,12 @@ type PublishBuildInput struct {
|
||||
BuildID string `json:"buildId"`
|
||||
}
|
||||
|
||||
func NewConfigService(store *postgres.Store, localEventDir, assetBaseURL string) *ConfigService {
|
||||
func NewConfigService(store *postgres.Store, localEventDir, assetBaseURL string, publisher *assets.OSSUtilPublisher) *ConfigService {
|
||||
return &ConfigService{
|
||||
store: store,
|
||||
localEventDir: localEventDir,
|
||||
assetBaseURL: strings.TrimRight(assetBaseURL, "/"),
|
||||
publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,9 +326,20 @@ func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInpu
|
||||
|
||||
configLabel := deriveConfigLabel(event, manifest, releaseNo)
|
||||
manifestURL := fmt.Sprintf("%s/event/releases/%s/%s/manifest.json", s.assetBaseURL, event.PublicID, releasePublicID)
|
||||
assetIndexURL := fmt.Sprintf("%s/event/releases/%s/%s/asset-index.json", s.assetBaseURL, event.PublicID, releasePublicID)
|
||||
checksum := security.HashText(buildRecord.ManifestJSON)
|
||||
routeCode := deriveRouteCode(manifest)
|
||||
|
||||
if s.publisher == nil || !s.publisher.Enabled() {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_unavailable", "asset publisher is not configured")
|
||||
}
|
||||
if err := s.publisher.UploadJSON(ctx, manifestURL, []byte(buildRecord.ManifestJSON)); err != nil {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_failed", "failed to upload manifest: "+err.Error())
|
||||
}
|
||||
if err := s.publisher.UploadJSON(ctx, assetIndexURL, []byte(buildRecord.AssetIndexJSON)); err != nil {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "asset_publish_failed", "failed to upload asset index: "+err.Error())
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -348,7 +362,7 @@ func (s *ConfigService) PublishBuild(ctx context.Context, input PublishBuildInpu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.store.ReplaceEventReleaseAssets(ctx, tx, releaseRecord.ID, s.mapBuildAssetsToReleaseAssets(releaseRecord.ID, manifestURL, &checksum, assetIndex)); err != nil {
|
||||
if err := s.store.ReplaceEventReleaseAssets(ctx, tx, releaseRecord.ID, s.mapBuildAssetsToReleaseAssets(releaseRecord.ID, manifestURL, assetIndexURL, &checksum, assetIndex)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -642,7 +656,7 @@ func deriveRouteCode(manifest map[string]any) *string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) mapBuildAssetsToReleaseAssets(eventReleaseID, manifestURL string, checksum *string, assetIndex []map[string]any) []postgres.UpsertEventReleaseAssetParams {
|
||||
func (s *ConfigService) mapBuildAssetsToReleaseAssets(eventReleaseID, manifestURL, assetIndexURL string, checksum *string, assetIndex []map[string]any) []postgres.UpsertEventReleaseAssetParams {
|
||||
assets := []postgres.UpsertEventReleaseAssetParams{
|
||||
{
|
||||
EventReleaseID: eventReleaseID,
|
||||
@@ -652,6 +666,13 @@ func (s *ConfigService) mapBuildAssetsToReleaseAssets(eventReleaseID, manifestUR
|
||||
Checksum: checksum,
|
||||
Meta: map[string]any{"source": "published-build"},
|
||||
},
|
||||
{
|
||||
EventReleaseID: eventReleaseID,
|
||||
AssetType: "other",
|
||||
AssetKey: "asset-index",
|
||||
AssetURL: assetIndexURL,
|
||||
Meta: map[string]any{"source": "published-build"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, asset := range assetIndex {
|
||||
|
||||
Reference in New Issue
Block a user