fix(p2): 按审查清单修 103 条「建议修」+ 奖牌榜改名 + 两个守卫(dev_0.2.0)
## 奖牌榜改名(主人要求)
「团体奖牌榜」→「单位奖牌榜」,界面 / 大屏渲染页 / 导出件(工作表名与
下载文件名 `单位奖牌明细.xlsx`)/ 用户文档 全部统一;`DISPLAY_PAGES` 是
大屏控制页与大屏页的共同来源,改一处两边同时生效。旧名保留为别名,
老链接与老配置(`display.pages`)照常有效。真 Chromium 验证:14 个管理端
路由 + 7 个基础数据分区 0 报错,大屏页标题已是「单位奖牌榜」。
## P2 清单(123 条,103 条标「建议修」)
按 A/B/C/D 分组、以「文件所有权互斥」分 10 个代理并行推进,另有独立复核
代理逐条核对:61 条声称已修 / 55 条证实 / 4 条部分 / 0 条不成立 / 0 垃圾。
主要交付(按风险从高到低):
- 写入侧校验:update 路径补必填与 TrimSpace、模板 rules 重复 rank、
「小项不存在」的四种回答、`defaultProfileId` 外键、decodeJSON 的 8MB
上限与 413(A01–A06、D02、D03 批量上限 500)
- 危险操作:一键恢复加互斥(第二个请求当场 409)、恢复超时不再说
「未执行」、`ConfirmDialog` pending 时锁死 ESC/遮罩/关闭
- 认证:改密与撤会话进同一事务、滑动续期写库失败不再假装成功、
CSRF Cookie 丢失自动补发、密码长度按字符(原按字节)、
反代 403 只在可信代理透传原 Host 时采信 `X-Forwarded-Host`(全等比较)、
过期会话定期清理、登出补审计、登录限流可配
- 数据可见性:导出「已发布未结算」行改为 `名次 — / 积分 — / 状态未结算`、
单位积分明细成绩列与其它导出件逐字一致、空单位也打合计行、
基础数据包命中「已彻底删除的单位」整行跳过并报界面路径、
报名导入换单位给明确提示、勾选报名有冲突不再假报成功
- 导入/导出:赛事包上限 512MB(**它是唯一带字体/图片的包**,字体上限
20MB)而 multipart 内存档位独立钉在 16MB、工作簿表名尾随空格、
导出件路由与文件名契约补全
- 前端:一次写请求只生成一个 requestId(幂等缓存此前从未命中)、
基础数据包补 `X-Request-Id`、示例数据模式禁写扩到撤销发布/存草稿、
访客 401 不再是裸错误、模板 `{{else if}}` 不再静默吞内容
- Console:按设计退出(切赛事/恢复)不再判成崩溃、旧数据目录读不出来
会记日志、渲染层日志按上限截断、`service.js` 补 17 例测试
## 测试
服务端用例 610 → 674(13 包全绿,新增老库跨九条迁移升级、panic 形状、
429 落 interceptions、三态对照、avg_mode 分母、会话失效、请求体上限等)
前端 449 → 531(vue-tsc + build 通过);Console 28 → 54(19 个文件)。
另修复一条会让整包挂死 600s 的用例(测试主协程堵在 client.Do,defer
轮不到执行 → 改为可取消 + 探针超时,改坏也 7.5s 快速失败)。
## 两个新守卫(防止再出现「组件 setup 抛异常」这类盲区)
- `web/scripts/check-declaration-order.mjs` 已进测试命令:沿 watch/
watchEffect 的 const 依赖链查 TDZ(正是「运动员页整块空白」的根因)
- `web/scripts/smoke-admin.mjs`(`pnpm smoke`):真 Chromium 点遍 14 个
管理端路由 + 7 个基础数据分区 + 大屏页,任何 console 错误即失败
## 文档
- `docs/升级说明.md` 新增 dev_0.2.0 一节(三件升级必知:改名、审计前缀
与历史记录、密码口径;现场变化;赛事包上限;反代与 CSRF)
- 使用手册 / 常见问题 / 部署指南 / 上线检查清单按实现同步
This commit is contained in:
1 parent
db94d9c119
commit
6841cf49aa
147 files changed
+8141
-423
No files matched your search
@@ -53,8 +53,8 @@ type athleteImportMatch struct {
|
||||
Skip bool
|
||||
}
|
||||
|
||||
// sameNameNewPerson 是「身份字段不同 → 按同名的另一个人新建」的清单项:
|
||||
// 导入响应里单独列出来给现场看,避免新建了档案却毫无提示。
|
||||
// sameNameNewPerson 是「新建的档案与库中已有的同名人员疑似重复」的清单项:
|
||||
// 导入响应里单独列出来给现场核对,避免新建了档案却毫无提示。
|
||||
// 证件号与号码布都要带上:现场要核对的是「表里这两个号有没有写错」。
|
||||
type sameNameNewPerson struct {
|
||||
Name string `json:"name"`
|
||||
@@ -63,8 +63,22 @@ type sameNameNewPerson struct {
|
||||
ExistingIDCard string `json:"existingIdCard"`
|
||||
NewBib string `json:"newBib"`
|
||||
ExistingBib string `json:"existingBib"`
|
||||
// Reason 说明为什么判成「另一个人」:identity = 证件号 / 号码布两边都有值且对不上;
|
||||
// team = 这个单位下一个人都没有,但别的单位下有同名档案(多半是换了单位)。
|
||||
// 现场核对的重点完全不同,所以两者要分得开。
|
||||
Reason string `json:"reason"`
|
||||
// ExistingTeamCode 是库中那条同名档案的单位编码(换单位场景要核对的就是它)。
|
||||
ExistingTeamCode string `json:"existingTeamCode"`
|
||||
}
|
||||
|
||||
// sameNameReason* 是 sameNameNewPerson.Reason 的取值。
|
||||
const (
|
||||
// sameNameReasonIdentity:表里的证件号(或号码布)与库中那条同名档案对不上 —— 多半是号写错了。
|
||||
sameNameReasonIdentity = "identity"
|
||||
// sameNameReasonTeam:本次这个单位下查不到同名的,但别的单位下有 —— 换了单位(也可能真是两个人同名)。
|
||||
sameNameReasonTeam = "team"
|
||||
)
|
||||
|
||||
// existingAthlete 是库中已有人员的可比字段。
|
||||
type existingAthlete struct {
|
||||
ID int64
|
||||
@@ -110,7 +124,7 @@ func (s *server) matchImportedAthlete(ctx context.Context, in athleteImportRow,
|
||||
}
|
||||
|
||||
// matchByNameTeam 处理「证件号与号码布都没命中」的情况,按姓名 + 单位找候选:
|
||||
// 0 条 → 新建;1 条且身份字段有一边没填或都对得上 → 命中并回填;
|
||||
// 0 条 → 新建(并核对别的单位下有没有同名档案);1 条且身份字段有一边没填或都对得上 → 命中并回填;
|
||||
// 1 条但两边都有值且都对不上 → 同名的另一个人,新建并报告;多条 → 仍有歧义,进冲突面板等用户决策。
|
||||
func (s *server) matchByNameTeam(ctx context.Context, in athleteImportRow, decisions map[string]string) (athleteImportMatch, error) {
|
||||
candidates, err := s.athletesByNameTeam(ctx, in.Name, in.TeamID)
|
||||
@@ -119,7 +133,19 @@ func (s *server) matchByNameTeam(ctx context.Context, in athleteImportRow, decis
|
||||
}
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
return s.createImportedAthlete(ctx, in)
|
||||
created, err := s.createImportedAthlete(ctx, in)
|
||||
if err != nil {
|
||||
return athleteImportMatch{}, err
|
||||
}
|
||||
// 换了单位的人正是走到这一支:这个单位下查不到同名的,别的单位下却有一个。
|
||||
// 不能替用户认人(同名不同单位往往真是两个人),但更不能悄悄多建一份档案 ——
|
||||
// 两份档案各自带报名,成绩与积分从此分家(清单 A13)。
|
||||
reported, err := s.sameNameElsewhere(ctx, in)
|
||||
if err != nil {
|
||||
return athleteImportMatch{}, err
|
||||
}
|
||||
created.SameNameNewPerson = reported
|
||||
return created, nil
|
||||
case 1:
|
||||
ex := candidates[0]
|
||||
// 两边都给了证件号 / 号码布、且对不上时才算「同名的另一个人」:
|
||||
@@ -130,14 +156,11 @@ func (s *server) matchByNameTeam(ctx context.Context, in athleteImportRow, decis
|
||||
if err != nil {
|
||||
return athleteImportMatch{}, err
|
||||
}
|
||||
created.SameNameNewPerson = &sameNameNewPerson{
|
||||
Name: in.Name,
|
||||
TeamCode: in.TeamCode,
|
||||
NewIDCard: in.IDCard,
|
||||
ExistingIDCard: strValue(ex.IDCard),
|
||||
NewBib: in.Bib,
|
||||
ExistingBib: strValue(ex.Bib),
|
||||
reported, err := s.sameNameReport(ctx, in, ex, sameNameReasonIdentity)
|
||||
if err != nil {
|
||||
return athleteImportMatch{}, err
|
||||
}
|
||||
created.SameNameNewPerson = reported
|
||||
return created, nil
|
||||
}
|
||||
return s.matchExistingAthlete(ctx, in, ex, decisions)
|
||||
@@ -146,6 +169,41 @@ func (s *server) matchByNameTeam(ctx context.Context, in athleteImportRow, decis
|
||||
}
|
||||
}
|
||||
|
||||
// sameNameReport 组装一条「新建的档案与库中这条同名人员疑似重复」的报告。
|
||||
// existing 是库中那条同名档案(同单位的候选,或跨单位找到的那条),reason 说明为什么怀疑重复。
|
||||
func (s *server) sameNameReport(ctx context.Context, in athleteImportRow, existing existingAthlete, reason string) (*sameNameNewPerson, error) {
|
||||
code, err := s.teamCodeByID(ctx, existing.TeamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sameNameNewPerson{
|
||||
Name: in.Name,
|
||||
TeamCode: in.TeamCode,
|
||||
NewIDCard: in.IDCard,
|
||||
ExistingIDCard: strValue(existing.IDCard),
|
||||
NewBib: in.Bib,
|
||||
ExistingBib: strValue(existing.Bib),
|
||||
Reason: reason,
|
||||
ExistingTeamCode: code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sameNameElsewhere 找库中同名、但不在本次这个单位下的人员,用于报告「换了单位」造成的新建档案。
|
||||
// 只报第一条(与多人候选只以第一条为代表同一口径):现场要核对的是「这个名字是不是本来就该是
|
||||
// 同一个人」,列一串同名人反而看不出重点。
|
||||
func (s *server) sameNameElsewhere(ctx context.Context, in athleteImportRow) (*sameNameNewPerson, error) {
|
||||
ex, err := scanExistingAthlete(s.ev.QueryRowContext(ctx,
|
||||
`SELECT `+importAthleteColumns+` FROM athletes WHERE name = ? AND team_id <> ? ORDER BY id LIMIT 1`,
|
||||
in.Name, in.TeamID))
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
return nil, internal(err)
|
||||
}
|
||||
return s.sameNameReport(ctx, in, ex, sameNameReasonTeam)
|
||||
}
|
||||
|
||||
// matchAmbiguousCandidates 处理姓名 + 单位命中多条候选:不能替用户挑人,一律进冲突面板。
|
||||
// 决策键是整行身份(证件号优先、否则号码布),无法区分同一行的多个候选,
|
||||
// 所以面板只以第一条候选为代表;有决策时按决策作用于该候选。
|
||||
@@ -329,8 +387,12 @@ func changedAthleteFields(ex existingAthlete, in athleteImportRow) []string {
|
||||
if ex.Name != in.Name {
|
||||
changed = append(changed, "name")
|
||||
}
|
||||
// 性别只认 男/女:表里没填或填了其它值时不做比较,避免误报冲突。
|
||||
if in.Gender != "" && (ex.Gender == nil || *ex.Gender != in.Gender) {
|
||||
// 性别与证件号/号码布同一口径(清单 A09):表里没填(含填了 男/女 以外的值)= 不参与比较;
|
||||
// 库中为空 = 待回填,不算差异。之前是「库里为空就算差异」,一份没填性别列的旧名单会让
|
||||
// 所有库里性别为空的人全掉进冲突面板(这些人本来还在静默回填证件号/号码布/年龄),
|
||||
// 而性别又从不回填 —— 现场只能逐条点「使用新数据」,与 devdocs/05 §7 的匹配表相反。
|
||||
// 只有两边都有值且不同(男 ↔ 女)才是真冲突。
|
||||
if in.Gender != "" && ex.Gender != nil && *ex.Gender != "" && *ex.Gender != in.Gender {
|
||||
changed = append(changed, "gender")
|
||||
}
|
||||
// 年龄只在两边都有值且不同时算冲突:历史数据没有年龄,缺一边属于待补,不算冲突。
|
||||
@@ -354,7 +416,7 @@ func identityDiffers(dbVal *string, inVal string) bool {
|
||||
return inVal != "" && dbVal != nil && *dbVal != "" && *dbVal != inVal
|
||||
}
|
||||
|
||||
// backfillEmptyAthleteFields 只补库中为空的证件号/号码布/年龄,已有值不动(差异已由 changedAthleteFields 判过)。
|
||||
// backfillEmptyAthleteFields 只补库中为空的证件号/号码布/年龄/性别,已有值不动(差异已由 changedAthleteFields 判过)。
|
||||
func (s *server) backfillEmptyAthleteFields(ctx context.Context, ex existingAthlete, in athleteImportRow) error {
|
||||
if in.IDCard != "" && (ex.IDCard == nil || *ex.IDCard == "") {
|
||||
if err := s.idCardTakenError(ctx, in.IDCard, ex.ID); err != nil {
|
||||
@@ -379,6 +441,13 @@ func (s *server) backfillEmptyAthleteFields(ctx context.Context, ex existingAthl
|
||||
return internal(err)
|
||||
}
|
||||
}
|
||||
// 性别与证件号/号码布/年龄一并回填:历史名单里性别为空是常态,口径就是「库中为空 = 待回填」,
|
||||
// 不回填就等于把这批人永远留在空性别上,下一份表再导入还会被当成差异(清单 A09)。
|
||||
if in.Gender != "" && (ex.Gender == nil || *ex.Gender == "") {
|
||||
if _, err := s.ev.ExecContext(ctx, `UPDATE athletes SET gender = ? WHERE id = ?`, in.Gender, ex.ID); err != nil {
|
||||
return internal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -456,6 +525,18 @@ type entryImportOutcome struct {
|
||||
// StatusChanged 表示「可排名 / 不可排名」跨了界(registered ↔ scratched / dq / dns)。
|
||||
// 同一侧的状态改动(scratched → dq)不影响谁算数,不触发重算。
|
||||
StatusChanged bool
|
||||
// MovedEntries 是这次新建报名时顺带移出的「旧单位过期报名」(换单位场景)。
|
||||
// 它不参与重算判断(Created 已经覆盖),只作报告:数据口径是对的,但操作员看不见
|
||||
// 就等于没发生,调用方要把它转成 notices[] 说出来(清单 A10)。
|
||||
MovedEntries []movedSameScopeEntry
|
||||
}
|
||||
|
||||
// movedSameScopeEntry 报告一条「换单位后旧单位那条报名已被移出」。
|
||||
// 姓名与新/旧单位都要带上:现场要核对的正是「这个人是不是真的换了单位」。
|
||||
type movedSameScopeEntry struct {
|
||||
Name string
|
||||
OldTeamCode string
|
||||
NewTeamCode string
|
||||
}
|
||||
|
||||
// entryStatusRankable 与 domain.EntryStatus.rankable() 同一口径:
|
||||
@@ -465,10 +546,111 @@ func entryStatusRankable(status string) bool {
|
||||
return status == "" || status == string(domain.EntryStatusRegistered)
|
||||
}
|
||||
|
||||
// staleScopeEntry 是「同一个人 + 同一个小项 + 同一个组别」下、单位与本次导入不同的旧报名。
|
||||
type staleScopeEntry struct {
|
||||
ID int64
|
||||
Name string
|
||||
TeamCode string
|
||||
}
|
||||
|
||||
// releaseStaleSameScopeEntries 在给这个人**新建**报名之前,处置同一个小项同一个组别里
|
||||
// 单位与本次不同的旧报名。
|
||||
//
|
||||
// entries 的唯一键是 (category_id, team_id, athlete_id),换了单位就绕开了它:keep-new 把人员
|
||||
// 改到新单位后,旧单位那条报名会原样留着 —— 同一个人在同一个组别出现两条,成绩录入页看得见
|
||||
// 两条,现场各录一次成绩就是同一人同一组别两条成绩、重复计分(清单 A10);
|
||||
// docs/报名名单导入规范.md 也承诺同一个项目的报名「不会产生重复报名」。
|
||||
//
|
||||
// 处置只有两种,都不许凭空销毁数据:
|
||||
// - 旧报名没有任何成绩痕迹(results / score_ledger / record_breaks 全空)→ 按批量移出的口径
|
||||
// 物理移出。它只是一条过期的名单行,删掉不丢任何数据;新单位这条紧接着新建,
|
||||
// 调用方拿到 Created 就会重算这一组(与 recomputeImportedScopes 同一口径,
|
||||
// 团体平均口径的分母也跟着这一增一减重算)。
|
||||
// - 旧报名已经留下成绩痕迹 → 一行不动并拦下本行:删掉等于抹掉已经产生的名次与积分,
|
||||
// 那属于「成绩 / 榜单」的处置,必须由用户到成绩录入页用批量移出显式做
|
||||
// (removeEntriesTx 会连带清流水并重算),导入不许替他做这个决定。
|
||||
//
|
||||
// 返回被移出的旧报名(可能为空):调用方要拿它给操作员一条 notices 提示(清单 A10)。
|
||||
func (s *server) releaseStaleSameScopeEntries(ctx context.Context, in entryDTO) ([]staleScopeEntry, error) {
|
||||
// 团体行(athlete_id 为空)不受影响:同一个组别里两个单位各报一支队伍本来就是正常的。
|
||||
if in.AthleteID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.ev.QueryContext(ctx,
|
||||
`SELECT e.id, a.name, t.code FROM entries e
|
||||
JOIN athletes a ON a.id = e.athlete_id
|
||||
JOIN teams t ON t.id = e.team_id
|
||||
WHERE e.event_id = ? AND e.category_id = ? AND e.athlete_id = ? AND e.team_id <> ?
|
||||
ORDER BY e.id`, in.EventID, in.CategoryID, *in.AthleteID, in.TeamID)
|
||||
if err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
stale := make([]staleScopeEntry, 0, 1)
|
||||
for rows.Next() {
|
||||
var item staleScopeEntry
|
||||
if err := rows.Scan(&item.ID, &item.Name, &item.TeamCode); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
stale = append(stale, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
for _, item := range stale {
|
||||
scored, err := s.entryHasResultTraces(ctx, item.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scored {
|
||||
return nil, staleScoredEntryError(item)
|
||||
}
|
||||
}
|
||||
for _, item := range stale {
|
||||
// 上面已确认这条旧报名没有任何成绩痕迹,所以只删报名行本身:批量移出(removeEntriesTx)
|
||||
// 还要连带清流水 / 成绩 / 破纪录,是因为那边允许移出已有成绩的报名,这里没有可清的。
|
||||
if _, err := s.ev.ExecContext(ctx, `DELETE FROM entries WHERE id = ?`, item.ID); err != nil {
|
||||
// 并发下刚有人给这条旧报名录了成绩:results 对 entries 没有 ON DELETE,外键会挡住删除。
|
||||
// 那就按「已有成绩」处理并拦下本行,绝不强行删掉别人的成绩痕迹。
|
||||
if isForeignKeyViolation(err) {
|
||||
return nil, staleScoredEntryError(item)
|
||||
}
|
||||
return nil, internal(err)
|
||||
}
|
||||
}
|
||||
return stale, nil
|
||||
}
|
||||
|
||||
// entryHasResultTraces 判断一条报名是否已经留下成绩痕迹:录过成绩(草稿也算)、
|
||||
// 有过流水、或登记过破纪录,任意一样成立就不再是「可以随手移出的过期名单行」。
|
||||
func (s *server) entryHasResultTraces(ctx context.Context, entryID int64) (bool, error) {
|
||||
var traces int
|
||||
if err := s.ev.QueryRowContext(ctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM results WHERE entry_id = ?)
|
||||
OR EXISTS(SELECT 1 FROM score_ledger WHERE entry_id = ?)
|
||||
OR EXISTS(SELECT 1 FROM record_breaks WHERE entry_id = ?)`,
|
||||
entryID, entryID, entryID).Scan(&traces); err != nil {
|
||||
return false, internal(err)
|
||||
}
|
||||
return traces != 0, nil
|
||||
}
|
||||
|
||||
// staleScoredEntryError 是「旧报名已有成绩」时的行级提示:不能只说不能做,必须给出下一步。
|
||||
// 返回纯 error 而不是 apiError:调用方(handlers_config.go 的导入主循环)对 upsert 的失败
|
||||
// 用 err.Error() 原样回显,包成 apiError 会多出「CONFLICT:」前缀,现场看不懂。
|
||||
func staleScoredEntryError(item staleScopeEntry) error {
|
||||
return fmt.Errorf("%s 在本组别还有一条单位 %s 的报名(已录过成绩),换单位后同一个人在同一组别不能留两条;请先到成绩录入页把单位 %s 那条移出,再重新上传这一行",
|
||||
item.Name, item.TeamCode, item.TeamCode)
|
||||
}
|
||||
|
||||
// upsertImportedEntry 按「组别 + 单位 + 运动员」去重;返回落点信息,Created 表示新建。
|
||||
//
|
||||
// 旧 status 必须在 UPDATE 之前读出来:调用方要靠它判断「这次导入有没有把
|
||||
// 正常参赛改成退赛 / 失格」,进而决定要不要重算这一组(见 recomputeImportedScopes)。
|
||||
//
|
||||
// 唯一键是 (category_id, team_id, athlete_id),换了单位就绕开了它:新建之前必须先处置
|
||||
// 同一个小项同一个组别里另一单位的那条旧报名(见 releaseStaleSameScopeEntries)。
|
||||
func (s *server) upsertImportedEntry(ctx context.Context, in entryDTO) (entryImportOutcome, error) {
|
||||
var (
|
||||
existing int64
|
||||
@@ -488,6 +670,10 @@ func (s *server) upsertImportedEntry(ctx context.Context, in entryDTO) (entryImp
|
||||
status := orDefault(in.Status, "registered")
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
moved, err := s.releaseStaleSameScopeEntries(ctx, in)
|
||||
if err != nil {
|
||||
return entryImportOutcome{}, err
|
||||
}
|
||||
if _, err := s.ev.ExecContext(ctx,
|
||||
`INSERT INTO entries (event_id, category_id, team_id, athlete_id, member_names, bib, seed, heat, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
@@ -495,7 +681,21 @@ func (s *server) upsertImportedEntry(ctx context.Context, in entryDTO) (entryImp
|
||||
nullStr(in.Bib), nullStr(in.Seed), nullI64(in.Heat), status); err != nil {
|
||||
return entryImportOutcome{}, err
|
||||
}
|
||||
return entryImportOutcome{Created: true, EventID: in.EventID, CategoryID: in.CategoryID}, nil
|
||||
outcome := entryImportOutcome{Created: true, EventID: in.EventID, CategoryID: in.CategoryID}
|
||||
if len(moved) > 0 {
|
||||
// 只在真移出过旧报名时查一次新单位编码:这是给现场看的提示文案的一部分,
|
||||
// 平时的导入不为它付代价。
|
||||
newCode, err := s.teamCodeByID(ctx, in.TeamID)
|
||||
if err != nil {
|
||||
return entryImportOutcome{}, err
|
||||
}
|
||||
for _, item := range moved {
|
||||
outcome.MovedEntries = append(outcome.MovedEntries, movedSameScopeEntry{
|
||||
Name: item.Name, OldTeamCode: item.TeamCode, NewTeamCode: newCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
return outcome, nil
|
||||
case err != nil:
|
||||
return entryImportOutcome{}, err
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user