Browse Source
- Wire canCompanyViewResume into candidate detail so ResumeVisibility (PRIVATE/APPLIED_COMPANIES/VERIFIED_COMPANIES) actually hides resume fields from companies. - Add candidatePhone/candidateEmail to candidate detail, populated only when contactVisible=true, otherwise omitted via JsonInclude(NON_NULL). - Migrate company candidate list/detail queries from EntityManager to ApplicationRepository Query, aligning with repository conventions. - Document privacy behavior and add unit tests for visibility and contact logic.backup/master-local-20261012
7 changed files with 367 additions and 146 deletions
@ -0,0 +1,142 @@ |
|||||
|
# 隐私设置落地:企业候选人详情接入 ResumeVisibility 与 contactVisible |
||||
|
|
||||
|
## 一、Summary |
||||
|
|
||||
|
将已在 `privacy` 包实现但从未被调用的 `PrivacySettingsService.canCompanyViewResume(...)` 接入企业候选人**详情**接口 `GET /company/candidates/{applicationId}`,使学生的 `ResumeVisibility`(`PRIVATE`/`APPLIED_COMPANIES`/`VERIFIED_COMPANIES`)真正控制企业可见的简历要素;同时消费 `contactVisible` 开关,按学生设定选择性地返回 `phone`/`email`。 |
||||
|
|
||||
|
路线决策已在澄清中确认: |
||||
|
- **仅详情过滤**:候选人列表 `GET /company/candidates`(及 `CandidateItemView`)保持不变。 |
||||
|
- **严格按枚举**:`PRIVATE`(默认)表示企业不可见简历要素——这是默认行为变更,需同步调整现有测试断言。 |
||||
|
- **仅隐藏简历要素**:不可见时隐藏 `school/education/graduationYear/skills/bio/location`;保留 `candidateName` 与 `candidateAvatarUrl`,便于识别投递人。`coverLetter` 非简历要素,始终返回。 |
||||
|
|
||||
|
## 二、Current State Analysis |
||||
|
|
||||
|
### 关键代码位置 |
||||
|
- 隐私策略方法(已存在,未接入):[PrivacySettingsService.canCompanyViewResume](file:///d:/1/zhi/zhilai/mydreamjob-server/src/main/java/com/mydreamjob/privacy/PrivacySettingsService.java#L116-L135) |
||||
|
- 签名:`boolean canCompanyViewResume(Long studentUserId, Long companyId, Long applicationId)` |
||||
|
- `PRIVATE`→false;`APPLIED_COMPANIES`→当该投递存在且状态非 `WITHDRAWN/REJECTED` 时 true;`VERIFIED_COMPANIES`→`company.verified==true` 时 true;无记录默认按 `PRIVATE`。 |
||||
|
- 详情查询与 DTO 组装:[ApplicationService.getCompanyCandidateDetail](file:///d:/1/zhi/zhilai/mydreamjob-server/src/main/java/com/mydreamjob/application/ApplicationService.java#L341-L400) |
||||
|
- 单次 JOIN 查询 `a, j, sp, u`(Application/Job/StudentProfile/User),已能拿到 `studentUserId`(`sp.userId` / `u.getId()`)、`job.companyId`(用于越权校验=`companyId`)、`app.getId()/getStatus()`,以及待返回的 `u.getPhone()/u.getEmail()`。 |
||||
|
- 组装时直接 `.candidateSchool(sp.getSchool())` 等,尚无任何隐私过滤。 |
||||
|
- 详情 DTO:[CandidateDetailView.java](file:///d:/1/zhi/zhilai/mydreamjob-server/src/main/java/com/mydreamjob/application/dto/CandidateDetailView.java) — 无 `phone/email`,无 `@JsonInclude`。 |
||||
|
- 现有详情测试:[ApplicationServiceTest.java#L292-L374](file:///d:/1/zhi/zhilai/mydreamjob-server/src/test/java/com/mydreamjob/application/ApplicationServiceTest.java#L292-L374) 采用 `@Mock EntityManager em` + `@InjectMocks`;`getCompanyCandidateDetail_shouldReturnDetail` 断言 `candidateSchool` 等非空。 |
||||
|
|
||||
|
### 设计约束 |
||||
|
- 依赖方向:`ApplicationService` → `PrivacySettingsService`(新注入),`PrivacySettingsService` 不依赖 `ApplicationService`,**无循环依赖**。 |
||||
|
- 性能:详情本就单条 JOIN;新增隐私判断在**单条详情**上执行,最坏多 1~2 次只读查询(见第 §三.3 注释),**非列表逐行,不构成 N+1**。 |
||||
|
- 保持项目既有风格:Controller 拿身份、Service 做权限与组装、`ApiResponse` 包装、`BusinessException` 处理。 |
||||
|
|
||||
|
### 影响面(对投递/application 模块的边界) |
||||
|
本改动**只落在投递模块的一处输出**:`ApplicationService.getCompanyCandidateDetail` 及其返回的 `CandidateDetailView`。明确「改与不改」: |
||||
|
- **改**:企业候选人**详情**接口的返回(按隐私隐藏简历要素 / 条件返回 phone/email;`@JsonInclude(NON_NULL)` 使原 null 字段从 `"x":null` 变为省略)。 |
||||
|
- **不改**:学生投递的 投递/撤回/状态流转/时间线、`apply/withdraw/getTimeline` 等方法、企业候选人**列表** `CandidateItemView`/`CandidateListResponse`/列表查询,全部原样。 |
||||
|
- 注意:学生默认 `ResumeVisibility=PRIVATE` → 接入后企业默认**不再看到**详情中的简历要素,属预期行为变更(前端契约需同步)。 |
||||
|
|
||||
|
## 三、Proposed Changes |
||||
|
|
||||
|
### 1. `privacy/PrivacySettingsService.java` — 新增一个只读方法(不动现有逻辑) |
||||
|
消费 `contactVisible` 需要读取开关。新增: |
||||
|
```java |
||||
|
/** 企业是否可查看该学生的联系方式(phone/email)。无记录或 contactVisible=false → 不可见。 */ |
||||
|
public boolean canViewContact(Long studentUserId) { |
||||
|
StudentPrivacySettings s = privacyRepository.findByStudentUserId(studentUserId).orElse(null); |
||||
|
return s != null && Boolean.TRUE.equals(s.getContactVisible()); |
||||
|
} |
||||
|
``` |
||||
|
- Why:application 侧复用 privacy 包封装,保持逻辑单一来源。 |
||||
|
- How:仅新增方法,不改 `canCompanyViewResume` 与既有方法。 |
||||
|
|
||||
|
### 2. `application/dto/CandidateDetailView.java` — 加联系方式字段 + JsonInclude |
||||
|
**字段「当前 → 改后」映射(企业候选人详情):** |
||||
|
|
||||
|
| 字段 | 当前 | 改后 | |
||||
|
|---|---|---| |
||||
|
| `applicationId/jobId/jobTitle/jobCity/jobEmploymentType/status/createdAt/updatedAt` | 返回 | 不变(始终) | |
||||
|
| `coverLetter` | 返回 | 不变(非简历要素) | |
||||
|
| `candidateName`、`candidateAvatarUrl` | 返回 | 不变(保留,识别投递人) | |
||||
|
| `candidateSchool/candidateEducation/candidateGraduationYear/candidateSkills/candidateBio/candidateLocation` | 返回 | `resumeVisible` 决定(可见才返回,否则 null 省略) | |
||||
|
| `candidatePhone`、`candidateEmail` | 不返回 | **新增**:`contactVisible=true` 才返回,否则 null 省略 | |
||||
|
|
||||
|
新增: |
||||
|
```java |
||||
|
private String candidatePhone; |
||||
|
private String candidateEmail; |
||||
|
``` |
||||
|
- 类上加 `@JsonInclude(JsonInclude.Include.NON_NULL)`;隐藏的简历要素与未开放的 phone/email 会因 null 被省略(符合"省略而非暴露 null"的契约)。 |
||||
|
- Why:`contactVisible=false` 时省略联系方式;不可见时简历要素整体省略。 |
||||
|
- How:仅加字段与注解;`CandidateItemView` 不动。 |
||||
|
|
||||
|
### 3. `application/ApplicationService.java` — 注入隐私服务并接入详情 |
||||
|
**注入:** |
||||
|
```java |
||||
|
private final PrivacySettingsService privacySettingsService; |
||||
|
``` |
||||
|
(`@RequiredArgsConstructor`,构造参数自动生效。) |
||||
|
|
||||
|
**`getCompanyCandidateDetail` 内、越权校验 `job.getCompanyId().equals(companyId)` 之后:** |
||||
|
```java |
||||
|
Long studentUserId = sp != null ? sp.getUserId() : (u != null ? u.getId() : null); |
||||
|
boolean resumeVisible = studentUserId != null |
||||
|
&& privacySettingsService.canCompanyViewResume(studentUserId, companyId, app.getId()); |
||||
|
boolean contactVisible = studentUserId != null |
||||
|
&& privacySettingsService.canViewContact(studentUserId); |
||||
|
``` |
||||
|
组装 DTO 时: |
||||
|
- 简历要素(`candidateSchool/candidateEducation/candidateGraduationYear/candidateSkills/candidateBio/candidateLocation`)改为 `resumeVisible ? <真实值> : null`。 |
||||
|
- 新增 `candidatePhone/candidateEmail` 为 `contactVisible ? u.getPhone()/u.getEmail() : null`。 |
||||
|
- 其余字段(`applicationId/jobId/jobTitle/jobCity/jobEmploymentType/status/coverLetter/createdAt/updatedAt/candidateName/candidateAvatarUrl`)保持原样。 |
||||
|
|
||||
|
- Why:`canCompanyViewResume` 已封装三档判定(含默认 PRIVATE、APPLIED 的有效投递排除、VERIFIED 的企业认证),直接复用避免重复实现。 |
||||
|
- How:只读组装;**性能说明**——`canCompanyViewResume` 分支:`VERIFIED_COMPANIES` 多 1 次 `companyRepository.findById`,`APPLIED_COMPANIES` 多 1 次 `applicationRepository.findById`,`PRIVATE` 提前返回不额外查;仅作用于单条详情,非列表逐行,不构成 N+1。 |
||||
|
|
||||
|
可选(扩展预留,本次不实现):后续如需企业查看简历文件,可在详情上附带一个 `resumeFileId` 并在独立端点用同一 `canCompanyViewResume` 保护;本次仅在 DTO 注释/文档标注扩展点,不落地。 |
||||
|
|
||||
|
### 4. 测试 (`application/ApplicationServiceTest.java`) |
||||
|
- 新增 `@Mock private com.mydreamjob.privacy.PrivacySettingsService privacySettingsService;`(`@InjectMocks` 自动注入)。 |
||||
|
- **调整现有用例**(因默认 `PRIVATE` 会隐藏,必须在用例中 stub): |
||||
|
- `getCompanyCandidateDetail_shouldReturnDetail`:`when(privacySettingsService.canCompanyViewResume(STUDENT_ID, COMPANY_ID, APP_ID)).thenReturn(true);` 并补充断言 `candidatePhone`/`candidateEmail` 依 `canViewContact` 结果。 |
||||
|
- `getCompanyCandidateDetail_noStudentProfile_shouldReturnNullFields`:不变(学生 profile 为空本就 null)。 |
||||
|
- `getCompanyCandidateDetail_wrongCompany` / `appNotFound`:在越权/不存在分支后不调用隐私方法,无需 stub;需确认越权分支在隐私判断之前先抛错。 |
||||
|
- **新增用例**: |
||||
|
- `resumeVisibility_private_hidesResumeFieldsWhileKeepsIdentity`:`canCompanyViewResume=false` 时,`candidateSchool/candidateEducation/candidateGraduationYear/candidateSkills/candidateBio/candidateLocation` 为 null,但 `candidateName/candidateAvatarUrl/coverLetter` 仍非空。 |
||||
|
- `resumeVisibility_appliedCompanies_validVisible / withdrawnHidden`:验证详情侧透传给 `canCompanyViewResume` 的参数正确(可 `verify` 调用参数)。 |
||||
|
- `contactVisible_true_returnsPhoneEmail / false_hidden`:`canViewContact` 为 true 时返回 `phone/email`,false 时为 null。 |
||||
|
|
||||
|
### 5. 可选 (若需序列化断言) `application/ApplicationControllerTest.java` |
||||
|
Controller 由 `applicationService.getCompanyCandidateDetail` 直接返回 DTO,本身不涉隐私。仅在需要验证 JSON 省略时可补充:构建含 phone/email 的 detail,断言 `$.data.candidatePhone`/`$.data.candidateEmail`;及未设置时字段不存在。若现有用例已覆盖 DTO 序列化,跳过此文件改动。 |
||||
|
|
||||
|
### 6. 文档与校验(可选、最小改动) |
||||
|
- [privacy-settings-api.md](file:///d:/1/zhi/zhilai/docs/privacy-settings-api.md) §5 补充:详情接口已接入 `ResumeVisibility`(隐藏简历要素)与 `contactVisible`(条件返回 phone/email),列表仍脱敏不变。 |
||||
|
- `verify_openapi.py` 只断言 path 数量(126),新增字段不改 path,**无需改动**。 |
||||
|
|
||||
|
## 四、Assumptions & Decisions |
||||
|
|
||||
|
| 决策点 | 结论 | |
||||
|
|---|---| |
||||
|
| 过滤范围 | 仅详情接口;列表 `CandidateItemView` 不变 | |
||||
|
| 隐藏字段 | 仅简历要素`school/education/graduationYear/skills/bio/location`;保留 `candidateName/avatarUrl` 与 `coverLetter` | |
||||
|
| PRIVATE 语义 | 严格按枚举——PRIVATE(默认)企业不可见简历要素(**默认行为变更**,同步测试) | |
||||
|
| 联系方式 | `contactVisible=true` 才返回 `phone/email`,否则省略 | |
||||
|
| 数据来源 | 复用详情既有的 JOIN 结果(`sp.userId`/`u.getId()`/`u.getPhone()`/`u.getEmail()`),不新增 DB 查询,无 N+1 | |
||||
|
| 简历文件 | 本次不新增企业查看简历文件端点;在 DTO 注释与文档标注扩展点 | |
||||
|
| privacy 包 | 仅新增只读方法 `canViewContact`;`canCompanyViewResume` 等现有逻辑不动 | |
||||
|
|
||||
|
## 五、Verification |
||||
|
|
||||
|
1. 编译:`mvn -f mydreamjob-server/pom.xml -q compile` |
||||
|
2. 测试:`mvn -f mydreamjob-server/pom.xml test`(重点关注 `ApplicationServiceTest`、`ApplicationControllerTest`、`privacy.*Test`) |
||||
|
3. 全量校验:`mvn -f mydreamjob-server/pom.xml clean verify` |
||||
|
4. OpenAPI 校验:`python docs/verify_openapi.py`(应不受影响,无 path 变更) |
||||
|
5. 手动契约抽查(可选):企业 token 递减设置不同 `ResumeVisibility`,调 `GET /company/candidates/{applicationId}`,核对简历要素是否按设定省略、`contactVisible=true` 时是否返回 phone/email。 |
||||
|
6. 投递模块回归(重点):确认学生 投递/撤回/状态流转/时间线 与候选人**列表**接口行为不变(因接入点仅在详情);可用现有 `ApplicationServiceTest` 中 `apply/withdraw/getTimeline` 相关用例回归验证。 |
||||
|
|
||||
|
## 六、将修改/新增的文件清单 |
||||
|
|
||||
|
改: |
||||
|
- `mydreamjob-server/.../privacy/PrivacySettingsService.java`(新增 `canViewContact`) |
||||
|
- `mydreamjob-server/.../application/ApplicationService.java`(注入 + 详情过滤/联系方式) |
||||
|
- `mydreamjob-server/.../application/dto/CandidateDetailView.java`(+phone/email +@JsonInclude) |
||||
|
- `mydreamjob-server/src/test/java/com/mydreamjob/application/ApplicationServiceTest.java` |
||||
|
- (可选)`mydreamjob-server/src/test/java/com/mydreamjob/application/ApplicationControllerTest.java`、`docs/privacy-settings-api.md` |
||||
|
|
||||
|
不改:`CandidateItemView`、`CandidateListResponse`、列表查询、`ResumeVisibility`、`StudentPrivacySettings`、数据库迁移。 |
||||
Loading…
Reference in new issue