package httpapi // 本文件是 REST 的 JSON 契约(camelCase,与 web/src/shared/types.ts 一一对应)。 // 数据库列是 snake_case,转换集中放在 repo.go 的扫描函数里。 type teamDTO struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` ShortName *string `json:"shortName"` LogoPath *string `json:"logoPath"` Color *string `json:"color"` SortOrder int `json:"sortOrder"` Active bool `json:"active"` // DeletedAt 非空表示「彻底删除」(对界面不可见),行仍在库里;停用是另一回事(active = false)。 DeletedAt *string `json:"deletedAt"` } type athleteDTO struct { ID int64 `json:"id"` Name string `json:"name"` Gender *string `json:"gender"` TeamID int64 `json:"teamId"` EmployeeNo *string `json:"employeeNo"` IDCard *string `json:"idCard"` Bib *string `json:"bib"` Age *int `json:"age"` Active bool `json:"active"` } type sportDTO struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` ShortName *string `json:"shortName"` DefaultProfileID *int64 `json:"defaultProfileId"` SortOrder int `json:"sortOrder"` Active bool `json:"active"` } type categoryDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` Name string `json:"name"` Multiplier float64 `json:"multiplier"` Gender *string `json:"gender"` AgeMin *int `json:"ageMin"` AgeMax *int `json:"ageMax"` SortOrder int `json:"sortOrder"` } type channelDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` Scope string `json:"scope"` Basis string `json:"basis"` ProfileID int64 `json:"profileId"` SortOrder int `json:"sortOrder"` // AggregateMode 是 basis=aggregate 通道的汇总口径:avg 按成员平均 / sum 按成员总和。 // 请求体里缺省(空串)按 avg 处理,不带该字段的旧客户端保存通道时口径不变。 // 响应里只下发非缺省值(sum);缺省(avg)不下发,老数据的响应体因此一个字节都不变。 AggregateMode string `json:"aggregateMode,omitempty"` // CountToTeam 决定该通道产出的流水是否计入单位总分;请求体里缺省(null)按 true 处理, // 「个人+团体」项目的个人通道传 false。响应里始终是具体值。 CountToTeam *bool `json:"countToTeam"` // CountToMedals 决定该通道产出的名次是否发奖牌;缺省(null)按 true 处理(与库中默认值一致)。 CountToMedals *bool `json:"countToMedals"` // CountToTeamMedals 决定该通道的个人奖牌是否并入单位奖牌榜;缺省按 true 处理。 // 只对 scope=athlete 的通道有意义:scope=team 的名次本来就记在单位头上。 CountToTeamMedals *bool `json:"countToTeamMedals"` } type eventDTO struct { ID int64 `json:"id"` SportID int64 `json:"sportId"` Code string `json:"code"` Name string `json:"name"` EventType string `json:"eventType"` ResultMode string `json:"resultMode"` ResultType string `json:"resultType"` Compare string `json:"compare"` RecordEligible bool `json:"recordEligible"` Scored bool `json:"scored"` Multiplier float64 `json:"multiplier"` Status string `json:"status"` ScheduledAt *string `json:"scheduledAt"` Venue *string `json:"venue"` SortOrder int `json:"sortOrder"` Remark *string `json:"remark"` AvgMode string `json:"avgMode"` // AwardMedals 决定该小项是否结算奖牌;请求体缺省按 true 处理(与库中默认值一致)。 // 关闭后小项的名次不产生任何奖牌,积分不受影响。 AwardMedals bool `json:"awardMedals"` Categories []categoryDTO `json:"categories,omitempty"` Channels []channelDTO `json:"channels,omitempty"` } type profileRuleDTO struct { Rank int `json:"rank"` Points float64 `json:"points"` } type profileDTO struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` TieMode string `json:"tieMode"` RecordBonusMode string `json:"recordBonusMode"` AwardRanks int `json:"awardRanks"` LastPlaceZero bool `json:"lastPlaceZero"` Rules []profileRuleDTO `json:"rules"` } type entryDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` CategoryID int64 `json:"categoryId"` TeamID int64 `json:"teamId"` // Squad 是「这个单位在本组别的第几支队」(自由文本:一队 / 二队 / 青年队)。 // 空串 = 不区分队伍:单队单位与升级前的老数据都是它,行为与升级前完全一致(见迁移 0025)。 // 只对纯团体报名(athleteId 为空)有意义:带运动员的报名按 (组别, 单位, 运动员) 唯一, // 同一个人在同一组别不会因为 squad 而多报一次。 Squad string `json:"squad"` AthleteID *int64 `json:"athleteId"` MemberNames []string `json:"memberNames"` // Athletes 是这条报名的**队员**(团队 / 对抗项目的成员运动员),读侧始终下发数组 // (空数组 = 只报单位,与升级前一致;不是 null,前端不必写两套渲染)。 // 它是人员关联(athleteId),memberNames 只是显示用的文字名单——两者可以同时存在: // 老数据、以及「人没录档案只登记姓名」的报名就只有 memberNames。 Athletes []entryAthleteDTO `json:"athletes"` Bib *string `json:"bib"` Seed *string `json:"seed"` // Heat 是录入 / 展示用的批次号,null 表示「未分批」;批次不参与算分。 Heat *int64 `json:"heat"` Status string `json:"status"` Remark *string `json:"remark"` AthleteName *string `json:"athleteName,omitempty"` TeamCode string `json:"teamCode,omitempty"` TeamName string `json:"teamName,omitempty"` ShortName *string `json:"shortName,omitempty"` CategoryName string `json:"categoryName,omitempty"` // TeamShares 是这条报名的归属份额(联队):读侧至少一条,第一条是主单位(= teamId)。 // 普通报名也下发一条独占份额,前端不必为「有没有联队」写两套渲染。 TeamShares []entryTeamShareDTO `json:"teamShares"` } // entryAthleteDTO 是报名上的一名**队员**(devdocs/20):团队 / 对抗项目的成员运动员。 // // - athleteId:人员档案的 id,**唯一身份**(认人时以号码布为准,姓名会重名); // - name / bib:展示字段(前端显示「姓名(号码布)」),写侧不需要带。 // // 写侧(PUT /api/entries/{id}/athletes)只认 athleteId:把姓名 / 号码布认成人是导入与界面 // 的事(导入的「队员名单」列可以写号码布),服务端不在这里二次猜测。 type entryAthleteDTO struct { AthleteID int64 `json:"athleteId"` Name string `json:"name,omitempty"` Bib *string `json:"bib,omitempty"` } // entryTeamShareDTO 是报名份额的一条(联队的一个成员单位)。 // // - weight:积分份额,**相对权重**(缺省 1)。引擎按权重之和归一,所以 1+1 与 0.5+0.5 等价; // 入库也就存原值,不做归一,避免 2:1 被写成 0.666…/0.333… 这种除不尽的数。 // - medal :这枚奖牌是否**整枚**记给该单位(缺省 true)。联队的金牌两个单位各记 1 枚, // 与 weight 无关——奖牌榜数整数枚,不按积分权重拆。 // - teamCode / teamName 是读侧补的展示字段(前端拼「A+B联队」用),写侧不需要带。 type entryTeamShareDTO struct { TeamID int64 `json:"teamId"` Weight float64 `json:"weight"` Medal *bool `json:"medal,omitempty"` TeamCode string `json:"teamCode,omitempty"` TeamName string `json:"teamName,omitempty"` } type entryWithResultDTO struct { entryDTO Result *resultDTO `json:"result"` } // entryHeatResultDTO 是批量设置批次的结果:本批条目当前统一的批次号(null = 未分批)。 type entryHeatResultDTO struct { Updated int `json:"updated"` Heat *int64 `json:"heat"` EntryIDs []int64 `json:"entryIds"` } type resultDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` EntryID int64 `json:"entryId"` Rank *int `json:"rank"` ResultText string `json:"resultText"` ResultValue *float64 `json:"resultValue"` Unit string `json:"unit"` IsTie bool `json:"isTie"` PointsOverride *float64 `json:"pointsOverride"` Status string `json:"status"` State string `json:"state"` RecordedBy *int64 `json:"recordedBy"` RecordedAt *string `json:"recordedAt"` VerifiedBy *int64 `json:"verifiedBy"` VerifiedAt *string `json:"verifiedAt"` Note *string `json:"note"` Points *float64 `json:"points,omitempty"` } type athleteStandingDTO struct { Rank int `json:"rank"` AthleteID int64 `json:"athleteId"` AthleteName string `json:"athleteName"` Gender *string `json:"gender"` TeamID int64 `json:"teamId"` TeamCode string `json:"teamCode"` TeamName string `json:"teamName"` ShortName *string `json:"shortName"` LogoPath *string `json:"logoPath"` TotalPoints float64 `json:"totalPoints"` Gold int `json:"gold"` Silver int `json:"silver"` Bronze int `json:"bronze"` } type breakdownItemDTO struct { LedgerID int64 `json:"ledgerId"` SourceType string `json:"sourceType"` EventID *int64 `json:"eventId"` EventCode *string `json:"eventCode"` EventName *string `json:"eventName"` CategoryName *string `json:"categoryName"` AthleteID *int64 `json:"athleteId"` AthleteName *string `json:"athleteName"` Rank *int `json:"rank"` ResultText *string `json:"resultText"` Points float64 `json:"points"` CreatedAt string `json:"createdAt"` // 修订痕迹(口径见 internal/revisions):这条成绩发布之后被改过几次、最后一次因为什么改、什么时候。 // // 三个字段一起看:RevisionCount 为 nil 表示**追不出来**(没有版本基线),为 0 表示追出来了、 // 确实没改过;前端只在 > 0 时显示「已修订 ×N」角标。宁可为空也不误报。 // LastRevisionReason / LastRevisedBy 都可能为 nil:现场没写原因、操作人已删号时就是空的。 RevisionCount *int `json:"revisionCount"` LastRevisionAt *string `json:"lastRevisionAt"` LastRevisionReason *string `json:"lastRevisionReason"` LastRevisedBy *string `json:"lastRevisedBy"` } type teamBreakdownDTO struct { TeamID int64 `json:"teamId"` TeamCode string `json:"teamCode"` TeamName string `json:"teamName"` TotalPoints float64 `json:"totalPoints"` Items []breakdownItemDTO `json:"items"` } type adjustmentDTO struct { ID int64 `json:"id"` TeamID int64 `json:"teamId"` Delta float64 `json:"delta"` Category string `json:"category"` Reason string `json:"reason"` EventID *int64 `json:"eventId"` Voided bool `json:"voided"` CreatedBy *int64 `json:"createdBy"` CreatedAt string `json:"createdAt"` } type recordEntryDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` Level string `json:"level"` ResultValue float64 `json:"resultValue"` ResultText string `json:"resultText"` HolderName *string `json:"holderName"` HolderTeamID *int64 `json:"holderTeamId"` AchievedAt *string `json:"achievedAt"` IsCurrent bool `json:"isCurrent"` Source string `json:"source"` } type recordBreakDTO struct { ID int64 `json:"id"` EventID int64 `json:"eventId"` EntryID int64 `json:"entryId"` Level string `json:"level"` ResultValue float64 `json:"resultValue"` ResultText string `json:"resultText"` BonusPoints float64 `json:"bonusPoints"` State string `json:"state"` ApprovedBy *int64 `json:"approvedBy"` ApprovedAt *string `json:"approvedAt"` // Note 是驳回 / 作废这条登记时写下的理由(0021 新增的列);从未被驳回的登记(含 // 老库里的历史行)为 null——空理由与「没有理由」是两回事,所以这里用指针。 Note *string `json:"note"` } type auditLogDTO struct { ID int64 `json:"id"` UserID *int64 `json:"userId"` Action string `json:"action"` EntityType string `json:"entityType"` EntityID *int64 `json:"entityId"` BeforeJSON *string `json:"beforeJson"` AfterJSON *string `json:"afterJson"` IP *string `json:"ip"` RequestID *string `json:"requestId"` CreatedAt string `json:"createdAt"` } type currentUserDTO struct { ID int64 `json:"id"` Username string `json:"username"` DisplayName string `json:"displayName"` Role string `json:"role"` LastLoginAt *string `json:"lastLoginAt"` MustChangePassword bool `json:"mustChangePassword"` // SessionExpiresAt 是当前会话的到期时间(RFC3339 UTC),供界面显示 // 「当前会话将于 … 过期」;未登录时为 null。 SessionExpiresAt *string `json:"sessionExpiresAt"` } type backupResultDTO struct { Path string `json:"path"` CreatedAt string `json:"createdAt"` } // GET/PUT /api/settings 的线上结构见 internal/dto.EventSettings(与 snapshot.settings 共用)。 // ---------- 用户管理 ---------- type userListItemDTO struct { ID int64 `json:"id"` Username string `json:"username"` DisplayName string `json:"displayName"` Role string `json:"role"` Active bool `json:"active"` MustChangePassword bool `json:"mustChangePassword"` LastLoginAt *string `json:"lastLoginAt"` } // resetPasswordResultDTO 里的 password 是唯一一次明文,落库只存哈希。 type resetPasswordResultDTO struct { ID int64 `json:"id"` Username string `json:"username"` Password string `json:"password"` Generated bool `json:"generated"` MustChangePassword bool `json:"mustChangePassword"` } // ---------- 变更历史 ---------- type revisionPageDTO struct { Items []revisionListItemDTO `json:"items"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } type revisionListItemDTO struct { ID int64 `json:"id"` ParentID *int64 `json:"parentId"` EventID int64 `json:"eventId"` EventName string `json:"eventName"` Action string `json:"action"` ActorID *int64 `json:"actorId"` ActorName string `json:"actorName"` Message *string `json:"message"` CreatedAt string `json:"createdAt"` ChangedCount int `json:"changedCount"` } type revisionDetailDTO struct { revisionListItemDTO Snapshot []resultDTO `json:"snapshot"` Diff revisionDiffDTO `json:"diff"` } type revisionDiffDTO struct { Results []revisionResultChangeDTO `json:"results"` Teams []revisionTeamDeltaDTO `json:"teams"` } type revisionResultChangeDTO struct { ResultID int64 `json:"resultId"` EntryID int64 `json:"entryId"` TeamID *int64 `json:"teamId"` Change string `json:"change"` Fields []string `json:"fields,omitempty"` Before *resultDTO `json:"before"` After *resultDTO `json:"after"` } type revisionTeamDeltaDTO struct { TeamID int64 `json:"teamId"` TeamCode string `json:"teamCode"` TeamName string `json:"teamName"` PointsBefore float64 `json:"pointsBefore"` PointsAfter float64 `json:"pointsAfter"` PointsDelta float64 `json:"pointsDelta"` ChangedResults int `json:"changedResults"` } // ---------- domain → DTO ---------- func nilIfEmpty(s string) *string { if s == "" { return nil } return &s }