Files
martial-admin-mini/doc/后端开发快速上手.md
宅房 5b75d0f4eb docs: 新增快速参考和项目状态看板
## 新增文档

1. 快速参考.md - 一页纸快速参考卡片
   - 3步启动流程
   - 接口清单
   - 调试技巧
   - 常见问题速查

2. 项目状态看板.md - 实时项目状态跟踪
   - 总体进度(72%)
   - 任务清单(前端100%,后端44%)
   - 接口开发状态
   - 测试状态
   - 代码统计
   - 时间线和里程碑

3. doc/后端开发快速上手.md - 后端开发者30分钟上手指南
   - 6步实现流程
   - 完整代码示例
   - SQL示例
   - VO类定义
   - 测试方法

## 文档体系

现在共有 21 个文档,约 25,000+ 行
- 快速上手文档: 3个
- 开发规范文档: 5个
- 测试指南文档: 2个
- 状态报告文档: 3个
- 项目说明文档: 8个

## 项目状态

 前端完成度: 100%
⚠️ 后端完成度: 44%
📚 文档完成度: 100%

下一步: 后端开始开发 5 个接口

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 01:00:42 +08:00

13 KiB
Raw Blame History

后端开发快速上手指南

30分钟了解需要做什么然后开始开发


📋 你需要做什么

创建1个Controller实现5个接口

@RestController
@RequestMapping("/api/mini")
public class MartialMiniController {
    // 5个接口方法
}

预计工作量: 6人天约1周


🚀 第一步创建Controller30分钟

1. 创建文件

src/main/java/org/springblade/modules/martial/controller/
└── MartialMiniController.java

2. 基础代码

package org.springblade.modules.martial.controller;

import org.springblade.core.tool.api.R;
import org.springframework.web.bind.annotation.*;
import lombok.AllArgsConstructor;

/**
 * 武术评分系统 - 小程序专用接口
 *
 * @author 你的名字
 */
@RestController
@RequestMapping("/api/mini")
@AllArgsConstructor
public class MartialMiniController {

    // 注入需要的Service
    // private IMartialJudgeInviteService judgeInviteService;
    // private IMartialAthleteService athleteService;
    // private IMartialScoreService scoreService;

    /**
     * 小程序登录
     */
    @PostMapping("/login")
    public R<LoginVO> login(@RequestBody LoginDTO dto) {
        // TODO: 实现登录逻辑
        return R.success(null);
    }

    /**
     * 获取评委的选手列表(普通评委)
     */
    @GetMapping("/athletes")
    public R<List<AthleteScoreVO>> getMyAthletes(
        @RequestParam Long judgeId,
        @RequestParam Long venueId,
        @RequestParam Long projectId
    ) {
        // TODO: 实现获取选手列表逻辑
        return R.success(null);
    }

    /**
     * 获取选手列表(裁判长)
     */
    @GetMapping("/athletes/admin")
    public R<List<AthleteAdminVO>> getAthletesForAdmin(
        @RequestParam Long competitionId,
        @RequestParam Long venueId,
        @RequestParam Long projectId
    ) {
        // TODO: 实现裁判长选手列表逻辑
        return R.success(null);
    }

    /**
     * 获取评分详情(裁判长查看)
     */
    @GetMapping("/score/detail/{athleteId}")
    public R<ScoreDetailVO> getScoreDetail(@PathVariable Long athleteId) {
        // TODO: 实现评分详情逻辑
        return R.success(null);
    }

    /**
     * 修改评分(裁判长)
     */
    @PutMapping("/score/modify")
    public R<ModifyResultVO> modifyScore(@RequestBody ModifyScoreDTO dto) {
        // TODO: 实现修改评分逻辑
        return R.success(null);
    }
}

🔴 第二步实现登录接口2天

接口规范

POST /api/mini/login

请求:
{
  "matchCode": "123",
  "inviteCode": "pub"
}

响应:
{
  "code": 200,
  "success": true,
  "msg": "登录成功",
  "data": {
    "token": "xxx",
    "userRole": "pub",
    "matchId": "123",
    "matchName": "2025年全国武术散打锦标赛...",
    "matchTime": "2025年6月25日 9:00",
    "judgeId": "456",
    "judgeName": "欧阳丽娜",
    "venueId": "1",
    "venueName": "第一场地",
    "projects": ["女子组长拳", "男子组陈氏太极拳"]
  }
}

实现逻辑

