package com.campus.controller; import com.campus.common.Result; import com.campus.dto.request.XuexinSubmitRequest; import com.campus.entity.User; import com.campus.entity.UserPoints; import com.campus.entity.XuexinReport; import com.campus.service.AuthorizationService; import com.campus.service.UserPointsService; import com.campus.service.UserService; import com.campus.service.ProfileService; import com.campus.service.XuexinReportService; import com.campus.util.OssUtil; import com.campus.util.UserContext; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.LinkedHashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/profile") @RequiredArgsConstructor public class ProfileController { private final UserService userService; private final ProfileService profileService; private final UserPointsService userPointsService; private final OssUtil ossUtil; private final AuthorizationService authorizationService; private final XuexinReportService xuexinReportService; @GetMapping public Result getMyProfile() { String userId = UserContext.getCurrentUserId(); return Result.success(userService.getUserById(userId)); } @GetMapping("/other") public Result getProfile(@RequestParam String userId) { return Result.success(userService.getPublicUserById(userId)); } @PutMapping public Result updateProfile(@RequestBody User updateData) { String userId = UserContext.getCurrentUserId(); return Result.success(profileService.updateProfile(UserContext.getCurrentUserId(), userId, updateData)); } @PutMapping("/other") public Result updateProfile( @RequestParam String userId, @RequestBody User updateData) { authorizationService.requireAdmin(); return Result.success(profileService.updateProfile(UserContext.getCurrentUserId(), userId, updateData)); } /** * 学信网认证 — 提交接口。 * *

前置条件:Step1 调 {@code POST /api/ocr/chsi-report} 已拿到 {@code reportId}, * 且其结果里 {@code dedup.status=AVAILABLE}。

* *

本接口不再信任请求体里的教育字段:姓名/学校/学历等一律以 * {@code xuexin_report} 表中的 OCR 记录为准,请求体只需 {@code reportId} * 且客户端不能覆盖 PDF 中识别出的认证字段。 * 这样即使前端伪造 {@code validChsiReport},也拿不到认证。

* *

完整校验与写入逻辑见 {@link XuexinReportService#submit}:

*
    *
  • 材料不存在 / 不属于当前用户 → 400「未找到认证材料,请重新上传报告」
  • *
  • 材料已过期或被替换 → 400「该认证材料已失效,请重新上传报告」
  • *
  • 已认证且换更低学历 → 400「…不支持更换为更低学历…」
  • *
  • 已认证未满冷静期 → 400「认证通过后 3 天内不可更换认证材料,请在 … 后重试」
  • *
  • 重复提交同一份材料 → 幂等成功,原样返回当前用户
  • *
*/ @PostMapping("/xuexin/submit") public Result submitXuexin(@RequestBody XuexinSubmitRequest req) { String userId = UserContext.getCurrentUserId(); return Result.success(xuexinReportService.submit(userId, req)); } /** 上传并立即保存当前用户头像,不依赖编辑资料页的其他字段保存。 */ @PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Result uploadAvatar(@RequestParam("file") MultipartFile file) { String userId = UserContext.getCurrentUserId(); User user = userService.getUserById(userId); String avatarUrl = ossUtil.uploadAvatar(file); user.setAvatar(avatarUrl); userService.saveUser(user); return Result.success(user); } /** * 查询当前用户学信网认证状态和已确认的学籍信息。 * *

新增 {@code certifiedAt}(认证时间)、{@code reportId}(当前生效材料), * 以及新版报告上的分院、系所、日期、民族、学历类别、学习形式和在线验证码。

*/ @GetMapping("/xuexin/status") public Result> xuexinStatus() { String userId = UserContext.getCurrentUserId(); User user = userService.getUserById(userId); XuexinReport activeReport = xuexinReportService.findActiveReport(userId); Map result = new LinkedHashMap<>(); result.put("certified", Boolean.TRUE.equals(user.getXuexinCertified())); result.put("number", user.getNumber()); result.put("school", user.getUniversityName()); result.put("department", user.getCollegeName()); result.put("major", user.getMajorName()); result.put("level", user.getEducation()); result.put("grade", user.getGrade()); result.put("enrollmentStatus", user.getXuexinEnrollmentStatus()); result.put("certifiedAt", user.getXuexinCertifiedAt()); result.put("reportId", user.getXuexinReportId()); result.put("reportName", activeReport == null ? null : activeReport.getReportName()); result.put("reportUpdatedDate", activeReport == null ? null : activeReport.getReportUpdatedDate()); result.put("name", activeReport == null ? null : activeReport.getName()); result.put("gender", activeReport == null ? null : activeReport.getGender()); result.put("birthDate", activeReport == null ? null : activeReport.getBirthDate()); result.put("ethnicity", activeReport == null ? null : activeReport.getEthnicity()); result.put("studyDuration", activeReport == null ? null : activeReport.getStudyDuration()); result.put("educationCategory", activeReport == null ? null : activeReport.getEducationCategory()); result.put("studyForm", activeReport == null ? null : activeReport.getStudyForm()); result.put("branchCampus", activeReport == null ? null : activeReport.getBranchCampus()); result.put("enrollmentDate", activeReport == null ? null : activeReport.getEnrollmentDate()); result.put("departureDate", activeReport == null ? null : activeReport.getDepartureDate()); result.put("onlineVerificationCode", activeReport == null ? null : activeReport.getOnlineVerificationCode()); return Result.success(result); } @GetMapping("/points") public Result getMyPoints() { String userId = UserContext.getCurrentUserId(); return Result.success(userPointsService.getUserPoints(userId)); } @GetMapping("/points-other") public Result getUserPoints(@RequestParam String userId) { authorizationService.requireAdmin(); return Result.success(userPointsService.getUserPoints(userId)); } }