P0(9 条全修): - 多通道项目奖牌重复计数:domain 层按 (EventID,CategoryID,EntryID/Subject) 去重, 通道优先级 athlete→team→id - 「只排名不计分」项目(scored:false)不再进奖牌榜,REST/WS/导出口径统一 - aggregate 流水 entry_id 恒 NULL 导致重算后分数翻倍:作废范围改按 event+category 定位 - 已发布成绩仍可被「存草稿」覆盖并清空 rank:服务端 409 STATE_INVALID + 前端禁编辑 - voidResult / 规则导入重算后名次不回写:接住 Ranked 调 applyComputedRanks - 赛事包覆盖导入可能「旧项目已删、新项目没成」:先改名成功再清理,失败回滚 - 导入赛事包可覆盖正在运行的赛事目录:name==当前项目直接 400 - Console spawn 失败后守护器永久卡死:清 child/pid + 调度重启 + stop 超时兜底 - /ws 快照向非 admin 下发 admin-only 配置:按角色裁剪 settings P1(计分口径 / 契约 / 现场): - standings.tieBreak 三处接线;profiles[].recordBonusMode 生效;破纪录校验 RecordEligible 与 entry 归属;破纪录登记校验已发布成绩 - export.loadRanked 与主路径对齐;ListBackups reason 解析;CLI backup 前缀修正 (原来只增不减会撑爆磁盘);单位积分明细/导出统一权重折算;停用单位展示 - 规则导入原子化(重算并入同一事务)+ 返回 compatible/affectedEventCount/ recalculatedLedgerRows;登录审计 user_id 修正 - WS 策略性断开用 Close(code,reason);HelloMessage 补 page/instanceId; 实例注册表清理;settings/单位/项目写操作补 change_log 触发大屏刷新 - 大屏:全量快照不再被版本守卫丢弃;个人积分榜刷新;公告返回页/轮播;行数截断; REST 竞态;1008 停止重连;握手超时;时区补 Z;401 仅 AUTH_REQUIRED 才登出 - 管理端:published 行禁编辑;saveDraft 补 unit(1:06.33 显示);前端预览改用服务端 points;空成绩行不再恒「已修改」;修订详情按 id 拉取;切项目请求序号与 dirty 保护; 发布后刷新项目状态;NAV/路由按角色过滤;recorder 不再打 admin-only 接口 - Console:大屏窗口销毁后重建;大屏 URL 复用 host 归一化;backup.bat 改纯 ASCII (已在 Meowhome 实跑验证) 自检:服务端 gofmt/build/vet 通过、12 包测试全绿;前端 vue-tsc + build 通过、 node --test 24/24;console check-syntax 12 文件通过。
504 lines
16 KiB
Go
504 lines
16 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
|
|
"git.yeij.top/AskaEth/Scoreboard/server/internal/middleware"
|
|
)
|
|
|
|
// ---------- events ----------
|
|
|
|
const eventColumns = `id, sport_id, code, name, event_type, result_mode, result_type, compare,
|
|
record_eligible, scored, multiplier, status, scheduled_at, venue, sort_order, remark`
|
|
|
|
func scanEvent(row interface{ Scan(...any) error }) (eventDTO, error) {
|
|
var (
|
|
e eventDTO
|
|
scheduledAt, venue, note sql.NullString
|
|
)
|
|
err := row.Scan(&e.ID, &e.SportID, &e.Code, &e.Name, &e.EventType, &e.ResultMode, &e.ResultType,
|
|
&e.Compare, &e.RecordEligible, &e.Scored, &e.Multiplier, &e.Status,
|
|
&scheduledAt, &venue, &e.SortOrder, ¬e)
|
|
e.ScheduledAt = strPtr(scheduledAt)
|
|
e.Venue = strPtr(venue)
|
|
e.Remark = strPtr(note)
|
|
return e, err
|
|
}
|
|
|
|
func (s *server) listEvents(ctx context.Context, sportID int64) ([]eventDTO, error) {
|
|
query := `SELECT ` + eventColumns + ` FROM events`
|
|
args := []any{}
|
|
if sportID > 0 {
|
|
query += ` WHERE sport_id = ?`
|
|
args = append(args, sportID)
|
|
}
|
|
query += ` ORDER BY sort_order, code`
|
|
|
|
rows, err := s.ev.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := make([]eventDTO, 0)
|
|
for rows.Next() {
|
|
e, err := scanEvent(rows)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *server) getEvent(ctx context.Context, id int64) (eventDTO, error) {
|
|
e, err := eventByIDQ(ctx, s.ev, id)
|
|
if err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
cats, err := s.listCategories(ctx, id)
|
|
if err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
chs, err := s.listChannels(ctx, id)
|
|
if err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
e.Categories = cats
|
|
e.Channels = chs
|
|
return e, nil
|
|
}
|
|
|
|
// eventByIDQ 在给定连接/事务内读取小项(不含组别/通道),供写事务内构造 change_log 载荷复用。
|
|
func eventByIDQ(ctx context.Context, q queryer, id int64) (eventDTO, error) {
|
|
e, err := scanEvent(q.QueryRowContext(ctx, `SELECT `+eventColumns+` FROM events WHERE id = ?`, id))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return eventDTO{}, notFound("小项不存在")
|
|
}
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (s *server) createEvent(ctx context.Context, in eventDTO) (eventDTO, error) {
|
|
if in.SportID == 0 || strings.TrimSpace(in.Code) == "" || strings.TrimSpace(in.Name) == "" {
|
|
return eventDTO{}, badRequest("sportId、code、name 必填", nil)
|
|
}
|
|
tx, err := s.ev.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`INSERT INTO events (sport_id, code, name, event_type, result_mode, result_type, compare,
|
|
record_eligible, scored, multiplier, status, scheduled_at, venue, sort_order, remark)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
in.SportID, in.Code, in.Name,
|
|
orDefault(in.EventType, "individual"), orDefault(in.ResultMode, "auto"),
|
|
orDefault(in.ResultType, "none"), orDefault(in.Compare, "none"),
|
|
in.RecordEligible, in.Scored, multiplierOrOne(in.Multiplier), orDefault(in.Status, "pending"),
|
|
nullStr(in.ScheduledAt), nullStr(in.Venue), in.SortOrder, nullStr(in.Remark))
|
|
if isUniqueViolation(err) {
|
|
return eventDTO{}, conflict("小项编码已存在")
|
|
}
|
|
if isForeignKeyViolation(err) {
|
|
return eventDTO{}, badRequest("sportId 不存在", nil)
|
|
}
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
id, err := lastInsertID(res)
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
// 项目增删改要推给大屏,否则底栏/分项页拿不到新项目。
|
|
if err := s.bumpEvent(ctx, tx, id); err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
return s.getEvent(ctx, id)
|
|
}
|
|
|
|
func (s *server) updateEvent(ctx context.Context, id int64, in eventDTO) (eventDTO, error) {
|
|
tx, err := s.ev.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE events SET sport_id = ?, code = ?, name = ?, event_type = ?, result_mode = ?,
|
|
result_type = ?, compare = ?, record_eligible = ?, scored = ?, multiplier = ?,
|
|
status = ?, scheduled_at = ?, venue = ?, sort_order = ?, remark = ?
|
|
WHERE id = ?`,
|
|
in.SportID, in.Code, in.Name,
|
|
orDefault(in.EventType, "individual"), orDefault(in.ResultMode, "auto"),
|
|
orDefault(in.ResultType, "none"), orDefault(in.Compare, "none"),
|
|
in.RecordEligible, in.Scored, multiplierOrOne(in.Multiplier), orDefault(in.Status, "pending"),
|
|
nullStr(in.ScheduledAt), nullStr(in.Venue), in.SortOrder, nullStr(in.Remark), id)
|
|
if isUniqueViolation(err) {
|
|
return eventDTO{}, conflict("小项编码已存在")
|
|
}
|
|
if isForeignKeyViolation(err) {
|
|
return eventDTO{}, badRequest("sportId 不存在", nil)
|
|
}
|
|
if err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
if err := requireChanges(res, "小项不存在"); err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
if err := s.bumpEvent(ctx, tx, id); err != nil {
|
|
return eventDTO{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return eventDTO{}, internal(err)
|
|
}
|
|
return s.getEvent(ctx, id)
|
|
}
|
|
|
|
// bumpEvent 在写事务内推送小项变更;组别/通道的改动同样以本小项为载体。
|
|
func (s *server) bumpEvent(ctx context.Context, tx *sql.Tx, eventID int64) error {
|
|
ev, err := eventByIDQ(ctx, tx, eventID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bumpVersion(ctx, tx, "events", eventID, "upsert", ev, middleware.User(ctx).ID)
|
|
}
|
|
|
|
func (s *server) deleteEvent(ctx context.Context, id int64) error {
|
|
has, err := exists(ctx, s.ev, `SELECT 1 FROM entries WHERE event_id = ? LIMIT 1`, id)
|
|
if err != nil {
|
|
return internal(err)
|
|
}
|
|
if has {
|
|
return conflict("小项已有报名数据,不能删除")
|
|
}
|
|
tx, err := s.ev.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return internal(err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
for _, stmt := range []string{
|
|
`DELETE FROM event_score_channels WHERE event_id = ?`,
|
|
`DELETE FROM event_categories WHERE event_id = ?`,
|
|
`DELETE FROM events WHERE id = ?`,
|
|
} {
|
|
if _, err := tx.ExecContext(ctx, stmt, id); err != nil {
|
|
return internal(err)
|
|
}
|
|
}
|
|
if err := bumpVersion(ctx, tx, "events", id, "void", map[string]any{"id": id}, middleware.User(ctx).ID); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return internal(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func multiplierOrOne(v float64) float64 {
|
|
if v <= 0 {
|
|
return 1
|
|
}
|
|
return v
|
|
}
|
|
|
|
// ---------- categories ----------
|
|
|
|
func (s *server) listCategories(ctx context.Context, eventID int64) ([]categoryDTO, error) {
|
|
rows, err := s.ev.QueryContext(ctx,
|
|
`SELECT id, event_id, name, multiplier, gender, age_min, age_max, sort_order
|
|
FROM event_categories WHERE event_id = ? ORDER BY sort_order, id`, eventID)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := make([]categoryDTO, 0)
|
|
for rows.Next() {
|
|
var (
|
|
c categoryDTO
|
|
gender sql.NullString
|
|
ageMin, ageMax sql.NullInt64
|
|
)
|
|
if err := rows.Scan(&c.ID, &c.EventID, &c.Name, &c.Multiplier, &gender, &ageMin, &ageMax, &c.SortOrder); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
c.Gender = strPtr(gender)
|
|
c.AgeMin = intPtr(ageMin)
|
|
c.AgeMax = intPtr(ageMax)
|
|
out = append(out, c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// replaceCategories 整体替换某小项的组别(devdocs/05 约定)。已有报名数据时拒绝替换,
|
|
// 否则外键会把历史报名项一并带走。
|
|
func (s *server) replaceCategories(ctx context.Context, eventID int64, in []categoryDTO) ([]categoryDTO, error) {
|
|
has, err := exists(ctx, s.ev,
|
|
`SELECT 1 FROM entries e JOIN event_categories c ON c.id = e.category_id
|
|
WHERE c.event_id = ? LIMIT 1`, eventID)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
if has {
|
|
return nil, conflict("该项目已有报名数据,不能整体替换组别")
|
|
}
|
|
|
|
tx, err := s.ev.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM event_categories WHERE event_id = ?`, eventID); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
for i, c := range in {
|
|
if strings.TrimSpace(c.Name) == "" {
|
|
return nil, badRequest("组别 name 必填", i)
|
|
}
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO event_categories (event_id, name, multiplier, gender, age_min, age_max, sort_order)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
eventID, c.Name, multiplierOrOne(c.Multiplier), nullStr(c.Gender), nullInt(c.AgeMin), nullInt(c.AgeMax), c.SortOrder); err != nil {
|
|
if isUniqueViolation(err) {
|
|
return nil, conflict("组别名称重复:" + c.Name)
|
|
}
|
|
return nil, internal(err)
|
|
}
|
|
}
|
|
if err := s.bumpEvent(ctx, tx, eventID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
return s.listCategories(ctx, eventID)
|
|
}
|
|
|
|
// ---------- score channels ----------
|
|
|
|
func (s *server) listChannels(ctx context.Context, eventID int64) ([]channelDTO, error) {
|
|
rows, err := s.ev.QueryContext(ctx,
|
|
`SELECT id, event_id, scope, basis, profile_id, sort_order
|
|
FROM event_score_channels WHERE event_id = ? ORDER BY sort_order, id`, eventID)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := make([]channelDTO, 0)
|
|
for rows.Next() {
|
|
var c channelDTO
|
|
if err := rows.Scan(&c.ID, &c.EventID, &c.Scope, &c.Basis, &c.ProfileID, &c.SortOrder); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *server) replaceChannels(ctx context.Context, eventID int64, in []channelDTO) ([]channelDTO, error) {
|
|
tx, err := s.ev.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM event_score_channels WHERE event_id = ?`, eventID); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
for i, c := range in {
|
|
if c.Scope != "athlete" && c.Scope != "team" {
|
|
return nil, badRequest("通道 scope 只能是 athlete/team", i)
|
|
}
|
|
switch c.Basis {
|
|
case "result", "manual", "aggregate":
|
|
default:
|
|
return nil, badRequest("通道 basis 非法", i)
|
|
}
|
|
if c.ProfileID == 0 {
|
|
return nil, badRequest("通道 profileId 必填", i)
|
|
}
|
|
if _, err := tx.ExecContext(ctx,
|
|
`INSERT INTO event_score_channels (event_id, scope, basis, profile_id, sort_order)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
eventID, c.Scope, c.Basis, c.ProfileID, c.SortOrder); err != nil {
|
|
if isUniqueViolation(err) {
|
|
return nil, conflict("同一 scope+basis 只能配置一个通道")
|
|
}
|
|
if isForeignKeyViolation(err) {
|
|
return nil, badRequest("profileId 不存在", i)
|
|
}
|
|
return nil, internal(err)
|
|
}
|
|
}
|
|
if err := s.bumpEvent(ctx, tx, eventID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
return s.listChannels(ctx, eventID)
|
|
}
|
|
|
|
// ---------- entries ----------
|
|
|
|
const entryColumns = `e.id, e.event_id, e.category_id, e.team_id, e.athlete_id, e.member_names,
|
|
e.bib, e.seed, e.status, e.remark, a.name, t.code, t.name, t.short_name, c.name`
|
|
|
|
func scanEntry(row interface{ Scan(...any) error }) (entryDTO, error) {
|
|
var (
|
|
e entryDTO
|
|
athleteID sql.NullInt64
|
|
memberNames sql.NullString
|
|
bib, seed, rem sql.NullString
|
|
athleteName sql.NullString
|
|
teamCode sql.NullString
|
|
teamName sql.NullString
|
|
shortName sql.NullString
|
|
categoryName sql.NullString
|
|
)
|
|
if err := row.Scan(&e.ID, &e.EventID, &e.CategoryID, &e.TeamID, &athleteID, &memberNames,
|
|
&bib, &seed, &e.Status, &rem, &athleteName, &teamCode, &teamName, &shortName, &categoryName); err != nil {
|
|
return e, err
|
|
}
|
|
e.AthleteID = i64Ptr(athleteID)
|
|
e.MemberNames = parseNames(memberNames)
|
|
e.Bib = strPtr(bib)
|
|
e.Seed = strPtr(seed)
|
|
e.Remark = strPtr(rem)
|
|
e.AthleteName = strPtr(athleteName)
|
|
e.TeamCode = teamCode.String
|
|
e.TeamName = teamName.String
|
|
e.ShortName = strPtr(shortName)
|
|
e.CategoryName = categoryName.String
|
|
return e, nil
|
|
}
|
|
|
|
const entrySelect = `SELECT ` + entryColumns + `
|
|
FROM entries e
|
|
LEFT JOIN athletes a ON a.id = e.athlete_id
|
|
JOIN teams t ON t.id = e.team_id
|
|
JOIN event_categories c ON c.id = e.category_id`
|
|
|
|
func (s *server) listEntries(ctx context.Context, eventID int64) ([]entryDTO, error) {
|
|
rows, err := s.ev.QueryContext(ctx, entrySelect+` WHERE e.event_id = ? ORDER BY c.sort_order, e.id`, eventID)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
out := make([]entryDTO, 0)
|
|
for rows.Next() {
|
|
e, err := scanEntry(rows)
|
|
if err != nil {
|
|
return nil, internal(err)
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// listEntriesWithResults 返回报名项及其成绩(无成绩时 result=null)。
|
|
func (s *server) listEntriesWithResults(ctx context.Context, eventID int64) ([]entryWithResultDTO, error) {
|
|
entries, err := s.listEntries(ctx, eventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results, err := s.resultsByEvent(ctx, eventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
byEntry := make(map[int64]resultDTO, len(results))
|
|
for _, r := range results {
|
|
byEntry[r.EntryID] = r
|
|
}
|
|
|
|
out := make([]entryWithResultDTO, 0, len(entries))
|
|
for _, e := range entries {
|
|
item := entryWithResultDTO{entryDTO: e}
|
|
if r, ok := byEntry[e.ID]; ok {
|
|
cp := r
|
|
item.Result = &cp
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *server) getEntry(ctx context.Context, id int64) (entryDTO, error) {
|
|
e, err := scanEntry(s.ev.QueryRowContext(ctx, entrySelect+` WHERE e.id = ?`, id))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return entryDTO{}, notFound("报名项不存在")
|
|
}
|
|
if err != nil {
|
|
return entryDTO{}, internal(err)
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (s *server) createEntry(ctx context.Context, in entryDTO) (entryDTO, error) {
|
|
if in.EventID == 0 || in.CategoryID == 0 || in.TeamID == 0 {
|
|
return entryDTO{}, badRequest("eventId、categoryId、teamId 必填", nil)
|
|
}
|
|
res, err := s.ev.ExecContext(ctx,
|
|
`INSERT INTO entries (event_id, category_id, team_id, athlete_id, member_names, bib, seed, status, remark)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
in.EventID, in.CategoryID, in.TeamID, nullI64(in.AthleteID), marshalNames(in.MemberNames),
|
|
nullStr(in.Bib), nullStr(in.Seed), orDefault(in.Status, "registered"), nullStr(in.Remark))
|
|
if isUniqueViolation(err) {
|
|
return entryDTO{}, conflict("该报名项已存在")
|
|
}
|
|
if isForeignKeyViolation(err) {
|
|
return entryDTO{}, badRequest("eventId/categoryId/teamId/athleteId 不存在", nil)
|
|
}
|
|
if err != nil {
|
|
return entryDTO{}, internal(err)
|
|
}
|
|
id, err := lastInsertID(res)
|
|
if err != nil {
|
|
return entryDTO{}, internal(err)
|
|
}
|
|
return s.getEntry(ctx, id)
|
|
}
|
|
|
|
func (s *server) updateEntry(ctx context.Context, id int64, in entryDTO) (entryDTO, error) {
|
|
res, err := s.ev.ExecContext(ctx,
|
|
`UPDATE entries SET category_id = ?, team_id = ?, athlete_id = ?, member_names = ?,
|
|
bib = ?, seed = ?, status = ?, remark = ? WHERE id = ?`,
|
|
in.CategoryID, in.TeamID, nullI64(in.AthleteID), marshalNames(in.MemberNames),
|
|
nullStr(in.Bib), nullStr(in.Seed), orDefault(in.Status, "registered"), nullStr(in.Remark), id)
|
|
if isUniqueViolation(err) {
|
|
return entryDTO{}, conflict("该报名项已存在")
|
|
}
|
|
if isForeignKeyViolation(err) {
|
|
return entryDTO{}, badRequest("categoryId/teamId/athleteId 不存在", nil)
|
|
}
|
|
if err != nil {
|
|
return entryDTO{}, internal(err)
|
|
}
|
|
if err := requireChanges(res, "报名项不存在"); err != nil {
|
|
return entryDTO{}, err
|
|
}
|
|
return s.getEntry(ctx, id)
|
|
}
|
|
|
|
func (s *server) deleteEntry(ctx context.Context, id int64) error {
|
|
// 报名项被成绩引用时不允许删除;业务数据一律软删(status=scratched)。
|
|
res, err := s.ev.ExecContext(ctx, `UPDATE entries SET status = 'scratched' WHERE id = ?`, id)
|
|
if err != nil {
|
|
return internal(err)
|
|
}
|
|
return requireChanges(res, "报名项不存在")
|
|
}
|