@PostMapping("/login")
public R<LoginVO> login(@RequestBody LoginDTO dto) {
    // 1. 验证比赛编码
    MartialCompetition competition = competitionService.getOne(
        Wrappers.<MartialCompetition>lambdaQuery()
            .eq(MartialCompetition::getCompetitionCode, dto.getMatchCode())
            .eq(MartialCompetition::getIsDeleted, 0)
    );
    if (competition == null) {
        return R.fail("比赛编码不存在");
    }

    // 2. 验证邀请码
    MartialJudgeInvite invite = judgeInviteService.getOne(
        Wrappers.<MartialJudgeInvite>lambdaQuery()
            .eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode())
            .eq(MartialJudgeInvite::getCompetitionId, competition.getId())
            .eq(MartialJudgeInvite::getIsUsed, 0)
            .eq(MartialJudgeInvite::getIsDeleted, 0)
            .gt(MartialJudgeInvite::getExpireTime, LocalDateTime.now())
    );
    if (invite == null) {
        return R.fail("邀请码错误或已失效");
    }

    // 3. 生成Token使用BladeX的Token生成机制
    String token = generateToken(invite.getJudgeId());

    // 4. 更新邀请码使用状态
    invite.setIsUsed(1);
    invite.setUseTime(LocalDateTime.now());
    invite.setAccessToken(token);
    judgeInviteService.updateById(invite);

    // 5. 查询评委信息
    MartialJudge judge = judgeService.getById(invite.getJudgeId());

    // 6. 查询场地信息(如果有)
    MartialVenue venue = null;
    if (invite.getVenueId() != null) {
        venue = venueService.getById(invite.getVenueId());
    }

    // 7. 解析项目列表
    List<String> projects = JSON.parseArray(invite.getProjects(), String.class);

    // 8. 构建响应
    LoginVO vo = new LoginVO();
    vo.setToken(token);
    vo.setUserRole(invite.getRole()); // "judge" 或 "chief_judge"
    vo.setMatchId(competition.getId().toString());
    vo.setMatchName(competition.getCompetitionName());
    vo.setMatchTime(formatDateTime(competition.getStartTime()));
    vo.setJudgeId(judge.getId().toString());
    vo.setJudgeName(judge.getJudgeName());
    if (venue != null) {
        vo.setVenueId(venue.getId().toString());
        vo.setVenueName(venue.getVenueName());
    }
    vo.setProjects(projects);

    return R.success(vo);
}

SQL示例

-- 验证邀请码
SELECT
  ji.id,
  ji.judge_id AS judgeId,
  ji.role,
  ji.venue_id AS venueId,
  ji.projects,
  j.judge_name AS judgeName,
  c.id AS matchId,
  c.competition_name AS matchName,
  c.start_time AS matchTime,
  v.venue_name AS venueName
FROM martial_judge_invite ji
LEFT JOIN martial_judge j ON ji.judge_id = j.id
LEFT JOIN martial_competition c ON ji.competition_id = c.id
LEFT JOIN martial_venue v ON ji.venue_id = v.id
WHERE ji.invite_code = ?
  AND c.competition_code = ?
  AND ji.is_used = 0
  AND ji.expire_time > NOW()
  AND ji.is_deleted = 0

🔴 第三步实现选手列表接口1天

接口规范

GET /api/mini/athletes?judgeId=456&venueId=1&projectId=5

响应:
{
  "code": 200,
  "success": true,
  "msg": "操作成功",
  "data": [
    {
      "athleteId": "1",
      "name": "张三",
      "idCard": "123456789000000000",
      "team": "少林寺武术大学院",
      "number": "123-4567898275",
      "myScore": 8.906,
      "totalScore": 8.907,
      "scored": true,
      "scoreTime": "2025-06-25 09:15:00"
    }
  ]
}

SQL示例

SELECT
  a.id AS athleteId,
  a.player_name AS name,
  a.id_card AS idCard,
  a.team_name AS team,
  a.player_no AS number,
  a.total_score AS totalScore,
  s.score AS myScore,
  CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS scored,
  s.score_time AS scoreTime
FROM martial_athlete a
LEFT JOIN martial_score s
  ON a.id = s.athlete_id
  AND s.judge_id = ?
WHERE a.venue_id = ?
  AND a.project_id = ?
  AND a.is_deleted = 0
ORDER BY a.order_num ASC

🟡 第四步实现裁判长选手列表1天

接口规范

GET /api/mini/athletes/admin?competitionId=123&venueId=1&projectId=5

响应:
{
  "code": 200,
  "success": true,
  "msg": "操作成功",
  "data": [
    {
      "athleteId": "1",
      "name": "张三",
      "idCard": "123456789000000000",
      "team": "少林寺武术大学院",
      "number": "123-4567898275",
      "totalScore": 8.907,
      "judgeCount": 6,
      "totalJudges": 6,
      "canModify": true
    }
  ]
}

SQL示例

SELECT
  a.id AS athleteId,
  a.player_name AS name,
  a.id_card AS idCard,
  a.team_name AS team,
  a.player_no AS number,
  a.total_score AS totalScore,
  COUNT(s.id) AS judgeCount,
  (SELECT COUNT(*) FROM martial_judge_project jp
   WHERE jp.project_id = ? AND jp.is_deleted = 0) AS totalJudges,
  CASE WHEN COUNT(s.id) = (SELECT COUNT(*) FROM martial_judge_project jp
   WHERE jp.project_id = ? AND jp.is_deleted = 0) THEN 1 ELSE 0 END AS canModify
FROM martial_athlete a
LEFT JOIN martial_score s ON a.id = s.athlete_id
WHERE a.venue_id = ?
  AND a.project_id = ?
  AND a.is_deleted = 0
GROUP BY a.id
ORDER BY a.order_num ASC

🟡 第五步实现评分详情接口1天

接口规范

GET /api/mini/score/detail/1

