diff --git a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/JoinRequestController.java b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/JoinRequestController.java
new file mode 100644
index 0000000..403fa85
--- /dev/null
+++ b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/JoinRequestController.java
@@ -0,0 +1,72 @@
+package cn.tianbaotong.fill.module.join.controller;
+
+import cn.tianbaotong.fill.common.api.ApiResult;
+import cn.tianbaotong.fill.module.join.dto.JoinDtos;
+import cn.tianbaotong.fill.module.join.service.JoinService;
+import cn.tianbaotong.fill.security.PermissionDef;
+import cn.tianbaotong.fill.security.RequirePerm;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 管理侧的加入申请审批(《03-接口设计》§8.7.3 ~ §8.7.5)。
+ *
+ *
权限码是现成的 {@code join:audit}({@code PermissionDef.JOIN_REQUEST_AUDIT})——
+ * 不需要新加:它就是为这条通道定义的。
+ *
+ *
⚠️ 没有"批量通过"接口,这是刻意的(契约 §8.7.5 的取舍):
+ * 批量通过要求管理员先逐条看过(列表已提供信息),而**驳回必须逐条填原因**;
+ * 且批量通过**不提供"全选"快捷键**。理由与"批量通过只带绿色项"同源 ——
+ * 身份认定环节的误操作代价极高。
+ */
+@Tag(name = "管理端 · 名单加入申请")
+@RestController
+@RequestMapping("/api/v1/roster/join-requests")
+@RequiredArgsConstructor
+public class JoinRequestController {
+
+ private final JoinService joinService;
+
+ @GetMapping
+ @RequirePerm(PermissionDef.JOIN_REQUEST_AUDIT)
+ @Operation(summary = "申请待办列表",
+ description = "默认只看待审(status=0)。每条带 waitHours(等最久的排最前)与 "
+ + "duplicateHint(该证件号在**其他任务**的名册里出现过,帮判断是不是本校学生)")
+ public ApiResult list(@RequestParam(required = false) Long taskId,
+ @RequestParam(required = false) Integer status,
+ @RequestParam(required = false) Long orgId,
+ @RequestParam(required = false) String keyword,
+ @RequestParam(required = false) Long pageNum,
+ @RequestParam(required = false) Long pageSize) {
+ return ApiResult.ok(joinService.list(taskId, status, orgId, keyword, pageNum, pageSize));
+ }
+
+ @PostMapping("/{id}/approve")
+ @RequirePerm(PermissionDef.JOIN_REQUEST_AUDIT)
+ @Operation(summary = "通过(建名册项 + 自动绑定)",
+ description = "事务内做三件事:建名册项、把申请人绑上去(免他重输)、回写申请。"
+ + "不传 orgId 时用学生的自述班级匹配组织名,匹配不上(或撞名)会要求手选。"
+ + "notifySent 恒为 false(通知渠道未实现),需要人工告知学生")
+ public ApiResult approve(@PathVariable Long id,
+ @RequestBody(required = false) JoinDtos.ApproveReq req) {
+ return ApiResult.ok(joinService.approve(id, req));
+ }
+
+ @PostMapping("/{id}/reject")
+ @RequirePerm(PermissionDef.JOIN_REQUEST_AUDIT)
+ @Operation(summary = "驳回(原因必填)",
+ description = "原因会原样给学生看,所以要说清「哪里不对、该怎么办」。"
+ + "已通过的申请不能驳回(40042)—— 要撤销请删名册项")
+ public ApiResult reject(@PathVariable Long id,
+ @RequestBody JoinDtos.RejectReq req) {
+ return ApiResult.ok(joinService.reject(id, req));
+ }
+}
diff --git a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/MpJoinRequestController.java b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/MpJoinRequestController.java
new file mode 100644
index 0000000..6e97873
--- /dev/null
+++ b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/join/controller/MpJoinRequestController.java
@@ -0,0 +1,48 @@
+package cn.tianbaotong.fill.module.join.controller;
+
+import cn.tianbaotong.fill.common.api.ApiResult;
+import cn.tianbaotong.fill.module.join.dto.JoinDtos;
+import cn.tianbaotong.fill.module.join.service.JoinService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * 学生侧的名单加入申请(《03-接口设计》§8.7.1 / §8.7.2)。
+ *
+ * 权限:登录即可(学生)。**不额外挂权限码** —— 这条通道就是给
+ * "门禁没匹配上、什么权限都没有"的人用的,要求权限码等于把门关上。
+ * 限次(同 openid 每任务 ≤2 次)在服务层做,那才是这条通道的闸门。
+ */
+@Tag(name = "填报端 · 名单加入申请")
+@RestController
+@RequestMapping("/api/v1/mp/roster/join-request")
+@RequiredArgsConstructor
+public class MpJoinRequestController {
+
+ private final JoinService joinService;
+
+ @PostMapping
+ @Operation(summary = "提交加入申请",
+ description = "**只解决「进名单」,不绕过门禁** —— 申请期间学生仍不能填报。"
+ + "证件号立刻转 HMAC + 脱敏,明文不落库。"
+ + "已在名册(40039)/ 已有待审申请(40038)/ 超过 2 次(40037)都会被拒")
+ public ApiResult submit(@RequestBody JoinDtos.SubmitReq req) {
+ return ApiResult.ok(joinService.submit(req));
+ }
+
+ @GetMapping
+ @Operation(summary = "我的申请(含状态与驳回原因)",
+ description = "通过之后 canFillNow=true:说明已经自动绑定,学生返回重新核验就能直接进")
+ public ApiResult