完善后端联调链路与模拟器多通道支持

This commit is contained in:
2026-04-01 18:48:59 +08:00
parent 94a1f0ba78
commit a70dc8d5d0
51 changed files with 4037 additions and 197 deletions

View File

@@ -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)

View File

@@ -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
}

View 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
}

View File

@@ -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 {