响应:
{
  "code": 200,
  "success": true,
  "msg": "操作成功",
  "data": {
    "athleteInfo": {
      "athleteId": "1",
      "name": "张三",
      "idCard": "123456789000000000",
      "team": "少林寺武术大学院",
      "number": "123-4567898275",
      "totalScore": 8.907
    },
    "judgeScores": [
      {
        "judgeId": "1",
        "judgeName": "欧阳丽娜",
        "score": 8.907,
        "scoreTime": "2025-06-25 09:15:00",
        "note": ""
      }
    ],
    "modification": null
  }
}

SQL示例

-- 选手信息
SELECT
  a.id AS athleteId,
  a.player_name AS name,
  a.id_card AS idCard,
  a.team_name AS team,
  a.player_no AS number,
  a.total_score AS totalScore
FROM martial_athlete a
WHERE a.id = ?

-- 评委评分
SELECT
  s.judge_id AS judgeId,
  s.judge_name AS judgeName,
  s.score,
  s.score_time AS scoreTime,
  s.note
FROM martial_score s
WHERE s.athlete_id = ?
ORDER BY s.score_time ASC

🟡 第六步实现修改评分接口1天

接口规范

PUT /api/mini/score/modify

请求:
{
  "athleteId": "1",
  "modifierId": "789",
  "modifiedScore": 8.910,
  "note": "修改原因"
}

响应:
{
  "code": 200,
  "success": true,
  "msg": "修改成功",
  "data": {
    "athleteId": "1",
    "originalScore": 8.907,
    "modifiedScore": 8.910,
    "modifyTime": "2025-06-25 10:00:00"
  }
}

实现逻辑

@PutMapping("/score/modify")
public R<ModifyResultVO> modifyScore(@RequestBody ModifyScoreDTO dto) {
    // 1. 验证权限(只有裁判长可以修改)
    MartialJudgeInvite invite = judgeInviteService.getOne(
        Wrappers.<MartialJudgeInvite>lambdaQuery()
            .eq(MartialJudgeInvite::getJudgeId, dto.getModifierId())
            .eq(MartialJudgeInvite::getRole, "chief_judge")
            .eq(MartialJudgeInvite::getIsDeleted, 0)
    );
    if (invite == null) {
        return R.fail("无权限修改评分");
    }

    // 2. 查询当前总分
    MartialAthlete athlete = athleteService.getById(dto.getAthleteId());
    BigDecimal originalScore = athlete.getTotalScore();

    // 3. 更新选手总分
    athlete.setTotalScore(dto.getModifiedScore());
    athlete.setUpdatedBy(dto.getModifierId());
    athlete.setUpdateTime(LocalDateTime.now());
    athleteService.updateById(athlete);

    // 4. 记录修改信息可以在athlete表中添加字段或创建修改记录表
    // ...

    // 5. 构建响应
    ModifyResultVO vo = new ModifyResultVO();
    vo.setAthleteId(dto.getAthleteId().toString());
    vo.setOriginalScore(originalScore);
    vo.setModifiedScore(dto.getModifiedScore());
    vo.setModifyTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    return R.success(vo);
}

📝 创建VO类

LoginVO.java

package org.springblade.modules.martial.vo;

import lombok.Data;
import java.util.List;

@Data
public class LoginVO {
    private String token;
    private String userRole;
    private String matchId;
    private String matchName;
    private String matchTime;
    private String judgeId;
    private String judgeName;
    private String venueId;
    private String venueName;
    private List<String> projects;
}

AthleteScoreVO.java

package org.springblade.modules.martial.vo;

import lombok.Data;
import java.math.BigDecimal;

@Data
public class AthleteScoreVO {
    private String athleteId;
    private String name;
    private String idCard;
    private String team;
    private String number;
    private BigDecimal myScore;
    private BigDecimal totalScore;
    private Boolean scored;
    private String scoreTime;
}

AthleteAdminVO.java

package org.springblade.modules.martial.vo;

import lombok.Data;
import java.math.BigDecimal;

@Data
public class AthleteAdminVO {
    private String athleteId;
    private String name;
    private String idCard;
    private String team;
    private String number;
    private BigDecimal totalScore;
    private Integer judgeCount;
    private Integer totalJudges;
    private Boolean canModify;
}

🧪 测试接口

使用Postman测试

1. 测试登录

POST http://localhost:8080/api/mini/login
Content-Type: application/json

{
  "matchCode": "123",
  "inviteCode": "pub"
}

2. 测试选手列表

GET http://localhost:8080/api/mini/athletes?judgeId=456&venueId=1&projectId=5
Blade-Auth: Bearer {token}

检查清单

开发前

  • 阅读接口规范文档
  • 了解现有数据库表结构
  • 准备测试数据

开发中

  • 创建 MartialMiniController
  • 创建所有VO类
  • 实现登录接口
  • 实现选手列表接口2个
  • 实现评分详情接口
  • 实现修改评分接口

开发后

  • 单元测试通过
  • Postman测试通过
  • 更新Swagger文档
  • 通知前端联调

📞 需要帮助?


预计完成时间: 6人天约1周

开始开发吧! 🚀