完善多赛道联调与全局产品架构
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Backend
|
||||
> 文档版本:v1.0
|
||||
> 最后更新:2026-04-02 08:28:05
|
||||
> 文档版本:v1.1
|
||||
> 最后更新:2026-04-02 09:35:44
|
||||
|
||||
|
||||
这套后端现在已经能支撑一条完整主链:
|
||||
@@ -46,5 +46,8 @@ go run .\cmd\api
|
||||
- 局生命周期:`start / finish / detail`
|
||||
- 局后结果:`/sessions/{id}/result`、`/me/results`
|
||||
- 开发工作台:`/dev/workbench`
|
||||
- 用户主链调试
|
||||
- 资源对象与 Event 组装调试
|
||||
- Build / Publish / Rollback 调试
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Backend TodoList
|
||||
> 文档版本:v1.0
|
||||
> 最后更新:2026-04-02 08:28:05
|
||||
> 文档版本:v1.2
|
||||
> 最后更新:2026-04-02 11:03:02
|
||||
|
||||
|
||||
## 1. 目标
|
||||
@@ -37,6 +37,8 @@
|
||||
|
||||
- `evt_demo_001` 的 release manifest 现已可正常加载
|
||||
- 小程序已能进入地图
|
||||
- `launch` 关键字段在当前阶段不再单边漂移
|
||||
- `cancelled / failed / finished` 已从 ongoing 口径里收稳
|
||||
- 模拟定位 / 调试日志问题已回到小程序与模拟器侧,不再属于 backend 当前阻塞
|
||||
|
||||
前端当前需要配合的事项:
|
||||
@@ -160,27 +162,36 @@ backend 现在需要做的是:
|
||||
|
||||
## 4. P1 应尽快做
|
||||
|
||||
## 4.1 给首页 / play / result 的 ongoing 语义再做一次回归确认
|
||||
## 4.1 多赛道 Variant 第一阶段最小契约
|
||||
|
||||
当前前端已经开始走:
|
||||
当前前端已给出:
|
||||
|
||||
- 首页聚合
|
||||
- `event play`
|
||||
- `launch`
|
||||
- `session start / finish`
|
||||
- 本地故障恢复
|
||||
- [多赛道 Variant 五层设计草案](D:/dev/cmr-mini/doc/gameplay/多赛道Variant五层设计草案.md)
|
||||
- [多赛道 Variant 前后端最小契约](D:/dev/cmr-mini/doc/gameplay/多赛道Variant前后端最小契约.md)
|
||||
|
||||
backend 建议再回归确认这几个接口对“进行中 session”的口径一致:
|
||||
backend 当前建议第一阶段只做最小闭环:
|
||||
|
||||
- `/me/entry-home`
|
||||
- `/events/{eventPublicID}/play`
|
||||
- `/sessions/{sessionPublicID}/result`
|
||||
- `play.assignmentMode`
|
||||
- `play.courseVariants[]`
|
||||
- `launch.variant.*`
|
||||
- `session / result / ongoing / recent` 补 `variantId / variantName / routeCode`
|
||||
|
||||
重点确认:
|
||||
当前目标:
|
||||
|
||||
1. `cancelled` 后不再继续出现在 ongoing 入口
|
||||
2. `failed` 后不再继续出现在 ongoing 入口
|
||||
3. `finished` 后结果页与首页摘要字段一致
|
||||
1. 一个 session 最终只绑定一个 `variantId`
|
||||
2. `launch` 返回最终绑定结果
|
||||
3. 恢复链不重新分配 variant
|
||||
4. 结果页、ongoing、历史结果都能追溯 variant
|
||||
|
||||
备注:
|
||||
|
||||
- 当前只先定最小契约,不先做完整后台 variant 编排模型
|
||||
- 当前第一阶段最小后端链路已补入:
|
||||
- `play.assignmentMode`
|
||||
- `play.courseVariants[]`
|
||||
- `launch.variant.*`
|
||||
- `session / result / ongoing / recent` 的 `variantId / variantName / routeCode`
|
||||
- 下一步应由前端按该契约联调,不再继续扩后台 variant 模型
|
||||
|
||||
## 4.2 增加用户身体资料读取接口
|
||||
|
||||
@@ -318,18 +329,18 @@ backend 后面如果要接业务结果页,最好提前定:
|
||||
|
||||
## 7. 我建议的最近动作
|
||||
|
||||
backend 现在最值得先做的,不是扩接口,而是先确认下面 3 条:
|
||||
backend 现在最值得先做的,不是继续铺更多页面接口,而是先推进下面 3 条:
|
||||
|
||||
1. `finished / failed / cancelled` 三态语义
|
||||
2. 放弃恢复是否写 `cancelled`
|
||||
3. `start / finish` 是否按幂等处理
|
||||
1. 与前端确认多赛道第一阶段最小契约
|
||||
2. 已按最小契约扩完 `play -> launch -> session/result`
|
||||
3. 再补用户身体资料接口和 workbench 恢复场景按钮
|
||||
|
||||
这 3 条一旦确定,前后端联调会顺很多。
|
||||
这样不会打断当前主链,同时能把下一阶段多赛道联调接上。
|
||||
|
||||
## 8. 一句话结论
|
||||
|
||||
当前 backend 最重要的任务不是“再加更多接口”,而是:
|
||||
|
||||
> 先把 session 运行态语义、放弃恢复语义和 ongoing session 口径定稳,再继续扩后台配置系统。
|
||||
> 在不破坏当前稳定主链的前提下,先把多赛道 Variant 第一阶段最小契约定稳,再继续向配置与后台模型延伸。
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 开发说明
|
||||
> 文档版本:v1.0
|
||||
> 最后更新:2026-04-02 08:28:05
|
||||
> 文档版本:v1.1
|
||||
> 最后更新:2026-04-02 09:35:44
|
||||
|
||||
|
||||
## 1. 环境变量
|
||||
@@ -51,6 +51,11 @@ cd D:\dev\cmr-mini\backend
|
||||
|
||||
- [http://127.0.0.1:18090/dev/workbench](http://127.0.0.1:18090/dev/workbench)
|
||||
|
||||
当前 workbench 已覆盖两类调试链:
|
||||
|
||||
- 用户主链:`bootstrap -> auth -> entry/home -> event play/launch -> session -> result`
|
||||
- 后台运营链:`maps/playfields/resource-packs -> admin event source -> build -> publish -> rollback`
|
||||
|
||||
## 3. 当前开发约定
|
||||
|
||||
### 3.1 开发阶段先不用 Redis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# API 清单
|
||||
> 文档版本:v1.0
|
||||
> 最后更新:2026-04-02 09:01:17
|
||||
> 文档版本:v1.1
|
||||
> 最后更新:2026-04-02 11:05:32
|
||||
|
||||
|
||||
本文档只记录当前 backend 已实现接口,不写未来规划接口。
|
||||
@@ -121,6 +121,12 @@
|
||||
- `ongoingSession`
|
||||
- `recentSession`
|
||||
|
||||
`ongoingSession / recentSession` 当前会额外带:
|
||||
|
||||
- `variantId`
|
||||
- `variantName`
|
||||
- `routeCode`
|
||||
|
||||
## 4. Event
|
||||
|
||||
### `GET /events/{eventPublicID}`
|
||||
@@ -150,6 +156,8 @@
|
||||
- `event`
|
||||
- `release`
|
||||
- `resolvedRelease`
|
||||
- `play.assignmentMode`
|
||||
- `play.courseVariants`
|
||||
- `play.canLaunch`
|
||||
- `play.primaryAction`
|
||||
- `play.launchSource`
|
||||
@@ -169,13 +177,21 @@
|
||||
请求体重点:
|
||||
|
||||
- `releaseId`
|
||||
- `variantId`
|
||||
- `clientType`
|
||||
- `deviceKey`
|
||||
|
||||
补充说明:
|
||||
|
||||
- 如果当前 release 声明了 `play.courseVariants[]`
|
||||
- `launch` 会返回最终绑定的 `launch.variant`
|
||||
- 当前为兼容旧调用方,`assignmentMode=manual` 且未传 `variantId` 时,backend 会先回退到首个可选 variant
|
||||
|
||||
返回重点:
|
||||
|
||||
- `launch.source`
|
||||
- `launch.resolvedRelease`
|
||||
- `launch.variant`
|
||||
- `launch.config`
|
||||
- `launch.business.sessionId`
|
||||
- `launch.business.sessionToken`
|
||||
@@ -228,6 +244,13 @@
|
||||
- `event`
|
||||
- `resolvedRelease`
|
||||
|
||||
`session` 当前会额外带:
|
||||
|
||||
- `assignmentMode`
|
||||
- `variantId`
|
||||
- `variantName`
|
||||
- `routeCode`
|
||||
|
||||
### `POST /sessions/{sessionPublicID}/start`
|
||||
|
||||
鉴权:
|
||||
@@ -312,6 +335,9 @@
|
||||
|
||||
- `releaseId`
|
||||
- `configLabel`
|
||||
- `variantId`
|
||||
- `variantName`
|
||||
- `routeCode`
|
||||
|
||||
### `GET /me/results`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 核心流程
|
||||
> 文档版本:v1.0
|
||||
> 最后更新:2026-04-02 08:28:05
|
||||
> 文档版本:v1.1
|
||||
> 最后更新:2026-04-02 11:03:02
|
||||
|
||||
|
||||
## 1. 总流程
|
||||
@@ -100,6 +100,7 @@ APP 当前主链是手机号验证码:
|
||||
|
||||
- 当前是否可启动
|
||||
- 当前会落到哪份 `release`
|
||||
- 当前是否存在多赛道 `variant` 编排
|
||||
- 是否有 ongoing session
|
||||
- 当前推荐动作是什么
|
||||
|
||||
@@ -112,12 +113,27 @@ APP 当前主链是手机号验证码:
|
||||
- `event`
|
||||
- `release`
|
||||
- `resolvedRelease`
|
||||
- `play.assignmentMode`
|
||||
- `play.courseVariants[]`
|
||||
- `play.canLaunch`
|
||||
- `play.primaryAction`
|
||||
- `play.launchSource`
|
||||
- `play.ongoingSession`
|
||||
- `play.recentSession`
|
||||
|
||||
当前多赛道第一阶段约束:
|
||||
|
||||
- `play.assignmentMode` 只先支持最小口径:
|
||||
- `manual`
|
||||
- `random`
|
||||
- `server-assigned`
|
||||
- `play.courseVariants[]` 只先返回准备页必需字段:
|
||||
- `id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `routeCode`
|
||||
- `selectable`
|
||||
|
||||
## 6. Launch 流程
|
||||
|
||||
### 6.1 当前原则
|
||||
@@ -135,6 +151,7 @@ APP 当前主链是手机号验证码:
|
||||
当前请求体支持:
|
||||
|
||||
- `releaseId`
|
||||
- `variantId`
|
||||
- `clientType`
|
||||
- `deviceKey`
|
||||
|
||||
@@ -142,6 +159,7 @@ APP 当前主链是手机号验证码:
|
||||
|
||||
- `launch.source`
|
||||
- `launch.resolvedRelease`
|
||||
- `launch.variant`
|
||||
- `launch.config`
|
||||
- `launch.business.sessionId`
|
||||
- `launch.business.sessionToken`
|
||||
@@ -158,6 +176,14 @@ APP 当前主链是手机号验证码:
|
||||
- `launch.resolvedRelease.releaseId`
|
||||
- `launch.resolvedRelease.manifestUrl`
|
||||
- `launch.resolvedRelease.manifestChecksumSha256`
|
||||
- `launch.variant.id`
|
||||
- `launch.variant.assignmentMode`
|
||||
|
||||
补充说明:
|
||||
|
||||
- 如果活动声明了多赛道 variant,`launch` 会返回本局最终绑定的 `variant`
|
||||
- 前端可以发起选择,但最终绑定以后端 `launch` 返回为准
|
||||
- 故障恢复不重新分配 variant
|
||||
|
||||
而不是再拿 `event` 自己去猜。
|
||||
|
||||
@@ -195,6 +221,11 @@ APP 当前主链是手机号验证码:
|
||||
- `cancelled` 和 `failed` 都不再作为 ongoing session 返回
|
||||
- “放弃恢复”当前正式收口为 `finish(cancelled)`
|
||||
- 同一局旧 `sessionToken` 在 `finish(cancelled)` 场景允许继续使用
|
||||
- 第一阶段若活动声明了多赛道,session 会固化:
|
||||
- `assignmentMode`
|
||||
- `variantId`
|
||||
- `variantName`
|
||||
- `routeCode`
|
||||
|
||||
### 7.4 幂等要求
|
||||
|
||||
@@ -232,6 +263,7 @@ APP 当前主链是手机号验证码:
|
||||
|
||||
- 一个 event 未来可能发布新版本
|
||||
- 历史结果必须追溯到当时真实跑过的那份 release
|
||||
- 如果一场活动存在多个 variant,结果与历史摘要也必须能追溯本局 `variantId`
|
||||
|
||||
## 9. 当前最应该坚持的流程约束
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,8 @@ type EntrySessionSummary struct {
|
||||
EventName string `json:"eventName"`
|
||||
ReleaseID *string `json:"releaseId,omitempty"`
|
||||
ConfigLabel *string `json:"configLabel,omitempty"`
|
||||
VariantID *string `json:"variantId,omitempty"`
|
||||
VariantName *string `json:"variantName,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
LaunchedAt string `json:"launchedAt"`
|
||||
StartedAt *string `json:"startedAt,omitempty"`
|
||||
@@ -139,10 +141,12 @@ func (s *EntryHomeService) GetEntryHome(ctx context.Context, input EntryHomeInpu
|
||||
|
||||
func buildEntrySessionSummary(session *postgres.Session) EntrySessionSummary {
|
||||
summary := EntrySessionSummary{
|
||||
ID: session.SessionPublicID,
|
||||
Status: session.Status,
|
||||
RouteCode: session.RouteCode,
|
||||
LaunchedAt: session.LaunchedAt.Format(timeRFC3339),
|
||||
ID: session.SessionPublicID,
|
||||
Status: session.Status,
|
||||
VariantID: session.VariantID,
|
||||
VariantName: session.VariantName,
|
||||
RouteCode: session.RouteCode,
|
||||
LaunchedAt: session.LaunchedAt.Format(timeRFC3339),
|
||||
}
|
||||
if session.EventPublicID != nil {
|
||||
summary.EventID = *session.EventPublicID
|
||||
|
||||
@@ -35,6 +35,8 @@ type EventPlayResult struct {
|
||||
} `json:"release,omitempty"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Play struct {
|
||||
AssignmentMode *string `json:"assignmentMode,omitempty"`
|
||||
CourseVariants []CourseVariantView `json:"courseVariants,omitempty"`
|
||||
CanLaunch bool `json:"canLaunch"`
|
||||
PrimaryAction string `json:"primaryAction"`
|
||||
Reason string `json:"reason"`
|
||||
@@ -77,6 +79,11 @@ func (s *EventPlayService) GetEventPlay(ctx context.Context, input EventPlayInpu
|
||||
result.Event.DisplayName = event.DisplayName
|
||||
result.Event.Summary = event.Summary
|
||||
result.Event.Status = event.Status
|
||||
variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
|
||||
result.Play.AssignmentMode = variantPlan.AssignmentMode
|
||||
if len(variantPlan.CourseVariants) > 0 {
|
||||
result.Play.CourseVariants = variantPlan.CourseVariants
|
||||
}
|
||||
if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
|
||||
result.Release = &struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
@@ -37,6 +37,7 @@ type LaunchEventInput struct {
|
||||
EventPublicID string
|
||||
UserID string
|
||||
ReleaseID string `json:"releaseId,omitempty"`
|
||||
VariantID string `json:"variantId,omitempty"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
}
|
||||
@@ -49,6 +50,7 @@ type LaunchEventResult struct {
|
||||
Launch struct {
|
||||
Source string `json:"source"`
|
||||
ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
|
||||
Variant *VariantBindingView `json:"variant,omitempty"`
|
||||
Config struct {
|
||||
ConfigURL string `json:"configUrl"`
|
||||
ConfigLabel string `json:"configLabel"`
|
||||
@@ -115,6 +117,7 @@ func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string)
|
||||
func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) {
|
||||
input.EventPublicID = strings.TrimSpace(input.EventPublicID)
|
||||
input.ReleaseID = strings.TrimSpace(input.ReleaseID)
|
||||
input.VariantID = strings.TrimSpace(input.VariantID)
|
||||
input.DeviceKey = strings.TrimSpace(input.DeviceKey)
|
||||
if err := validateClientType(input.ClientType); err != nil {
|
||||
return nil, err
|
||||
@@ -139,6 +142,24 @@ func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput)
|
||||
if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
|
||||
return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
|
||||
}
|
||||
variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
|
||||
variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routeCode := event.RouteCode
|
||||
var assignmentMode *string
|
||||
var variantID *string
|
||||
var variantName *string
|
||||
if variant != nil {
|
||||
resultMode := variant.AssignmentMode
|
||||
assignmentMode = &resultMode
|
||||
variantID = &variant.ID
|
||||
variantName = &variant.Name
|
||||
if variant.RouteCode != nil {
|
||||
routeCode = variant.RouteCode
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.store.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -163,7 +184,10 @@ func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput)
|
||||
EventReleaseID: *event.CurrentReleaseID,
|
||||
DeviceKey: input.DeviceKey,
|
||||
ClientType: input.ClientType,
|
||||
RouteCode: event.RouteCode,
|
||||
AssignmentMode: assignmentMode,
|
||||
VariantID: variantID,
|
||||
VariantName: variantName,
|
||||
RouteCode: routeCode,
|
||||
SessionTokenHash: security.HashText(sessionToken),
|
||||
SessionTokenExpiresAt: sessionTokenExpiresAt,
|
||||
})
|
||||
@@ -180,16 +204,17 @@ func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput)
|
||||
result.Event.DisplayName = event.DisplayName
|
||||
result.Launch.Source = LaunchSourceEventCurrentRelease
|
||||
result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
|
||||
result.Launch.Variant = variant
|
||||
result.Launch.Config.ConfigURL = *event.ManifestURL
|
||||
result.Launch.Config.ConfigLabel = *event.ConfigLabel
|
||||
result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
|
||||
result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
|
||||
result.Launch.Config.RouteCode = event.RouteCode
|
||||
result.Launch.Config.RouteCode = routeCode
|
||||
result.Launch.Business.Source = "direct-event"
|
||||
result.Launch.Business.EventID = event.PublicID
|
||||
result.Launch.Business.SessionID = session.SessionPublicID
|
||||
result.Launch.Business.SessionToken = sessionToken
|
||||
result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
|
||||
result.Launch.Business.RouteCode = event.RouteCode
|
||||
result.Launch.Business.RouteCode = routeCode
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ type SessionResult struct {
|
||||
Status string `json:"status"`
|
||||
ClientType string `json:"clientType"`
|
||||
DeviceKey string `json:"deviceKey"`
|
||||
AssignmentMode *string `json:"assignmentMode,omitempty"`
|
||||
VariantID *string `json:"variantId,omitempty"`
|
||||
VariantName *string `json:"variantName,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"`
|
||||
LaunchedAt string `json:"launchedAt"`
|
||||
@@ -264,6 +267,9 @@ func buildSessionResult(session *postgres.Session) *SessionResult {
|
||||
result.Session.Status = session.Status
|
||||
result.Session.ClientType = session.ClientType
|
||||
result.Session.DeviceKey = session.DeviceKey
|
||||
result.Session.AssignmentMode = session.AssignmentMode
|
||||
result.Session.VariantID = session.VariantID
|
||||
result.Session.VariantName = session.VariantName
|
||||
result.Session.RouteCode = session.RouteCode
|
||||
result.Session.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
|
||||
result.Session.LaunchedAt = session.LaunchedAt.Format(time.RFC3339)
|
||||
|
||||
189
backend/internal/service/variant_contract.go
Normal file
189
backend/internal/service/variant_contract.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"cmr-backend/internal/apperr"
|
||||
)
|
||||
|
||||
const (
|
||||
AssignmentModeManual = "manual"
|
||||
AssignmentModeRandom = "random"
|
||||
AssignmentModeServerAssigned = "server-assigned"
|
||||
)
|
||||
|
||||
type CourseVariantView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
Selectable bool `json:"selectable"`
|
||||
}
|
||||
|
||||
type VariantBindingView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RouteCode *string `json:"routeCode,omitempty"`
|
||||
AssignmentMode string `json:"assignmentMode"`
|
||||
}
|
||||
|
||||
type VariantPlan struct {
|
||||
AssignmentMode *string
|
||||
CourseVariants []CourseVariantView
|
||||
}
|
||||
|
||||
func resolveVariantPlan(payloadJSON *string) VariantPlan {
|
||||
if payloadJSON == nil || strings.TrimSpace(*payloadJSON) == "" {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(*payloadJSON), &payload); err != nil {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
play, _ := payload["play"].(map[string]any)
|
||||
if len(play) == 0 {
|
||||
return VariantPlan{}
|
||||
}
|
||||
|
||||
result := VariantPlan{}
|
||||
if rawMode, ok := play["assignmentMode"].(string); ok {
|
||||
if normalized := normalizeAssignmentMode(rawMode); normalized != nil {
|
||||
result.AssignmentMode = normalized
|
||||
}
|
||||
}
|
||||
|
||||
rawVariants, _ := play["courseVariants"].([]any)
|
||||
if len(rawVariants) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, raw := range rawVariants {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := item["id"].(string)
|
||||
name, _ := item["name"].(string)
|
||||
id = strings.TrimSpace(id)
|
||||
name = strings.TrimSpace(name)
|
||||
if id == "" || name == "" {
|
||||
continue
|
||||
}
|
||||
var description *string
|
||||
if value, ok := item["description"].(string); ok && strings.TrimSpace(value) != "" {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
description = &trimmed
|
||||
}
|
||||
var routeCode *string
|
||||
if value, ok := item["routeCode"].(string); ok && strings.TrimSpace(value) != "" {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
routeCode = &trimmed
|
||||
}
|
||||
selectable := true
|
||||
if value, ok := item["selectable"].(bool); ok {
|
||||
selectable = value
|
||||
}
|
||||
result.CourseVariants = append(result.CourseVariants, CourseVariantView{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Description: description,
|
||||
RouteCode: routeCode,
|
||||
Selectable: selectable,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveLaunchVariant(plan VariantPlan, requestedVariantID string) (*VariantBindingView, error) {
|
||||
requestedVariantID = strings.TrimSpace(requestedVariantID)
|
||||
if len(plan.CourseVariants) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mode := AssignmentModeManual
|
||||
if plan.AssignmentMode != nil {
|
||||
mode = *plan.AssignmentMode
|
||||
}
|
||||
|
||||
if requestedVariantID != "" {
|
||||
for _, item := range plan.CourseVariants {
|
||||
if item.ID == requestedVariantID {
|
||||
if !item.Selectable && mode == AssignmentModeManual {
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_selectable", "requested variant is not selectable")
|
||||
}
|
||||
return &VariantBindingView{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
RouteCode: item.RouteCode,
|
||||
AssignmentMode: mode,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "requested variant does not exist")
|
||||
}
|
||||
|
||||
selected, err := selectDefaultVariant(plan.CourseVariants, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &VariantBindingView{
|
||||
ID: selected.ID,
|
||||
Name: selected.Name,
|
||||
RouteCode: selected.RouteCode,
|
||||
AssignmentMode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeAssignmentMode(value string) *string {
|
||||
switch strings.TrimSpace(value) {
|
||||
case AssignmentModeManual:
|
||||
mode := AssignmentModeManual
|
||||
return &mode
|
||||
case AssignmentModeRandom:
|
||||
mode := AssignmentModeRandom
|
||||
return &mode
|
||||
case AssignmentModeServerAssigned:
|
||||
mode := AssignmentModeServerAssigned
|
||||
return &mode
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func selectDefaultVariant(items []CourseVariantView, mode string) (*CourseVariantView, error) {
|
||||
candidates := make([]CourseVariantView, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Selectable {
|
||||
candidates = append(candidates, item)
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
candidates = append(candidates, items...)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "course variants are empty")
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case AssignmentModeRandom:
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates))))
|
||||
if err != nil {
|
||||
return nil, apperr.New(http.StatusInternalServerError, "variant_select_failed", fmt.Sprintf("failed to select random variant: %v", err))
|
||||
}
|
||||
selected := candidates[int(index.Int64())]
|
||||
return &selected, nil
|
||||
case AssignmentModeServerAssigned, AssignmentModeManual:
|
||||
fallthrough
|
||||
default:
|
||||
selected := candidates[0]
|
||||
return &selected, nil
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,16 @@ import (
|
||||
)
|
||||
|
||||
type DemoBootstrapSummary struct {
|
||||
TenantCode string `json:"tenantCode"`
|
||||
ChannelCode string `json:"channelCode"`
|
||||
EventID string `json:"eventId"`
|
||||
ReleaseID string `json:"releaseId"`
|
||||
SourceID string `json:"sourceId"`
|
||||
BuildID string `json:"buildId"`
|
||||
CardID string `json:"cardId"`
|
||||
TenantCode string `json:"tenantCode"`
|
||||
ChannelCode string `json:"channelCode"`
|
||||
EventID string `json:"eventId"`
|
||||
ReleaseID string `json:"releaseId"`
|
||||
SourceID string `json:"sourceId"`
|
||||
BuildID string `json:"buildId"`
|
||||
CardID string `json:"cardId"`
|
||||
VariantManualEventID string `json:"variantManualEventId"`
|
||||
VariantManualRelease string `json:"variantManualReleaseId"`
|
||||
VariantManualCardID string `json:"variantManualCardId"`
|
||||
}
|
||||
|
||||
func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
|
||||
@@ -88,7 +91,7 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
||||
$1,
|
||||
1,
|
||||
'Demo Config v1',
|
||||
'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json',
|
||||
'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
|
||||
'demo-checksum-001',
|
||||
'route-demo-001',
|
||||
'published'
|
||||
@@ -224,7 +227,7 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
||||
EventReleaseID: releaseRow.ID,
|
||||
AssetType: "manifest",
|
||||
AssetKey: "manifest",
|
||||
AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json",
|
||||
AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json",
|
||||
Checksum: &manifestChecksum,
|
||||
Meta: map[string]any{"source": "release-manifest"},
|
||||
},
|
||||
@@ -308,17 +311,149 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
||||
return nil, fmt.Errorf("ensure demo card: %w", err)
|
||||
}
|
||||
|
||||
var manualEventID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO events (
|
||||
tenant_id, event_public_id, slug, display_name, summary, status
|
||||
)
|
||||
VALUES ($1, 'evt_demo_variant_manual_001', 'demo-variant-manual-run', 'Demo Variant Manual Run', 'Manual 多赛道联调活动', 'active')
|
||||
ON CONFLICT (event_public_id) DO UPDATE SET
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
slug = EXCLUDED.slug,
|
||||
display_name = EXCLUDED.display_name,
|
||||
summary = EXCLUDED.summary,
|
||||
status = EXCLUDED.status
|
||||
RETURNING id
|
||||
`, tenantID).Scan(&manualEventID); err != nil {
|
||||
return nil, fmt.Errorf("ensure variant manual demo event: %w", err)
|
||||
}
|
||||
|
||||
var manualReleaseRow struct {
|
||||
ID string
|
||||
PublicID string
|
||||
}
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO event_releases (
|
||||
release_public_id,
|
||||
event_id,
|
||||
release_no,
|
||||
config_label,
|
||||
manifest_url,
|
||||
manifest_checksum_sha256,
|
||||
route_code,
|
||||
status,
|
||||
payload_jsonb
|
||||
)
|
||||
VALUES (
|
||||
'rel_demo_variant_manual_001',
|
||||
$1,
|
||||
1,
|
||||
'Demo Variant Manual Config v1',
|
||||
'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
|
||||
'demo-variant-checksum-001',
|
||||
'route-variant-a',
|
||||
'published',
|
||||
$2::jsonb
|
||||
)
|
||||
ON CONFLICT (release_public_id) DO UPDATE SET
|
||||
event_id = EXCLUDED.event_id,
|
||||
config_label = EXCLUDED.config_label,
|
||||
manifest_url = EXCLUDED.manifest_url,
|
||||
manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
|
||||
route_code = EXCLUDED.route_code,
|
||||
status = EXCLUDED.status,
|
||||
payload_jsonb = EXCLUDED.payload_jsonb
|
||||
RETURNING id, release_public_id
|
||||
`, manualEventID, `{
|
||||
"play": {
|
||||
"assignmentMode": "manual",
|
||||
"courseVariants": [
|
||||
{
|
||||
"id": "variant_a",
|
||||
"name": "A 线",
|
||||
"description": "短线体验版",
|
||||
"routeCode": "route-variant-a",
|
||||
"selectable": true
|
||||
},
|
||||
{
|
||||
"id": "variant_b",
|
||||
"name": "B 线",
|
||||
"description": "长线挑战版",
|
||||
"routeCode": "route-variant-b",
|
||||
"selectable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}`).Scan(&manualReleaseRow.ID, &manualReleaseRow.PublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure variant manual demo release: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE events
|
||||
SET current_release_id = $2
|
||||
WHERE id = $1
|
||||
`, manualEventID, manualReleaseRow.ID); err != nil {
|
||||
return nil, fmt.Errorf("attach variant manual demo release: %w", err)
|
||||
}
|
||||
|
||||
var manualCardPublicID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO cards (
|
||||
card_public_id,
|
||||
tenant_id,
|
||||
entry_channel_id,
|
||||
card_type,
|
||||
title,
|
||||
subtitle,
|
||||
cover_url,
|
||||
event_id,
|
||||
display_slot,
|
||||
display_priority,
|
||||
status
|
||||
)
|
||||
VALUES (
|
||||
'card_demo_variant_manual_001',
|
||||
$1,
|
||||
$2,
|
||||
'event',
|
||||
'Demo Variant Manual Run',
|
||||
'多赛道手动选择联调',
|
||||
'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
|
||||
$3,
|
||||
'home_primary',
|
||||
95,
|
||||
'active'
|
||||
)
|
||||
ON CONFLICT (card_public_id) DO UPDATE SET
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
entry_channel_id = EXCLUDED.entry_channel_id,
|
||||
card_type = EXCLUDED.card_type,
|
||||
title = EXCLUDED.title,
|
||||
subtitle = EXCLUDED.subtitle,
|
||||
cover_url = EXCLUDED.cover_url,
|
||||
event_id = EXCLUDED.event_id,
|
||||
display_slot = EXCLUDED.display_slot,
|
||||
display_priority = EXCLUDED.display_priority,
|
||||
status = EXCLUDED.status
|
||||
RETURNING card_public_id
|
||||
`, tenantID, channelID, manualEventID).Scan(&manualCardPublicID); err != nil {
|
||||
return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DemoBootstrapSummary{
|
||||
TenantCode: "tenant_demo",
|
||||
ChannelCode: "mini-demo",
|
||||
EventID: "evt_demo_001",
|
||||
ReleaseID: releaseRow.PublicID,
|
||||
SourceID: source.ID,
|
||||
BuildID: build.ID,
|
||||
CardID: cardPublicID,
|
||||
TenantCode: "tenant_demo",
|
||||
ChannelCode: "mini-demo",
|
||||
EventID: "evt_demo_001",
|
||||
ReleaseID: releaseRow.PublicID,
|
||||
SourceID: source.ID,
|
||||
BuildID: build.ID,
|
||||
CardID: cardPublicID,
|
||||
VariantManualEventID: "evt_demo_variant_manual_001",
|
||||
VariantManualRelease: manualReleaseRow.PublicID,
|
||||
VariantManualCardID: manualCardPublicID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type Event struct {
|
||||
ManifestURL *string
|
||||
ManifestChecksum *string
|
||||
RouteCode *string
|
||||
ReleasePayloadJSON *string
|
||||
}
|
||||
|
||||
type EventRelease struct {
|
||||
@@ -45,6 +46,9 @@ type CreateGameSessionParams struct {
|
||||
EventReleaseID string
|
||||
DeviceKey string
|
||||
ClientType string
|
||||
AssignmentMode *string
|
||||
VariantID *string
|
||||
VariantName *string
|
||||
RouteCode *string
|
||||
SessionTokenHash string
|
||||
SessionTokenExpiresAt time.Time
|
||||
@@ -58,6 +62,9 @@ type GameSession struct {
|
||||
EventReleaseID string
|
||||
DeviceKey string
|
||||
ClientType string
|
||||
AssignmentMode *string
|
||||
VariantID *string
|
||||
VariantName *string
|
||||
RouteCode *string
|
||||
Status string
|
||||
SessionTokenExpiresAt time.Time
|
||||
@@ -77,7 +84,8 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code
|
||||
er.route_code,
|
||||
er.payload_jsonb::text
|
||||
FROM events e
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
WHERE e.event_public_id = $1
|
||||
@@ -98,6 +106,7 @@ func (s *Store) GetEventByPublicID(ctx context.Context, eventPublicID string) (*
|
||||
&event.ManifestURL,
|
||||
&event.ManifestChecksum,
|
||||
&event.RouteCode,
|
||||
&event.ReleasePayloadJSON,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -122,7 +131,8 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
er.config_label,
|
||||
er.manifest_url,
|
||||
er.manifest_checksum_sha256,
|
||||
er.route_code
|
||||
er.route_code,
|
||||
er.payload_jsonb::text
|
||||
FROM events e
|
||||
LEFT JOIN event_releases er ON er.id = e.current_release_id
|
||||
WHERE e.id = $1
|
||||
@@ -143,6 +153,7 @@ func (s *Store) GetEventByID(ctx context.Context, eventID string) (*Event, error
|
||||
&event.ManifestURL,
|
||||
&event.ManifestChecksum,
|
||||
&event.RouteCode,
|
||||
&event.ReleasePayloadJSON,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -235,13 +246,16 @@ func (s *Store) CreateGameSession(ctx context.Context, tx Tx, params CreateGameS
|
||||
event_release_id,
|
||||
device_key,
|
||||
client_type,
|
||||
assignment_mode,
|
||||
variant_id,
|
||||
variant_name,
|
||||
route_code,
|
||||
session_token_hash,
|
||||
session_token_expires_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, session_public_id, user_id, event_id, event_release_id, device_key, client_type, route_code, status, session_token_expires_at
|
||||
`, params.SessionPublicID, params.UserID, params.EventID, params.EventReleaseID, params.DeviceKey, params.ClientType, params.RouteCode, params.SessionTokenHash, params.SessionTokenExpiresAt)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING id, session_public_id, user_id, event_id, event_release_id, device_key, client_type, assignment_mode, variant_id, variant_name, route_code, status, session_token_expires_at
|
||||
`, params.SessionPublicID, params.UserID, params.EventID, params.EventReleaseID, params.DeviceKey, params.ClientType, params.AssignmentMode, params.VariantID, params.VariantName, params.RouteCode, params.SessionTokenHash, params.SessionTokenExpiresAt)
|
||||
|
||||
var session GameSession
|
||||
err := row.Scan(
|
||||
@@ -252,6 +266,9 @@ func (s *Store) CreateGameSession(ctx context.Context, tx Tx, params CreateGameS
|
||||
&session.EventReleaseID,
|
||||
&session.DeviceKey,
|
||||
&session.ClientType,
|
||||
&session.AssignmentMode,
|
||||
&session.VariantID,
|
||||
&session.VariantName,
|
||||
&session.RouteCode,
|
||||
&session.Status,
|
||||
&session.SessionTokenExpiresAt,
|
||||
|
||||
@@ -101,6 +101,9 @@ func (s *Store) GetSessionResultByPublicID(ctx context.Context, sessionPublicID
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -149,6 +152,9 @@ func (s *Store) ListSessionResultsByUserID(ctx context.Context, userID string, l
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -244,6 +250,9 @@ func scanSessionResultRecord(row pgx.Row) (*SessionResultRecord, error) {
|
||||
&record.ManifestChecksum,
|
||||
&record.DeviceKey,
|
||||
&record.ClientType,
|
||||
&record.AssignmentMode,
|
||||
&record.VariantID,
|
||||
&record.VariantName,
|
||||
&record.RouteCode,
|
||||
&record.Status,
|
||||
&record.SessionTokenHash,
|
||||
@@ -317,6 +326,9 @@ func scanSessionResultRecordFromRows(rows pgx.Rows) (*SessionResultRecord, error
|
||||
&record.ManifestChecksum,
|
||||
&record.DeviceKey,
|
||||
&record.ClientType,
|
||||
&record.AssignmentMode,
|
||||
&record.VariantID,
|
||||
&record.VariantName,
|
||||
&record.RouteCode,
|
||||
&record.Status,
|
||||
&record.SessionTokenHash,
|
||||
|
||||
@@ -21,6 +21,9 @@ type Session struct {
|
||||
ManifestChecksum *string
|
||||
DeviceKey string
|
||||
ClientType string
|
||||
AssignmentMode *string
|
||||
VariantID *string
|
||||
VariantName *string
|
||||
RouteCode *string
|
||||
Status string
|
||||
SessionTokenHash string
|
||||
@@ -51,6 +54,9 @@ func (s *Store) GetSessionByPublicID(ctx context.Context, sessionPublicID string
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -83,6 +89,9 @@ func (s *Store) GetSessionByPublicIDForUpdate(ctx context.Context, tx Tx, sessio
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -119,6 +128,9 @@ func (s *Store) ListSessionsByUserID(ctx context.Context, userID string, limit i
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -172,6 +184,9 @@ func (s *Store) ListSessionsByUserAndEvent(ctx context.Context, userID, eventID
|
||||
er.manifest_checksum_sha256,
|
||||
gs.device_key,
|
||||
gs.client_type,
|
||||
gs.assignment_mode,
|
||||
gs.variant_id,
|
||||
gs.variant_name,
|
||||
gs.route_code,
|
||||
gs.status,
|
||||
gs.session_token_hash,
|
||||
@@ -249,6 +264,9 @@ func scanSession(row pgx.Row) (*Session, error) {
|
||||
&session.ManifestChecksum,
|
||||
&session.DeviceKey,
|
||||
&session.ClientType,
|
||||
&session.AssignmentMode,
|
||||
&session.VariantID,
|
||||
&session.VariantName,
|
||||
&session.RouteCode,
|
||||
&session.Status,
|
||||
&session.SessionTokenHash,
|
||||
@@ -282,6 +300,9 @@ func scanSessionFromRows(rows pgx.Rows) (*Session, error) {
|
||||
&session.ManifestChecksum,
|
||||
&session.DeviceKey,
|
||||
&session.ClientType,
|
||||
&session.AssignmentMode,
|
||||
&session.VariantID,
|
||||
&session.VariantName,
|
||||
&session.RouteCode,
|
||||
&session.Status,
|
||||
&session.SessionTokenHash,
|
||||
|
||||
11
backend/migrations/0007_variant_minimal.sql
Normal file
11
backend/migrations/0007_variant_minimal.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE game_sessions
|
||||
ADD COLUMN assignment_mode TEXT CHECK (assignment_mode IN ('manual', 'random', 'server-assigned')),
|
||||
ADD COLUMN variant_id TEXT,
|
||||
ADD COLUMN variant_name TEXT;
|
||||
|
||||
CREATE INDEX game_sessions_variant_id_idx ON game_sessions(variant_id);
|
||||
CREATE INDEX game_sessions_assignment_mode_idx ON game_sessions(assignment_mode);
|
||||
|
||||
COMMIT;
|
||||
@@ -46,4 +46,15 @@ if ($workbenchAddr.StartsWith(":")) {
|
||||
Write-Host ("http://" + $workbenchAddr + "/dev/workbench")
|
||||
Write-Host ""
|
||||
|
||||
go run .\cmd\api
|
||||
$exePath = Join-Path $backendDir "cmr-backend.exe"
|
||||
|
||||
Write-Host "Build:" -ForegroundColor Yellow
|
||||
Write-Host $exePath
|
||||
Write-Host ""
|
||||
|
||||
go build -o $exePath .\cmd\api
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "go build failed"
|
||||
}
|
||||
|
||||
& $exePath
|
||||
|
||||
@@ -10,4 +10,4 @@ if (-not (Test-Path $scriptPath)) {
|
||||
|
||||
Set-Location $backendDir
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File $scriptPath
|
||||
& $scriptPath
|
||||
|
||||
Reference in New Issue
Block a user