package httpapi import ( "context" "database/sql" "errors" ) func (s *server) lookupID(ctx context.Context, query string, args ...any) (int64, error) { var id int64 if err := s.ev.QueryRowContext(ctx, query, args...).Scan(&id); err != nil { return 0, err } return id, nil } // athleteImportRow 是报名表里一行的人员信息(Gender 已归一为 男/女 或空)。 type athleteImportRow struct { Line int Name string Gender string TeamID int64 TeamCode string IDCard string } // athleteImportMatch 是人员匹配结果;Conflict 非空表示本轮不落库,等用户决策。 type athleteImportMatch struct { AthleteID int64 TeamID int64 Conflict *importConflict } // existingAthlete 是库中已有人员的可比字段。 type existingAthlete struct { ID int64 Name string Gender *string TeamID int64 EmployeeNo *string } // matchImportedAthlete 按契约顺序匹配人员:证件号 → 姓名+单位(库中证件号为空则回填)→ 新建。 // 证件号命中但姓名/性别/单位与库中不一致时记为冲突;decisions 里对该证件号有决策时按决策落库。 func (s *server) matchImportedAthlete(ctx context.Context, in athleteImportRow, decisions map[string]string) (athleteImportMatch, error) { existing, err := s.athleteByIDCard(ctx, in.IDCard) switch { case err == nil: changed := changedAthleteFields(existing, in) if len(changed) == 0 { return athleteImportMatch{AthleteID: existing.ID, TeamID: in.TeamID}, nil } conflict, err := s.importConflictFor(ctx, in, existing, changed) if err != nil { return athleteImportMatch{}, err } action, decided := decisions[in.IDCard] switch { case !decided: return athleteImportMatch{Conflict: &conflict}, nil case action == decisionKeepExisting: // 人员字段保持库里的值,报名也跟随库里的单位。 return athleteImportMatch{AthleteID: existing.ID, TeamID: existing.TeamID}, nil default: if err := s.overwriteAthleteFromImport(ctx, existing.ID, in); err != nil { return athleteImportMatch{}, err } return athleteImportMatch{AthleteID: existing.ID, TeamID: in.TeamID}, nil } case errors.Is(err, sql.ErrNoRows): return s.matchOrCreateAthleteByName(ctx, in) default: return athleteImportMatch{}, internal(err) } } // matchOrCreateAthleteByName 处理证件号未命中的情况: // 姓名+单位命中且库中证件号为空 → 视为同一人并回填证件号;否则新建人员。 func (s *server) matchOrCreateAthleteByName(ctx context.Context, in athleteImportRow) (athleteImportMatch, error) { var id int64 err := s.ev.QueryRowContext(ctx, `SELECT id FROM athletes WHERE name = ? AND team_id = ? AND (id_card IS NULL OR id_card = '') ORDER BY id LIMIT 1`, in.Name, in.TeamID).Scan(&id) switch { case err == nil: if _, err := s.ev.ExecContext(ctx, `UPDATE athletes SET id_card = ? WHERE id = ?`, in.IDCard, id); err != nil { return athleteImportMatch{}, internal(err) } return athleteImportMatch{AthleteID: id, TeamID: in.TeamID}, nil case !errors.Is(err, sql.ErrNoRows): return athleteImportMatch{}, internal(err) } res, err := s.ev.ExecContext(ctx, `INSERT INTO athletes (name, gender, team_id, id_card, active) VALUES (?, ?, ?, ?, 1)`, in.Name, nullStr(&in.Gender), in.TeamID, in.IDCard) if err != nil { return athleteImportMatch{}, internal(err) } id, err = lastInsertID(res) if err != nil { return athleteImportMatch{}, internal(err) } return athleteImportMatch{AthleteID: id, TeamID: in.TeamID}, nil } func (s *server) athleteByIDCard(ctx context.Context, idCard string) (existingAthlete, error) { var ( a existingAthlete gender, empNo sql.NullString teamID sql.NullInt64 ) err := s.ev.QueryRowContext(ctx, `SELECT id, name, gender, team_id, employee_no FROM athletes WHERE id_card = ? LIMIT 1`, idCard). Scan(&a.ID, &a.Name, &gender, &teamID, &empNo) if err != nil { return existingAthlete{}, err } a.Gender = strPtr(gender) a.EmployeeNo = strPtr(empNo) a.TeamID = teamID.Int64 return a, nil } // changedAthleteFields 列出表里与库中不一致的字段名,顺序固定便于前端展示。 func changedAthleteFields(ex existingAthlete, in athleteImportRow) []string { var changed []string if ex.Name != in.Name { changed = append(changed, "name") } // 性别只认 男/女:表里没填或填了其它值时不做比较,避免误报冲突。 if in.Gender != "" && (ex.Gender == nil || *ex.Gender != in.Gender) { changed = append(changed, "gender") } if ex.TeamID != in.TeamID { changed = append(changed, "teamCode") } return changed } func (s *server) importConflictFor(ctx context.Context, in athleteImportRow, ex existingAthlete, changed []string) (importConflict, error) { existingCode, err := s.teamCodeByID(ctx, ex.TeamID) if err != nil { return importConflict{}, err } return importConflict{ Row: in.Line, IDCard: in.IDCard, AthleteID: ex.ID, Existing: importAthleteView{ Name: ex.Name, Gender: ex.Gender, TeamCode: existingCode, EmployeeNo: ex.EmployeeNo, }, Incoming: importAthleteView{ Name: in.Name, Gender: nilIfEmpty(in.Gender), TeamCode: in.TeamCode, }, ChangedFields: changed, }, nil } func (s *server) teamCodeByID(ctx context.Context, id int64) (string, error) { if id == 0 { return "", nil } var code string if err := s.ev.QueryRowContext(ctx, `SELECT code FROM teams WHERE id = ?`, id).Scan(&code); err != nil { return "", internal(err) } return code, nil } // overwriteAthleteFromImport 用表里的字段覆盖库中人员(keep-new 决策)。 // 表里没填性别时保留库里的值,避免决策后反而丢字段。 func (s *server) overwriteAthleteFromImport(ctx context.Context, id int64, in athleteImportRow) error { if _, err := s.ev.ExecContext(ctx, `UPDATE athletes SET name = ?, team_id = ?, gender = COALESCE(?, gender) WHERE id = ?`, in.Name, in.TeamID, nullStr(&in.Gender), id); err != nil { return internal(err) } return nil } // upsertImportedEntry 按「组别 + 单位 + 运动员」去重;返回 true 表示新建。 func (s *server) upsertImportedEntry(ctx context.Context, in entryDTO) (bool, error) { var existing int64 var err error if in.AthleteID != nil { err = s.ev.QueryRowContext(ctx, `SELECT id FROM entries WHERE category_id = ? AND team_id = ? AND athlete_id = ?`, in.CategoryID, in.TeamID, *in.AthleteID).Scan(&existing) } else { err = s.ev.QueryRowContext(ctx, `SELECT id FROM entries WHERE category_id = ? AND team_id = ? AND athlete_id IS NULL`, in.CategoryID, in.TeamID).Scan(&existing) } switch { case errors.Is(err, sql.ErrNoRows): if _, err := s.ev.ExecContext(ctx, `INSERT INTO entries (event_id, category_id, team_id, athlete_id, member_names, bib, seed, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, in.EventID, in.CategoryID, in.TeamID, nullI64(in.AthleteID), marshalNames(in.MemberNames), nullStr(in.Bib), nullStr(in.Seed), orDefault(in.Status, "registered")); err != nil { return false, err } return true, nil case err != nil: return false, err default: if _, err := s.ev.ExecContext(ctx, `UPDATE entries SET member_names = ?, bib = ?, seed = ?, status = ? WHERE id = ?`, marshalNames(in.MemberNames), nullStr(in.Bib), nullStr(in.Seed), orDefault(in.Status, "registered"), existing); err != nil { return false, err } return false, nil } }