fix: 补 .gitattributes(CRLF for .bat) + README 端口 19540 + 提交 console 锁文件
This commit is contained in:
1 parent
c121111f51
commit
b80c64a115
39 files changed
+4217
-345
No files matched your search
@@ -0,0 +1,31 @@
|
||||
package dto
|
||||
|
||||
// EventResultGroup 是 snapshot.latestResults 的一组「小项 + 组别」成绩,
|
||||
// 与 web/src/shared/types.ts 的 EventResultGroup 对齐。
|
||||
type EventResultGroup struct {
|
||||
EventID int64 `json:"eventId"`
|
||||
EventCode string `json:"eventCode"`
|
||||
EventName string `json:"eventName"`
|
||||
CategoryID int64 `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName"`
|
||||
PublishedAt *string `json:"publishedAt"`
|
||||
Rows []EventResultRow `json:"rows"`
|
||||
}
|
||||
|
||||
// EventResultRow 是组内的一条成绩行;字段与 types.ts 的 EventResultRow 一一对应。
|
||||
type EventResultRow struct {
|
||||
EntryID int64 `json:"entryId"`
|
||||
Rank *int `json:"rank"`
|
||||
TeamID int64 `json:"teamId"`
|
||||
TeamCode string `json:"teamCode"`
|
||||
TeamName string `json:"teamName"`
|
||||
ShortName *string `json:"shortName"`
|
||||
AthleteID *int64 `json:"athleteId"`
|
||||
AthleteName *string `json:"athleteName"`
|
||||
MemberNames []string `json:"memberNames"`
|
||||
ResultText string `json:"resultText"`
|
||||
ResultValue *float64 `json:"resultValue"`
|
||||
Unit string `json:"unit"`
|
||||
IsTie bool `json:"isTie"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Package dto 存放实时层与 REST 层共用的线上数据结构与纯转换函数。
|
||||
//
|
||||
// 放在这里而不是 httpapi:realtime 与 httpapi 是同层出口,互相 import 会造成分层倒置;
|
||||
// 本包只依赖 domain 与标准库,不做任何 IO。
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EventSettings 是 GET/PUT /api/settings 与 snapshot.settings 的线上结构,
|
||||
// 与 web/src/shared/types.ts 的 EventSettings 一一对应。
|
||||
type EventSettings struct {
|
||||
Event EventSettingsEvent `json:"event"`
|
||||
Display EventSettingsDisplay `json:"display"`
|
||||
Standings EventSettingsStandings `json:"standings"`
|
||||
DefaultProfileID *int64 `json:"defaultProfileId"`
|
||||
RequireApproval bool `json:"requireApproval"`
|
||||
}
|
||||
|
||||
// EventSettingsEvent 是赛事基本信息。
|
||||
type EventSettingsEvent struct {
|
||||
Name string `json:"name"`
|
||||
Subtitle *string `json:"subtitle"`
|
||||
Host *string `json:"host"`
|
||||
LogoPath *string `json:"logoPath"`
|
||||
PrimaryColor *string `json:"primaryColor"`
|
||||
}
|
||||
|
||||
// EventSettingsDisplay 是大屏展示配置。
|
||||
type EventSettingsDisplay struct {
|
||||
Background *string `json:"background"`
|
||||
Logo *string `json:"logo"`
|
||||
Title string `json:"title"`
|
||||
Subtitle *string `json:"subtitle"`
|
||||
PrimaryColor *string `json:"primaryColor"`
|
||||
RotateSecond int `json:"rotateSeconds"`
|
||||
}
|
||||
|
||||
// EventSettingsStandings 是榜单口径配置。
|
||||
type EventSettingsStandings struct {
|
||||
IndividualToTeamWeight float64 `json:"individualToTeamWeight"`
|
||||
RankingMode string `json:"rankingMode"`
|
||||
ShowIndividualBoard bool `json:"showIndividualBoard"`
|
||||
}
|
||||
|
||||
// ParseEventSettings 把 settings 表的点号键聚合成嵌套对象。
|
||||
//
|
||||
// 默认值必须与 REST 的 getSettings 逐条一致:大屏首包走 snapshot、管理端走 /api/settings,
|
||||
// 两边口径不同会让同一块屏刷新前后显示不一样的标题。
|
||||
// defaultProfileId 需要查 scoring_profiles 反查 code,属于 IO,由调用方补填。
|
||||
func ParseEventSettings(values map[string]string) EventSettings {
|
||||
var out EventSettings
|
||||
|
||||
out.Event.Name = orDefault(values["event.name"], "未命名赛事")
|
||||
out.Event.Subtitle = nilIfEmpty(values["event.subtitle"])
|
||||
out.Event.Host = nilIfEmpty(values["event.organizer"])
|
||||
out.Event.LogoPath = nilIfEmpty(values["event.logo_path"])
|
||||
out.Event.PrimaryColor = nilIfEmpty(values["display.theme_color"])
|
||||
|
||||
out.Display.Background = nilIfEmpty(values["display.background"])
|
||||
out.Display.Logo = nilIfEmpty(values["display.logo"])
|
||||
if out.Display.Logo == nil {
|
||||
out.Display.Logo = out.Event.LogoPath
|
||||
}
|
||||
out.Display.Title = orDefault(values["display.title"], out.Event.Name)
|
||||
out.Display.Subtitle = nilIfEmpty(values["display.subtitle"])
|
||||
if out.Display.Subtitle == nil {
|
||||
out.Display.Subtitle = out.Event.Subtitle
|
||||
}
|
||||
out.Display.PrimaryColor = out.Event.PrimaryColor
|
||||
out.Display.RotateSecond = atoiDefault(values["display.rotate_seconds"], 15)
|
||||
|
||||
out.Standings.IndividualToTeamWeight = atofDefault(values["standings.individual_to_team_weight"], 1)
|
||||
out.Standings.RankingMode = orDefault(values["standings.mode"], "points")
|
||||
out.Standings.ShowIndividualBoard = boolDefault(
|
||||
firstNonEmpty(values["standings.show_individual_board"], values["standings.show_individual_medals"]), true)
|
||||
|
||||
out.RequireApproval = boolDefault(values["results.review_required"], false)
|
||||
return out
|
||||
}
|
||||
|
||||
func nilIfEmpty(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func orDefault(v, def string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func atoiDefault(v string, def int) int {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func atofDefault(v string, def float64) float64 {
|
||||
n, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func boolDefault(v string, def bool) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "true", "1", "yes":
|
||||
return true
|
||||
case "false", "0", "no":
|
||||
return false
|
||||
default:
|
||||
return def
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package dto
|
||||
|
||||
import "git.yeij.top/AskaEth/Scoreboard/server/internal/domain"
|
||||
|
||||
// StandingRow 是 GET /api/standings 与 snapshot.standings 的线上结构。
|
||||
//
|
||||
// 字段名与 web/src/shared/types.ts 的 StandingRow 对齐:大屏与管理端共用一份解析逻辑,
|
||||
// 不能再把 domain.StandingsRow 的 points/code/medals 直接下发。
|
||||
type StandingRow struct {
|
||||
TeamID int64 `json:"teamId"`
|
||||
TeamCode string `json:"teamCode"`
|
||||
TeamName string `json:"teamName"`
|
||||
ShortName *string `json:"shortName"`
|
||||
Color *string `json:"color"`
|
||||
LogoPath *string `json:"logoPath"`
|
||||
Rank int `json:"rank"`
|
||||
Gold int `json:"gold"`
|
||||
Silver int `json:"silver"`
|
||||
Bronze int `json:"bronze"`
|
||||
MedalTotal int `json:"medalTotal"`
|
||||
TotalPoints float64 `json:"totalPoints"`
|
||||
RankChange *int `json:"rankChange"`
|
||||
}
|
||||
|
||||
// StandingRows 把 domain 榜单转换成线上结构。
|
||||
// teams 用于补齐颜色/logo 等 domain 榜单没带的展示字段;rankChange 快照没有对比基准,与 REST 一致下发 null。
|
||||
func StandingRows(rows []domain.StandingsRow, teams []domain.Team) []StandingRow {
|
||||
byID := make(map[int64]domain.Team, len(teams))
|
||||
for _, t := range teams {
|
||||
byID[t.ID] = t
|
||||
}
|
||||
|
||||
out := make([]StandingRow, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
t := byID[r.TeamID]
|
||||
row := StandingRow{
|
||||
TeamID: r.TeamID,
|
||||
TeamCode: r.Code,
|
||||
TeamName: r.Name,
|
||||
ShortName: nilIfEmpty(r.ShortName),
|
||||
Rank: r.Rank,
|
||||
Gold: r.Gold,
|
||||
Silver: r.Silver,
|
||||
Bronze: r.Bronze,
|
||||
MedalTotal: r.Medals,
|
||||
TotalPoints: r.Points,
|
||||
RankChange: nil,
|
||||
}
|
||||
if t.ID != 0 {
|
||||
row.Color = nilIfEmpty(t.Color)
|
||||
row.LogoPath = nilIfEmpty(t.LogoPath)
|
||||
if row.TeamCode == "" {
|
||||
row.TeamCode = t.Code
|
||||
}
|
||||
if row.TeamName == "" {
|
||||
row.TeamName = t.Name
|
||||
}
|
||||
if row.ShortName == nil {
|
||||
row.ShortName = nilIfEmpty(t.ShortName)
|
||||
}
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user