55 changed files with 1875 additions and 88 deletions
@ -0,0 +1,49 @@ |
|||
# 沟通姓名与头像同步:前端增量对接 |
|||
|
|||
日期:2026-10-28。对象:求职者端/企业端前端联调人员。**本次为本地后端改动,尚未提交、推送或发布;线上 Jenkins #107 不包含本次修复。** 本文沿用已确认的头像/Logo 对接结构,作为原文档的增量,不改写历史验收状态。 |
|||
|
|||
## 1. 展示规则 |
|||
|
|||
- 对方为求职者:`contactName` 优先使用当前个人资料 `realName`;缺失、空串或纯空白时回退账号 `nickname`,再回退“求职者”。姓名/昵称首尾空白去除。 |
|||
- 对方为企业:`contactName` 继续使用公司名称,`avatarUrl` 继续使用公司 Logo,不替换为 HR 的个人头像或姓名。 |
|||
- DIRECT 和 TALENT_INVITATION 的会话摘要均按上述非匿名规则展示姓名和头像。 |
|||
- 修改资料姓名不强行改写账号 nickname;修改头像不影响公司 Logo。 |
|||
- 没有新增手机号、邮箱、简历正文、用户 ID 等展示字段。原消息 senderScope、已有内部 ID 隐藏和简历权限规则保持不变。 |
|||
|
|||
## 2. 接口与权限 |
|||
|
|||
API 路径、认证和响应字段不变,无新增迁移或依赖: |
|||
|
|||
| 接口 | 用途 | |
|||
|---|---| |
|||
| `PUT /api/v1/profile/me` | 保存资料姓名 realName;已有在线简历的账号继续遵循 revision 规则,不能为了改名绕过并发保护 | |
|||
| 现有在线简历保存接口 | 保存 basicInfo.realName,并同步到资料;沿用既有完整保存及 revision 契约 | |
|||
| `GET /api/v1/conversations` | 返回最新会话摘要,前端使用 contactName、avatarUrl | |
|||
| `GET /api/v1/conversations/{id}` | 返回最新会话详情摘要,字段来源与列表一致 | |
|||
| `POST /api/v1/auth/me/avatar` | 上传本人头像,沿用原上传协议,返回新的头像版本 URL | |
|||
| `GET /api/v1/profile-images/{imageId}` | 读取头像,仍携带正常 access token;公司 Logo 的原公开规则不变 | |
|||
|
|||
不新增让前端传 userId 查他人姓名的能力。非成员不能读取会话;邀约头像读取仍检查成员、拉黑、撤回、企业认证及已有企业接受/匹配沟通授权。移除展示匿名不等于关闭这些门禁。 |
|||
|
|||
图片/文件附件继续只允许 DIRECT 会话。不能通过本次头像调整扩大 TALENT_INVITATION 的附件权限。 |
|||
|
|||
## 3. 缓存更新与前端修改点 |
|||
|
|||
1. 修改姓名后,等待保存接口真正成功;409 冲突、网络错误或服务端失败不能假装已保存。保存成功后刷新个人资料、在线简历的相应缓存。 |
|||
2. 会话列表和详情统一使用后端 contactName,不再用历史路由参数、第一次登录的 nickname、硬编码名字或旧消息快照长期覆盖。 |
|||
3. 企业端进入/回到沟通页、打开会话详情时重新读取摘要,更新标题与头像。若已有统一轮询/刷新机制,应让它同步合并这两个字段。 |
|||
4. 本次未新增“对方改名”WebSocket 事件。用户 A 改名后,不能只刷新 A 本机就声称企业 B 已刷新;B 应在上述刷新点重新拉取。要实现当前打开页面的即时更新,需前端接入既有刷新机制或另行设计事件。 |
|||
5. 头像更换后使用后端最新版本 URL,并携带认证头;旧 URL 失效不能一直沿用。姓名变化不需要拼接虚假的头像缓存版本。 |
|||
6. 切账号清理会话缓存;异步请求按账号/会话 ID 或请求代次保护,旧响应不能覆盖新账号、新会话的标题与头像。 |
|||
7. 名称/头像加载失败保留明确错误或合理占位,不显示假名字,不从手机号推断姓名。 |
|||
|
|||
## 4. 联调验收 |
|||
|
|||
- 求职者修改 realName 后,企业重新 GET 列表、详情,contactName 均返回新值;再次修改也更新,原 nickname 不被强行覆盖。 |
|||
- 无资料记录、null/空白 realName 时回退 nickname;两者均空白时为“求职者”。 |
|||
- DIRECT/TALENT_INVITATION 摘要遵循同一显示规则;企业一侧仍显示公司名称和 Logo。 |
|||
- 邀约会话的已授权成员可加载个人头像;匿名、非成员、拉黑后读取仍拒绝;企业未获沟通授权或投递撤回不因展示规则变化而获权。 |
|||
- 点击同一个会话、返回列表、重新进入、切账号等场景,标题/列表名字/头像不互相覆盖。 |
|||
- 邀约附件仍不能通过 DIRECT 附件接口读取或发送;不新增个人敏感字段。 |
|||
|
|||
请前端回传实际请求/响应(去除 Token 和个人信息)、姓名保存后的两端录屏/截图、缓存刷新方式、类型检查和测试结果。后端定向测试、真实 MySQL、同次 JAR 黑盒、前端实机与线上发布应分别登记,不能把之前 #107 的通知发布验收当作本次姓名修复也已上线。 |
|||
@ -0,0 +1,94 @@ |
|||
# 统一招聘行业目录对接说明 |
|||
|
|||
适用对象:前后端联调工程师。范围:目录、企业资料、岗位筛选、求职意向、推荐及技能映射;不包含前端实现、线上数据回填或发布授权。 |
|||
|
|||
本批为本地后端改动,不能据此认为当前线上已支持。最终验证状态以本批验收报告为准。 |
|||
|
|||
## 目录及字段参考 |
|||
|
|||
唯一主目录为 `catalog/industries-v1.json`,版本 `recruitment-industry-v1`。这是项目招聘场景目录,不宣称是国家行业标准。共18个大类、78个细分类;目录版本和编码稳定,调整展示名称不改变编码。新增、停用或拆分类别需单独审核兼容策略,不复用已发出的编码。 |
|||
|
|||
大类覆盖互联网/信息技术、金融、制造、汽车、消费品与零售、医疗健康、教育、建筑与房地产、物流与交通、能源、专业服务、生活服务、文化传媒、农林牧渔、公共与社会服务、通信、旅游与住宿、环保。 |
|||
|
|||
`GET /api/v1/jobs/industry-catalog?scope=ALL` 无需登录,返回标准 `ApiResponse`。`data`: |
|||
|
|||
```json |
|||
{ |
|||
"version": "recruitment-industry-v1", |
|||
"scope": "ALL", |
|||
"countScope": "GLOBAL_PUBLISHED_NOT_FILTERED_BY_CITY", |
|||
"items": [ |
|||
{"code":"FINANCE","parentCode":null,"name":"金融","level":1,"publishedJobCount":2,"skillIndustryCode":null}, |
|||
{"code":"FINANCE_BANKING","parentCode":"FINANCE","name":"银行","level":2,"publishedJobCount":2,"skillIndustryCode":null} |
|||
], |
|||
"unmappedPublishedJobCount": 1 |
|||
} |
|||
``` |
|||
|
|||
示例仅演示部分条目和计数,不是实际环境数据。全局序列化可能省略空值字段,前端需兼容 `null` 和字段缺省。例如已有金融技能集时 FINANCE 的 `skillIndustryCode` 是 FINANCE。 |
|||
|
|||
- `ALL`:完整两级目录,包括零岗位类别。企业资料和求职意向必须用此目录。 |
|||
- `WITH_JOBS`:只返回当前存在已发布岗位的类别及其大类,不能作为可填写的完整行业范围。 |
|||
- `publishedJobCount` 为全站 PUBLISHED 岗位数,不叠加当前城市等筛选。大类包含自身和细分类,父子数不可相加当作岗位总数。 |
|||
- `unmappedPublishedJobCount`:无法可靠映射行业的已发布岗位数;不强塞进“其他”。 |
|||
- 未知 scope 返回400;不发送中文“不限”作为编码。 |
|||
|
|||
## 企业、岗位及求职意向接入 |
|||
|
|||
### 企业 |
|||
|
|||
企业创建、更新、企业注册内的 `company` 对象可传: |
|||
|
|||
```json |
|||
{"industryCode":"FINANCE_BANKING"} |
|||
``` |
|||
|
|||
旧 `industry` 名称参数保留。若同时传编码和名称,名称必须精确映射到该编码,否则400;不做模糊猜测。更新两字段都缺省表示保留;`industryCode:""` 且名称缺省/空白表示清空。未知非空编码400。 |
|||
|
|||
企业自查、公开企业摘要、岗位列表/详情内企业摘要增加 `industryCode`。已存编码时展示目录现行名称;旧数据能精确映射时可在读取时返回编码,但不写回数据库;未知旧名称原样展示,编码为空或缺省。 |
|||
|
|||
岗位行业继承所属企业,不新增岗位独立行业字段。企业行业修改会影响其岗位的行业筛选与计数。不要从岗位标题推导行业。 |
|||
|
|||
### 岗位筛选 |
|||
|
|||
`GET /api/v1/jobs/search?industryCode=FINANCE,MANUFACTURING`。 |
|||
|
|||
最多20个去重编码、每项64字符、整个参数最多4096字符。大类包含全部细分类,同维度OR,与城市/学历等维度AND。未知编码或空分隔项400。编码匹配优先用企业已存编码;旧数据只使用精确别名匹配,未知数据仍可在不限定行业时看到。 |
|||
|
|||
旧 `industry` 名称精确筛选语义不变;新旧参数同时提供时AND,建议新前端只用 `industryCode`。旧 `/jobs/filters.industries` 仍是有岗名称选项,不是完整目录。 |
|||
|
|||
### 求职意向 |
|||
|
|||
`PUT /api/v1/profile/me/preferences` 和在线简历 `content.jobIntention` 增加 `preferredIndustryCodes` 字符串数组,最多20项。例如: |
|||
|
|||
```json |
|||
{"preferredIndustryCodes":["FINANCE_BANKING","INTERNET"],"revision":7} |
|||
``` |
|||
|
|||
偏好局部更新中编码和旧名称都缺省则保留,只传编码 `[]` 明确清空。只传旧名称会保留原文,并生成可确定的编码;同时传两组选择时,可确定的编码集合必须一致,否则400且不部分写入。两组同时带有待确认旧名称时保留未知名称,使 GET→编辑其他字段→保存不会丢失历史内容;要连同这些未知名称一起删除,请把两组数组都置为 `[]`。 |
|||
|
|||
偏好读取返回 `preferredIndustryCodes`,并以 `unmappedPreferredIndustries` 列出未知旧名称;旧名称列表仍保留。在线简历也返回编码数组;不能静默删掉旧名称。旧客户端整份保存没有编码字段时根据名称做确定映射,保持兼容。 |
|||
|
|||
原有 revision 并发保护不变:已有在线简历的旧偏好更新必须携带最新 revision,冲突仍409/6002。此功能不绕开版本校验。 |
|||
|
|||
## 推荐与技能映射 |
|||
|
|||
推荐行业引用同一主目录,数据库 `job_recommendation_taxonomies` 的 INDUSTRY 行是兼容投影,非第二套自主维护的字典。旧六个编码保留;新增编码由 V65 提供。推荐偏好按编码和大类展开参与原有偏好评分,不改变城市/薪资硬筛选规则。 |
|||
|
|||
岗位推荐和人才发现使用同一评分映射,同时归一化偏好与目标的旧别名;未知旧偏好保留为未匹配条件,不能静默移除其评分权重。岗位匹配诊断的行业参考项读取岗位所属企业的编码,不再用岗位描述猜行业;原有10分参考权重与城市/薪资门槛不变。个人数据导出同时携带已存的行业编码。 |
|||
|
|||
行业与职位类别独立。FINANCE_BANKING(银行)可以搭配 IT_SOFTWARE(软件职位类别);JOB_FAMILY 不再返回行业父级以限制选择。职位类别本身仍需已启用、类型正确。推荐配置里的行业需与业务实际保持一致,不能根据职位类别推断。 |
|||
|
|||
技能仍使用现有 `industryId + occupationId` 数据集查询方式,不把行业编码当数字ID。目录条目的 `skillIndustryCode` 仅在数据库确有对应技能集时提供;当前已有互联网、金融、教育数据,其余行业不虚构词云或学习路径。 |
|||
|
|||
技能的 occupation 与 industry 绑定表示现有数据集范围,不是“该行业只能招聘这些职业”的业务限制。前端没有可用技能映射时应明确展示“该行业技能数据暂未覆盖”,不能把它隐藏出行业选择器,也不能随意套用互联网技能。 |
|||
|
|||
## 旧数据待确认与验收限制 |
|||
|
|||
V65 仅新增企业编码/偏好编码列、索引和新增推荐行业条目,不批量回填或改写旧名称,不修改历史迁移。旧名称仅按中央目录精确别名表解析,未知或混合业务名称保留待确认。名称别名新增必须检查冲突;不得为消除待确认而统归“其他”。 |
|||
|
|||
发布前在隔离副本汇总现有企业行业、求职意向名称的映射结果供业务确认;生产回填须单独授权,不在本批自动执行。 |
|||
|
|||
联调需覆盖:完整目录与有岗目录差异、零岗位类别、编码保存重读、金融大类筛选银行、城市条件联用、未知旧名称保留、清空、revision冲突、银行招聘软件工程师、技能未覆盖提示。岗位数量无结果应展示真实空态,不能回退到全行业结果。 |
|||
|
|||
本批不修改前端,不发起真实AI/短信/地图调用;定向测试、真实MySQL、全量构建和同次JAR验证分别记账,不互相冒充。 |
|||
@ -0,0 +1,95 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.mydreamjob.common.exception.BusinessException; |
|||
import com.mydreamjob.common.exception.ErrorCode; |
|||
import java.util.*; |
|||
|
|||
/** 所有招聘行业使用同一版本化目录;旧名称仅精确别名映射,不做模糊推断。 */ |
|||
public final class IndustryCatalog { |
|||
public record Node(String code,String parentCode,String name,List<String> aliases) {} |
|||
public record Definition(String version,String description,List<Node> nodes) {} |
|||
public record Selection(String code,String name) {} |
|||
private static final Definition DEFINITION; |
|||
private static final Map<String,Node> CODES; |
|||
private static final Map<String,String> ALIASES; |
|||
static { |
|||
try(var input=IndustryCatalog.class.getResourceAsStream("/catalog/industries-v1.json")) { |
|||
if(input==null)throw new IllegalStateException("industry catalog missing"); |
|||
Definition definition=new ObjectMapper().readValue(input,Definition.class); |
|||
Map<String,Node> codes=new LinkedHashMap<>();Map<String,String> aliases=new HashMap<>(); |
|||
for(Node node:definition.nodes()) { |
|||
if(!node.code().matches("[A-Z][A-Z0-9_]{0,63}") || codes.putIfAbsent(node.code(),node)!=null) |
|||
throw new IllegalStateException("invalid or duplicate industry code"); |
|||
for(String label:labels(node)) { |
|||
String previous=aliases.putIfAbsent(key(label),node.code()); |
|||
if(previous!=null&&!previous.equals(node.code()))throw new IllegalStateException("ambiguous industry alias"); |
|||
} |
|||
} |
|||
for(Node node:codes.values())if(node.parentCode()!=null && |
|||
(!codes.containsKey(node.parentCode()) || codes.get(node.parentCode()).parentCode()!=null)) |
|||
throw new IllegalStateException("industry catalog must have two levels"); |
|||
DEFINITION=definition;CODES=Collections.unmodifiableMap(codes);ALIASES=Map.copyOf(aliases); |
|||
}catch(Exception error){throw new ExceptionInInitializerError(error);} |
|||
} |
|||
private IndustryCatalog() {} |
|||
private static String key(String text){return text.strip().toLowerCase(Locale.ROOT);} |
|||
public static String version(){return DEFINITION.version();} |
|||
public static List<Node> nodes(){return List.copyOf(CODES.values());} |
|||
public static Node node(String code){return CODES.get(code);} |
|||
public static List<String> labels(Node node){var values=new ArrayList<>(node.aliases());values.add(node.name());values.add(node.code());return List.copyOf(values);} |
|||
public static String resolveLegacy(String value){return value==null||value.isBlank()?null:ALIASES.get(key(value));} |
|||
public static String requireCode(String value){ |
|||
if(value==null || !CODES.containsKey(value.strip()))throw bad("unknown industryCode"); |
|||
return value.strip(); |
|||
} |
|||
public static String displayName(String code,String legacy){Node node=CODES.get(code);return node==null?legacy:node.name();} |
|||
public static Selection selection(String code,String legacy) { |
|||
if (legacy != null && legacy.length() > 80) throw bad("industry name exceeds 80 characters"); |
|||
if(code!=null) { |
|||
if(code.isBlank()) { |
|||
if(legacy!=null&&!legacy.isBlank())throw bad("industry and industryCode conflict"); |
|||
return new Selection(null,null); |
|||
} |
|||
String canonical=requireCode(code); |
|||
if(legacy!=null&&!legacy.isBlank()&&!canonical.equals(resolveLegacy(legacy)))throw bad("industry and industryCode conflict"); |
|||
return new Selection(canonical,node(canonical).name()); |
|||
} |
|||
return new Selection(resolveLegacy(legacy),legacy); |
|||
} |
|||
public static List<String> preferenceCodes(List<String> explicit,List<String> legacy) { |
|||
if(explicit==null)return legacy==null?List.of():legacy.stream().map(IndustryCatalog::resolveLegacy).filter(Objects::nonNull).distinct().toList(); |
|||
if(explicit.size()>20)throw bad("at most 20 industry codes"); |
|||
var result=explicit.stream().map(IndustryCatalog::requireCode).distinct().toList(); |
|||
if(legacy!=null&&!legacy.isEmpty()) { |
|||
if(!new HashSet<>(result).equals(new HashSet<>(preferenceCodes(null,legacy)))) |
|||
throw bad("preferred industries and codes conflict"); |
|||
} |
|||
return result; |
|||
} |
|||
public static List<String> preferenceNames(List<String> codes, List<String> legacy) { |
|||
var result = new ArrayList<>(codes.stream().map(code -> node(code).name()).toList()); |
|||
if (legacy != null) legacy.stream().filter(Objects::nonNull).filter(name -> resolveLegacy(name) == null).forEach(result::add); |
|||
return List.copyOf(result); |
|||
} |
|||
public static Set<String> expand(Collection<String> codes) { |
|||
Set<String> result=new LinkedHashSet<>(); |
|||
for(String code:codes){String canonical=requireCode(code);result.add(canonical);for(Node n:CODES.values())if(canonical.equals(n.parentCode()))result.add(n.code());} |
|||
return result; |
|||
} |
|||
public static List<String> legacyLabels(Collection<String> codes) { |
|||
return codes.stream().map(CODES::get).filter(Objects::nonNull).flatMap(n->labels(n).stream()).map(IndustryCatalog::key).distinct().toList(); |
|||
} |
|||
/** 共享评分输入:展开已知编码,未知旧名称仍保留为未匹配条件,不能静默移除权重。 */ |
|||
public static List<String> scoringCriteria(List<String> explicit, List<String> legacy) { |
|||
var codes = explicit == null ? preferenceCodes(null, legacy) : explicit; |
|||
var result = new LinkedHashSet<>(expand(codes)); |
|||
if (legacy != null) legacy.stream().filter(Objects::nonNull).filter(value -> resolveLegacy(value) == null).forEach(result::add); |
|||
return new ArrayList<>(result); |
|||
} |
|||
public static String scoringTarget(String value) { |
|||
String code = resolveLegacy(value); |
|||
return code == null ? value : code; |
|||
} |
|||
private static BusinessException bad(String message){return new BusinessException(ErrorCode.BAD_REQUEST,message);} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.mydreamjob.common.api.ApiResponse; |
|||
import com.mydreamjob.common.logging.RequestIdAccessor; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
@RestController @RequiredArgsConstructor @Tag(name="岗位") |
|||
public class IndustryDirectoryController { |
|||
private final IndustryDirectoryService directory; |
|||
private final RequestIdAccessor requestIds; |
|||
@GetMapping("/jobs/industry-catalog") |
|||
@Operation(operationId="getIndustryCatalog",summary="统一招聘行业目录",description="ALL为完整两级目录,WITH_JOBS仅含当前有PUBLISHED岗位的行业;计数为全站发布岗位,非当前城市筛选结果。编码稳定;未确定映射的旧名称不归其他。skillIndustryCode非空才表示已有技能数据集,不代表职位类别。") |
|||
public ApiResponse<IndustryDirectoryService.Directory> get(@RequestParam(defaultValue="ALL") String scope,HttpServletRequest request) { |
|||
return ApiResponse.ok(directory.directory(scope),requestIds.get(request)); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.mydreamjob.common.exception.*; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
import java.util.*; |
|||
|
|||
@Service @RequiredArgsConstructor |
|||
public class IndustryDirectoryService { |
|||
private final JdbcTemplate jdbc; |
|||
@io.swagger.v3.oas.annotations.media.Schema(name="IndustryDirectoryItem") |
|||
public record Item(String code,String parentCode,String name,int level,long publishedJobCount,String skillIndustryCode) {} |
|||
@io.swagger.v3.oas.annotations.media.Schema(name="IndustryDirectory") |
|||
public record Directory(String version,String scope,String countScope,List<Item> items,long unmappedPublishedJobCount) {} |
|||
public Directory directory(String scope) { |
|||
if(!Set.of("ALL","WITH_JOBS").contains(scope))throw new BusinessException(ErrorCode.BAD_REQUEST,"invalid industry directory scope"); |
|||
Map<String,Long> counts=new HashMap<>();long[] unmapped={0}; |
|||
jdbc.query("SELECT c.industry_code,c.industry,COUNT(j.id) AS n FROM jobs j LEFT JOIN companies c ON c.id=j.company_id WHERE j.status='PUBLISHED' GROUP BY c.industry_code,c.industry",rs->{ |
|||
String code=rs.getString(1);if(code==null)code=IndustryCatalog.resolveLegacy(rs.getString(2)); |
|||
long count=rs.getLong(3); |
|||
if(IndustryCatalog.node(code)==null)unmapped[0]+=count; |
|||
else {counts.merge(code,count,Long::sum);String parent=IndustryCatalog.node(code).parentCode();if(parent!=null)counts.merge(parent,count,Long::sum);} |
|||
}); |
|||
Set<String> skillCodes=new HashSet<>(jdbc.queryForList("SELECT code FROM skill_industries",String.class)); |
|||
var items=IndustryCatalog.nodes().stream().filter(n->"ALL".equals(scope)||counts.getOrDefault(n.code(),0L)>0) |
|||
.map(n->new Item(n.code(),n.parentCode(),n.name(),n.parentCode()==null?1:2,counts.getOrDefault(n.code(),0L),skillCodes.contains(n.code())?n.code():null)).toList(); |
|||
return new Directory(IndustryCatalog.version(),scope,"GLOBAL_PUBLISHED_NOT_FILTERED_BY_CITY",items,unmapped[0]); |
|||
} |
|||
} |
|||
@ -0,0 +1,629 @@ |
|||
{ |
|||
"version": "recruitment-industry-v1", |
|||
"description": "项目招聘行业目录;不宣称等同国家统计行业分类。行业与职位类别独立。", |
|||
"nodes": [ |
|||
{ |
|||
"code": "INTERNET", |
|||
"parentCode": null, |
|||
"name": "互联网/信息技术", |
|||
"aliases": [ |
|||
"互联网", |
|||
"互联网/IT", |
|||
"IT", |
|||
"信息技术" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "INTERNET_SOFTWARE", |
|||
"parentCode": "INTERNET", |
|||
"name": "软件与信息服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "INTERNET_NETWORK", |
|||
"parentCode": "INTERNET", |
|||
"name": "互联网平台", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "INTERNET_CLOUD", |
|||
"parentCode": "INTERNET", |
|||
"name": "云计算与数据服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "INTERNET_SECURITY", |
|||
"parentCode": "INTERNET", |
|||
"name": "网络安全", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "INTERNET_GAMES", |
|||
"parentCode": "INTERNET", |
|||
"name": "游戏与数字内容", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "FINANCE", |
|||
"parentCode": null, |
|||
"name": "金融", |
|||
"aliases": [ |
|||
"金融业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "FINANCE_BANKING", |
|||
"parentCode": "FINANCE", |
|||
"name": "银行", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "FINANCE_SECURITIES", |
|||
"parentCode": "FINANCE", |
|||
"name": "证券与基金", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "FINANCE_INSURANCE", |
|||
"parentCode": "FINANCE", |
|||
"name": "保险", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "FINANCE_PAYMENT", |
|||
"parentCode": "FINANCE", |
|||
"name": "支付与金融科技", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "FINANCE_ASSET", |
|||
"parentCode": "FINANCE", |
|||
"name": "资产管理", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING", |
|||
"parentCode": null, |
|||
"name": "制造业", |
|||
"aliases": [ |
|||
"制造", |
|||
"工业制造" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_MACHINERY", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "机械设备", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_ELECTRONICS", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "电子与半导体", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_ELECTRICAL", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "电气设备", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_CHEMICAL", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "化工与材料", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_METAL", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "金属与加工", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "MANUFACTURING_INSTRUMENT", |
|||
"parentCode": "MANUFACTURING", |
|||
"name": "仪器仪表", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AUTOMOTIVE", |
|||
"parentCode": null, |
|||
"name": "汽车", |
|||
"aliases": [ |
|||
"汽车行业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "AUTOMOTIVE_VEHICLE", |
|||
"parentCode": "AUTOMOTIVE", |
|||
"name": "整车制造", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AUTOMOTIVE_PARTS", |
|||
"parentCode": "AUTOMOTIVE", |
|||
"name": "汽车零部件", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AUTOMOTIVE_EV", |
|||
"parentCode": "AUTOMOTIVE", |
|||
"name": "新能源汽车", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AUTOMOTIVE_SERVICE", |
|||
"parentCode": "AUTOMOTIVE", |
|||
"name": "汽车销售与服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL", |
|||
"parentCode": null, |
|||
"name": "消费品与零售", |
|||
"aliases": [ |
|||
"零售/消费", |
|||
"零售", |
|||
"消费品" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_FMCG", |
|||
"parentCode": "RETAIL", |
|||
"name": "日用消费品", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_FOOD", |
|||
"parentCode": "RETAIL", |
|||
"name": "食品饮料", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_APPAREL", |
|||
"parentCode": "RETAIL", |
|||
"name": "服装纺织", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_HOME", |
|||
"parentCode": "RETAIL", |
|||
"name": "家居家电", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_ECOMMERCE", |
|||
"parentCode": "RETAIL", |
|||
"name": "电子商务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "RETAIL_STORE", |
|||
"parentCode": "RETAIL", |
|||
"name": "零售与批发", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE", |
|||
"parentCode": null, |
|||
"name": "医疗健康", |
|||
"aliases": [ |
|||
"医疗", |
|||
"医疗/健康" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE_HOSPITAL", |
|||
"parentCode": "HEALTHCARE", |
|||
"name": "医疗服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE_PHARMA", |
|||
"parentCode": "HEALTHCARE", |
|||
"name": "医药", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE_DEVICE", |
|||
"parentCode": "HEALTHCARE", |
|||
"name": "医疗器械", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE_BIOTECH", |
|||
"parentCode": "HEALTHCARE", |
|||
"name": "生物技术", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "HEALTHCARE_CARE", |
|||
"parentCode": "HEALTHCARE", |
|||
"name": "养老与健康服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "EDUCATION", |
|||
"parentCode": null, |
|||
"name": "教育", |
|||
"aliases": [ |
|||
"教育培训" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "EDUCATION_SCHOOL", |
|||
"parentCode": "EDUCATION", |
|||
"name": "学校教育", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "EDUCATION_VOCATIONAL", |
|||
"parentCode": "EDUCATION", |
|||
"name": "职业教育", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "EDUCATION_TRAINING", |
|||
"parentCode": "EDUCATION", |
|||
"name": "培训服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "EDUCATION_EDTECH", |
|||
"parentCode": "EDUCATION", |
|||
"name": "教育科技", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION", |
|||
"parentCode": null, |
|||
"name": "建筑与房地产", |
|||
"aliases": [ |
|||
"建筑", |
|||
"房地产" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION_ENGINEERING", |
|||
"parentCode": "CONSTRUCTION", |
|||
"name": "建筑工程", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION_DESIGN", |
|||
"parentCode": "CONSTRUCTION", |
|||
"name": "建筑设计", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION_PROPERTY", |
|||
"parentCode": "CONSTRUCTION", |
|||
"name": "房地产开发", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION_MANAGEMENT", |
|||
"parentCode": "CONSTRUCTION", |
|||
"name": "物业管理", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CONSTRUCTION_DECORATION", |
|||
"parentCode": "CONSTRUCTION", |
|||
"name": "装饰装修", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS", |
|||
"parentCode": null, |
|||
"name": "物流与交通", |
|||
"aliases": [ |
|||
"物流", |
|||
"交通运输" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS_EXPRESS", |
|||
"parentCode": "LOGISTICS", |
|||
"name": "快递物流", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS_SUPPLY_CHAIN", |
|||
"parentCode": "LOGISTICS", |
|||
"name": "仓储与供应链", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS_SHIPPING", |
|||
"parentCode": "LOGISTICS", |
|||
"name": "航运港口", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS_AVIATION", |
|||
"parentCode": "LOGISTICS", |
|||
"name": "航空", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LOGISTICS_ROAD_RAIL", |
|||
"parentCode": "LOGISTICS", |
|||
"name": "公路与铁路运输", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENERGY", |
|||
"parentCode": null, |
|||
"name": "能源", |
|||
"aliases": [ |
|||
"能源行业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "ENERGY_POWER", |
|||
"parentCode": "ENERGY", |
|||
"name": "电力", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENERGY_RENEWABLE", |
|||
"parentCode": "ENERGY", |
|||
"name": "新能源", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENERGY_OIL_GAS", |
|||
"parentCode": "ENERGY", |
|||
"name": "石油与天然气", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENERGY_MINING", |
|||
"parentCode": "ENERGY", |
|||
"name": "矿产资源", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL", |
|||
"parentCode": null, |
|||
"name": "专业服务", |
|||
"aliases": [ |
|||
"专业服务业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL_CONSULTING", |
|||
"parentCode": "PROFESSIONAL", |
|||
"name": "管理咨询", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL_LEGAL", |
|||
"parentCode": "PROFESSIONAL", |
|||
"name": "法律服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL_ACCOUNTING", |
|||
"parentCode": "PROFESSIONAL", |
|||
"name": "会计审计与税务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL_HR", |
|||
"parentCode": "PROFESSIONAL", |
|||
"name": "人力资源服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PROFESSIONAL_TESTING", |
|||
"parentCode": "PROFESSIONAL", |
|||
"name": "检验检测与认证", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES", |
|||
"parentCode": null, |
|||
"name": "生活服务", |
|||
"aliases": [ |
|||
"生活服务业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES_CATERING", |
|||
"parentCode": "LIFE_SERVICES", |
|||
"name": "餐饮", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES_DOMESTIC", |
|||
"parentCode": "LIFE_SERVICES", |
|||
"name": "家政与社区服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES_BEAUTY", |
|||
"parentCode": "LIFE_SERVICES", |
|||
"name": "美容美发", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES_REPAIR", |
|||
"parentCode": "LIFE_SERVICES", |
|||
"name": "维修与居民服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "LIFE_SERVICES_SPORTS", |
|||
"parentCode": "LIFE_SERVICES", |
|||
"name": "体育健身", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CULTURE_MEDIA", |
|||
"parentCode": null, |
|||
"name": "文化传媒", |
|||
"aliases": [ |
|||
"文化", |
|||
"传媒" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "CULTURE_MEDIA_ADVERTISING", |
|||
"parentCode": "CULTURE_MEDIA", |
|||
"name": "广告营销", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CULTURE_MEDIA_PUBLISHING", |
|||
"parentCode": "CULTURE_MEDIA", |
|||
"name": "出版与新闻", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CULTURE_MEDIA_FILM", |
|||
"parentCode": "CULTURE_MEDIA", |
|||
"name": "影视演艺", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "CULTURE_MEDIA_DESIGN", |
|||
"parentCode": "CULTURE_MEDIA", |
|||
"name": "创意设计", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AGRICULTURE", |
|||
"parentCode": null, |
|||
"name": "农林牧渔", |
|||
"aliases": [ |
|||
"农业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "AGRICULTURE_FARMING", |
|||
"parentCode": "AGRICULTURE", |
|||
"name": "种植与林业", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AGRICULTURE_LIVESTOCK", |
|||
"parentCode": "AGRICULTURE", |
|||
"name": "畜牧与渔业", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "AGRICULTURE_AGTECH", |
|||
"parentCode": "AGRICULTURE", |
|||
"name": "农业科技", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PUBLIC_SERVICES", |
|||
"parentCode": null, |
|||
"name": "公共与社会服务", |
|||
"aliases": [ |
|||
"公共服务" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "PUBLIC_SERVICES_PUBLIC", |
|||
"parentCode": "PUBLIC_SERVICES", |
|||
"name": "公共事业", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PUBLIC_SERVICES_NONPROFIT", |
|||
"parentCode": "PUBLIC_SERVICES", |
|||
"name": "社会组织与公益", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "PUBLIC_SERVICES_RESEARCH", |
|||
"parentCode": "PUBLIC_SERVICES", |
|||
"name": "科研机构", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TELECOM", |
|||
"parentCode": null, |
|||
"name": "通信", |
|||
"aliases": [ |
|||
"通信行业" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "TELECOM_OPERATOR", |
|||
"parentCode": "TELECOM", |
|||
"name": "通信运营", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TELECOM_EQUIPMENT", |
|||
"parentCode": "TELECOM", |
|||
"name": "通信设备", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TELECOM_NETWORK", |
|||
"parentCode": "TELECOM", |
|||
"name": "通信工程", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TOURISM", |
|||
"parentCode": null, |
|||
"name": "旅游与住宿", |
|||
"aliases": [ |
|||
"旅游", |
|||
"酒店" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "TOURISM_HOTEL", |
|||
"parentCode": "TOURISM", |
|||
"name": "酒店与住宿", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TOURISM_TRAVEL", |
|||
"parentCode": "TOURISM", |
|||
"name": "旅行服务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "TOURISM_SCENIC", |
|||
"parentCode": "TOURISM", |
|||
"name": "景区与文旅", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENVIRONMENT", |
|||
"parentCode": null, |
|||
"name": "环保", |
|||
"aliases": [ |
|||
"环境保护" |
|||
] |
|||
}, |
|||
{ |
|||
"code": "ENVIRONMENT_WASTE", |
|||
"parentCode": "ENVIRONMENT", |
|||
"name": "废弃物处理", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENVIRONMENT_WATER", |
|||
"parentCode": "ENVIRONMENT", |
|||
"name": "水务", |
|||
"aliases": [] |
|||
}, |
|||
{ |
|||
"code": "ENVIRONMENT_ENVTECH", |
|||
"parentCode": "ENVIRONMENT", |
|||
"name": "环保技术与工程", |
|||
"aliases": [] |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
-- 新增编码字段;不回填、不覆盖任何旧企业或求职意向名称。 |
|||
ALTER TABLE companies ADD COLUMN industry_code VARCHAR(64) NULL; |
|||
CREATE INDEX idx_companies_industry_code ON companies(industry_code); |
|||
ALTER TABLE student_preferences ADD COLUMN preferred_industry_codes JSON NULL; |
|||
|
|||
-- 旧推荐编码保留;新增目录编码供既有 FK 引用,名称展示以中央目录为准。 |
|||
INSERT INTO job_recommendation_taxonomies(code,parent_code,taxonomy_type,display_name,sort_order) VALUES |
|||
('INTERNET_SOFTWARE','INTERNET','INDUSTRY','软件与信息服务',1000), |
|||
('INTERNET_NETWORK','INTERNET','INDUSTRY','互联网平台',1001), |
|||
('INTERNET_CLOUD','INTERNET','INDUSTRY','云计算与数据服务',1002), |
|||
('INTERNET_SECURITY','INTERNET','INDUSTRY','网络安全',1003), |
|||
('INTERNET_GAMES','INTERNET','INDUSTRY','游戏与数字内容',1004), |
|||
('FINANCE_BANKING','FINANCE','INDUSTRY','银行',1005), |
|||
('FINANCE_SECURITIES','FINANCE','INDUSTRY','证券与基金',1006), |
|||
('FINANCE_INSURANCE','FINANCE','INDUSTRY','保险',1007), |
|||
('FINANCE_PAYMENT','FINANCE','INDUSTRY','支付与金融科技',1008), |
|||
('FINANCE_ASSET','FINANCE','INDUSTRY','资产管理',1009), |
|||
('MANUFACTURING_MACHINERY','MANUFACTURING','INDUSTRY','机械设备',1010), |
|||
('MANUFACTURING_ELECTRONICS','MANUFACTURING','INDUSTRY','电子与半导体',1011), |
|||
('MANUFACTURING_ELECTRICAL','MANUFACTURING','INDUSTRY','电气设备',1012), |
|||
('MANUFACTURING_CHEMICAL','MANUFACTURING','INDUSTRY','化工与材料',1013), |
|||
('MANUFACTURING_METAL','MANUFACTURING','INDUSTRY','金属与加工',1014), |
|||
('MANUFACTURING_INSTRUMENT','MANUFACTURING','INDUSTRY','仪器仪表',1015), |
|||
('AUTOMOTIVE',NULL,'INDUSTRY','汽车',1016), |
|||
('AUTOMOTIVE_VEHICLE','AUTOMOTIVE','INDUSTRY','整车制造',1017), |
|||
('AUTOMOTIVE_PARTS','AUTOMOTIVE','INDUSTRY','汽车零部件',1018), |
|||
('AUTOMOTIVE_EV','AUTOMOTIVE','INDUSTRY','新能源汽车',1019), |
|||
('AUTOMOTIVE_SERVICE','AUTOMOTIVE','INDUSTRY','汽车销售与服务',1020), |
|||
('RETAIL_FMCG','RETAIL','INDUSTRY','日用消费品',1021), |
|||
('RETAIL_FOOD','RETAIL','INDUSTRY','食品饮料',1022), |
|||
('RETAIL_APPAREL','RETAIL','INDUSTRY','服装纺织',1023), |
|||
('RETAIL_HOME','RETAIL','INDUSTRY','家居家电',1024), |
|||
('RETAIL_ECOMMERCE','RETAIL','INDUSTRY','电子商务',1025), |
|||
('RETAIL_STORE','RETAIL','INDUSTRY','零售与批发',1026), |
|||
('HEALTHCARE_HOSPITAL','HEALTHCARE','INDUSTRY','医疗服务',1027), |
|||
('HEALTHCARE_PHARMA','HEALTHCARE','INDUSTRY','医药',1028), |
|||
('HEALTHCARE_DEVICE','HEALTHCARE','INDUSTRY','医疗器械',1029), |
|||
('HEALTHCARE_BIOTECH','HEALTHCARE','INDUSTRY','生物技术',1030), |
|||
('HEALTHCARE_CARE','HEALTHCARE','INDUSTRY','养老与健康服务',1031), |
|||
('EDUCATION_SCHOOL','EDUCATION','INDUSTRY','学校教育',1032), |
|||
('EDUCATION_VOCATIONAL','EDUCATION','INDUSTRY','职业教育',1033), |
|||
('EDUCATION_TRAINING','EDUCATION','INDUSTRY','培训服务',1034), |
|||
('EDUCATION_EDTECH','EDUCATION','INDUSTRY','教育科技',1035), |
|||
('CONSTRUCTION',NULL,'INDUSTRY','建筑与房地产',1036), |
|||
('CONSTRUCTION_ENGINEERING','CONSTRUCTION','INDUSTRY','建筑工程',1037), |
|||
('CONSTRUCTION_DESIGN','CONSTRUCTION','INDUSTRY','建筑设计',1038), |
|||
('CONSTRUCTION_PROPERTY','CONSTRUCTION','INDUSTRY','房地产开发',1039), |
|||
('CONSTRUCTION_MANAGEMENT','CONSTRUCTION','INDUSTRY','物业管理',1040), |
|||
('CONSTRUCTION_DECORATION','CONSTRUCTION','INDUSTRY','装饰装修',1041), |
|||
('LOGISTICS',NULL,'INDUSTRY','物流与交通',1042), |
|||
('LOGISTICS_EXPRESS','LOGISTICS','INDUSTRY','快递物流',1043), |
|||
('LOGISTICS_SUPPLY_CHAIN','LOGISTICS','INDUSTRY','仓储与供应链',1044), |
|||
('LOGISTICS_SHIPPING','LOGISTICS','INDUSTRY','航运港口',1045), |
|||
('LOGISTICS_AVIATION','LOGISTICS','INDUSTRY','航空',1046), |
|||
('LOGISTICS_ROAD_RAIL','LOGISTICS','INDUSTRY','公路与铁路运输',1047), |
|||
('ENERGY',NULL,'INDUSTRY','能源',1048), |
|||
('ENERGY_POWER','ENERGY','INDUSTRY','电力',1049), |
|||
('ENERGY_RENEWABLE','ENERGY','INDUSTRY','新能源',1050), |
|||
('ENERGY_OIL_GAS','ENERGY','INDUSTRY','石油与天然气',1051), |
|||
('ENERGY_MINING','ENERGY','INDUSTRY','矿产资源',1052), |
|||
('PROFESSIONAL',NULL,'INDUSTRY','专业服务',1053), |
|||
('PROFESSIONAL_CONSULTING','PROFESSIONAL','INDUSTRY','管理咨询',1054), |
|||
('PROFESSIONAL_LEGAL','PROFESSIONAL','INDUSTRY','法律服务',1055), |
|||
('PROFESSIONAL_ACCOUNTING','PROFESSIONAL','INDUSTRY','会计审计与税务',1056), |
|||
('PROFESSIONAL_HR','PROFESSIONAL','INDUSTRY','人力资源服务',1057), |
|||
('PROFESSIONAL_TESTING','PROFESSIONAL','INDUSTRY','检验检测与认证',1058), |
|||
('LIFE_SERVICES',NULL,'INDUSTRY','生活服务',1059), |
|||
('LIFE_SERVICES_CATERING','LIFE_SERVICES','INDUSTRY','餐饮',1060), |
|||
('LIFE_SERVICES_DOMESTIC','LIFE_SERVICES','INDUSTRY','家政与社区服务',1061), |
|||
('LIFE_SERVICES_BEAUTY','LIFE_SERVICES','INDUSTRY','美容美发',1062), |
|||
('LIFE_SERVICES_REPAIR','LIFE_SERVICES','INDUSTRY','维修与居民服务',1063), |
|||
('LIFE_SERVICES_SPORTS','LIFE_SERVICES','INDUSTRY','体育健身',1064), |
|||
('CULTURE_MEDIA',NULL,'INDUSTRY','文化传媒',1065), |
|||
('CULTURE_MEDIA_ADVERTISING','CULTURE_MEDIA','INDUSTRY','广告营销',1066), |
|||
('CULTURE_MEDIA_PUBLISHING','CULTURE_MEDIA','INDUSTRY','出版与新闻',1067), |
|||
('CULTURE_MEDIA_FILM','CULTURE_MEDIA','INDUSTRY','影视演艺',1068), |
|||
('CULTURE_MEDIA_DESIGN','CULTURE_MEDIA','INDUSTRY','创意设计',1069), |
|||
('AGRICULTURE',NULL,'INDUSTRY','农林牧渔',1070), |
|||
('AGRICULTURE_FARMING','AGRICULTURE','INDUSTRY','种植与林业',1071), |
|||
('AGRICULTURE_LIVESTOCK','AGRICULTURE','INDUSTRY','畜牧与渔业',1072), |
|||
('AGRICULTURE_AGTECH','AGRICULTURE','INDUSTRY','农业科技',1073), |
|||
('PUBLIC_SERVICES',NULL,'INDUSTRY','公共与社会服务',1074), |
|||
('PUBLIC_SERVICES_PUBLIC','PUBLIC_SERVICES','INDUSTRY','公共事业',1075), |
|||
('PUBLIC_SERVICES_NONPROFIT','PUBLIC_SERVICES','INDUSTRY','社会组织与公益',1076), |
|||
('PUBLIC_SERVICES_RESEARCH','PUBLIC_SERVICES','INDUSTRY','科研机构',1077), |
|||
('TELECOM',NULL,'INDUSTRY','通信',1078), |
|||
('TELECOM_OPERATOR','TELECOM','INDUSTRY','通信运营',1079), |
|||
('TELECOM_EQUIPMENT','TELECOM','INDUSTRY','通信设备',1080), |
|||
('TELECOM_NETWORK','TELECOM','INDUSTRY','通信工程',1081), |
|||
('TOURISM',NULL,'INDUSTRY','旅游与住宿',1082), |
|||
('TOURISM_HOTEL','TOURISM','INDUSTRY','酒店与住宿',1083), |
|||
('TOURISM_TRAVEL','TOURISM','INDUSTRY','旅行服务',1084), |
|||
('TOURISM_SCENIC','TOURISM','INDUSTRY','景区与文旅',1085), |
|||
('ENVIRONMENT',NULL,'INDUSTRY','环保',1086), |
|||
('ENVIRONMENT_WASTE','ENVIRONMENT','INDUSTRY','废弃物处理',1087), |
|||
('ENVIRONMENT_WATER','ENVIRONMENT','INDUSTRY','水务',1088), |
|||
('ENVIRONMENT_ENVTECH','ENVIRONMENT','INDUSTRY','环保技术与工程',1089); |
|||
@ -0,0 +1,39 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.mydreamjob.common.exception.BusinessException; |
|||
import org.junit.jupiter.api.Test; |
|||
import java.util.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
|
|||
class IndustryCatalogTest { |
|||
@Test void catalogHasBroadTwoLevelCoverageAndStableLegacyCodes() { |
|||
assertEquals(18, IndustryCatalog.nodes().stream().filter(n -> n.parentCode() == null).count()); |
|||
assertTrue(IndustryCatalog.nodes().size() > 90); |
|||
for (String code : List.of("INTERNET", "FINANCE", "MANUFACTURING", "HEALTHCARE", "EDUCATION", "RETAIL", |
|||
"AUTOMOTIVE", "CONSTRUCTION", "LOGISTICS", "ENERGY", "PROFESSIONAL", "LIFE_SERVICES")) { |
|||
assertNotNull(IndustryCatalog.node(code)); |
|||
assertTrue(IndustryCatalog.nodes().stream().anyMatch(n -> code.equals(n.parentCode()))); |
|||
} |
|||
} |
|||
@Test void aliasesAreExactAndUnknownsStayUnmapped() { |
|||
assertEquals("INTERNET", IndustryCatalog.resolveLegacy(" 互联网/IT ")); |
|||
assertEquals("FINANCE_BANKING", IndustryCatalog.resolveLegacy("银行")); |
|||
assertNull(IndustryCatalog.resolveLegacy("软件工程师")); |
|||
assertNull(IndustryCatalog.resolveLegacy("未核实的综合业务")); |
|||
} |
|||
@Test void codeAndLabelConflictsAreRejected() { |
|||
assertThrows(BusinessException.class, () -> IndustryCatalog.selection("FINANCE_BANKING", "教育")); |
|||
assertThrows(BusinessException.class, () -> IndustryCatalog.selection("UNKNOWN", null)); |
|||
assertEquals("银行", IndustryCatalog.selection("FINANCE_BANKING", null).name()); |
|||
} |
|||
@Test void parentExpansionDoesNotConflateOccupation() { |
|||
assertTrue(IndustryCatalog.expand(List.of("FINANCE")).contains("FINANCE_BANKING")); |
|||
assertEquals(Set.of("FINANCE_BANKING"), IndustryCatalog.expand(List.of("FINANCE_BANKING"))); |
|||
assertThrows(BusinessException.class, () -> IndustryCatalog.expand(List.of("IT_SOFTWARE"))); |
|||
} |
|||
@Test void legacyPreferenceKeepsUnknownOutsideKnownCodeSet() { |
|||
assertEquals(List.of("FINANCE"), IndustryCatalog.preferenceCodes(null, List.of("金融", "未知旧名称"))); |
|||
assertThrows(BusinessException.class, () -> IndustryCatalog.preferenceCodes(List.of("FINANCE"), List.of("教育"))); |
|||
assertEquals(List.of(), IndustryCatalog.preferenceCodes(List.of(), List.of())); |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.mydreamjob.config.MySql57MigrationConfiguration; |
|||
import org.flywaydb.core.Flyway; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.testcontainers.containers.MySQLContainer; |
|||
import java.sql.*; |
|||
import java.util.*; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
|
|||
/** 独立验证 V64→V65,不改写旧名称、历史 checksum 或既有技能数据。 */ |
|||
class IndustryMySqlMigrationTest { |
|||
@Test void mysql84() throws Exception { verify("mysql:8.4"); } |
|||
@Test void mysql57() throws Exception { verify("mysql:5.7.36"); } |
|||
private Flyway flyway(MySQLContainer<?> db, String version) { |
|||
var config = Flyway.configure().dataSource(db.getJdbcUrl(), db.getUsername(), db.getPassword()) |
|||
.locations("classpath:db/migration").target(version); |
|||
MySql57MigrationConfiguration.configure(config); return config.load(); |
|||
} |
|||
private List<String> history(Statement sql) throws SQLException { |
|||
var result = new ArrayList<String>(); |
|||
try (var rs = sql.executeQuery("SELECT version,checksum,success FROM flyway_schema_history WHERE version<>'65' ORDER BY installed_rank")) { |
|||
while (rs.next()) result.add(rs.getString(1) + "|" + rs.getString(2) + "|" + rs.getBoolean(3)); |
|||
} |
|||
return result; |
|||
} |
|||
private void verify(String image) throws Exception { |
|||
try (var db = new MySQLContainer<>(image).withDatabaseName("industry_migration").withUsername("industry_test") |
|||
.withPassword("synthetic-only").withCommand("--max-allowed-packet=33554432", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci") |
|||
.withUrlParam("characterEncoding", "UTF-8") |
|||
.withCreateContainerCmdModifier(c -> c.getHostConfig().withMemory(768L * 1024 * 1024))) { |
|||
db.start(); flyway(db, "64").migrate(); |
|||
try (var connection = DriverManager.getConnection(db.getJdbcUrl(), db.getUsername(), db.getPassword()); var sql = connection.createStatement()) { |
|||
sql.execute("INSERT INTO users(id,phone,role,is_active,is_verified) VALUES(965001,'19800965001','COMPANY',1,1),(965002,'19800965002','STUDENT',1,0)"); |
|||
sql.execute("INSERT INTO companies(id,owner_user_id,name,industry,verified) VALUES(965001,965001,'legacy company','unconfirmed legacy label',1)"); |
|||
sql.execute("INSERT INTO student_preferences(student_user_id,preferred_industries) VALUES(965002,JSON_ARRAY('old unknown industry','finance'))"); |
|||
var before = history(sql); |
|||
assertEquals(1, flyway(db, "65").migrate().migrationsExecuted); |
|||
assertEquals(before, history(sql)); |
|||
try (var rs = sql.executeQuery("SELECT industry,industry_code FROM companies WHERE id=965001")) { |
|||
assertTrue(rs.next()); assertEquals("unconfirmed legacy label", rs.getString(1)); assertNull(rs.getString(2)); |
|||
} |
|||
try (var rs = sql.executeQuery("SELECT preferred_industries,preferred_industry_codes FROM student_preferences WHERE student_user_id=965002")) { |
|||
assertTrue(rs.next()); assertTrue(rs.getString(1).contains("old unknown industry")); assertNull(rs.getString(2)); |
|||
} |
|||
try (var rs = sql.executeQuery("SELECT count(*) FROM job_recommendation_taxonomies WHERE taxonomy_type='INDUSTRY'")) { |
|||
assertTrue(rs.next()); assertEquals(IndustryCatalog.nodes().size(), rs.getInt(1)); |
|||
} |
|||
try (var rs = sql.executeQuery("SELECT code,display_name,parent_code FROM job_recommendation_taxonomies WHERE taxonomy_type='INDUSTRY'")) { |
|||
while (rs.next()) { assertNotNull(IndustryCatalog.node(rs.getString(1))); assertEquals(IndustryCatalog.node(rs.getString(1)).parentCode(), rs.getString(3)); } |
|||
} |
|||
try (var rs = sql.executeQuery("SELECT code FROM skill_industries")) { |
|||
while (rs.next()) assertNotNull(IndustryCatalog.node(rs.getString(1))); |
|||
} |
|||
assertEquals(0, flyway(db, "65").migrate().migrationsExecuted); |
|||
flyway(db, "65").validate(); assertEquals(before, history(sql)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,145 @@ |
|||
package com.mydreamjob.industry; |
|||
|
|||
import com.fasterxml.jackson.databind.*; |
|||
import com.mydreamjob.auth.*; |
|||
import com.mydreamjob.company.*; |
|||
import com.mydreamjob.company.dto.*; |
|||
import com.mydreamjob.job.*; |
|||
import com.mydreamjob.preference.*; |
|||
import com.mydreamjob.preference.dto.StudentPreferenceRequest; |
|||
import com.mydreamjob.onlineresume.*; |
|||
import com.mydreamjob.recommendation.JobRecommendationService; |
|||
import jakarta.persistence.EntityManager; |
|||
import org.junit.jupiter.api.*; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.test.context.ActiveProfiles; |
|||
import org.springframework.test.web.servlet.MockMvc; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import java.util.*; |
|||
import java.util.concurrent.atomic.AtomicLong; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; |
|||
|
|||
@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("integration-test") @Transactional |
|||
class UnifiedIndustryIntegrationTest { |
|||
private static final AtomicLong PHONE = new AtomicLong(19865000000L); |
|||
@Autowired EntityManager em; |
|||
@Autowired MockMvc mvc; |
|||
@Autowired ObjectMapper mapper; |
|||
@Autowired IndustryDirectoryService directory; |
|||
@Autowired CompanyService companies; |
|||
@Autowired JobService jobs; |
|||
@Autowired PreferenceService preferences; |
|||
@Autowired OnlineResumeService resumes; |
|||
@Autowired JobRecommendationService recommendations; |
|||
@MockBean RedisTemplate<String, Object> redis; |
|||
|
|||
User user(User.Role role) { |
|||
var u = User.builder().phone(Long.toString(PHONE.incrementAndGet())).role(role).roles(Set.of(role)).active(true).build(); |
|||
em.persist(u); em.flush(); return u; |
|||
} |
|||
Company company(String name, String code, String legacy) { |
|||
var owner = user(User.Role.COMPANY); |
|||
var c = Company.builder().ownerUserId(owner.getId()).name(name).industryCode(code).industry(legacy).verified(true).build(); |
|||
em.persist(c); em.flush(); return c; |
|||
} |
|||
Job job(Company company, String status) { |
|||
var j = Job.builder().companyId(company.getId()).title("行业独立测试软件工程师").description("合成岗位") |
|||
.city("行业测试城").employmentType("FULL_TIME").status(status).build(); |
|||
em.persist(j); em.flush(); return j; |
|||
} |
|||
JsonNode data(String path) throws Exception { |
|||
return mapper.readTree(mvc.perform(get(path)).andExpect(status().isOk()).andReturn().getResponse().getContentAsString(java.nio.charset.StandardCharsets.UTF_8)).path("data"); |
|||
} |
|||
@Test void completeDirectoryIsPublicAndContainsZeroJobIndustries() throws Exception { |
|||
assertEquals(IndustryCatalog.nodes().size(), data("/jobs/industry-catalog").path("items").size()); |
|||
mvc.perform(get("/jobs/industry-catalog?scope=OTHER")).andExpect(status().isBadRequest()); |
|||
assertTrue(directory.directory("ALL").items().stream().anyMatch(i -> i.publishedJobCount() == 0)); |
|||
} |
|||
@Test void runtimeContractDocumentsDirectoryAndStableCodes() throws Exception { |
|||
var response = mvc.perform(get("/v3/api-docs")).andExpect(status().isOk()).andReturn().getResponse().getContentAsString(java.nio.charset.StandardCharsets.UTF_8); |
|||
java.nio.file.Files.writeString(java.nio.file.Path.of("target/industry-runtime-openapi.json"), response); |
|||
var spec = mapper.readTree(response); |
|||
assertEquals("getIndustryCatalog", spec.path("paths").path("/jobs/industry-catalog").path("get").path("operationId").asText()); |
|||
assertTrue(spec.path("components").path("schemas").path("CompanyUpdateRequest").path("properties").has("industryCode")); |
|||
assertTrue(spec.path("components").path("schemas").path("StudentPreferenceRequest").path("properties").has("preferredIndustryCodes")); |
|||
} |
|||
@Test void countsIncludeLegacyAliasesAndChildrenButExcludeDraftAndUnknown() { |
|||
var bank = company("银行企业", "FINANCE_BANKING", "银行"); |
|||
job(bank, "PUBLISHED"); job(bank, "DRAFT"); |
|||
job(company("旧银行", null, "银行"), "PUBLISHED"); |
|||
var before = directory.directory("ALL").unmappedPublishedJobCount(); |
|||
job(company("未知", null, "不可确认旧行业"), "PUBLISHED"); |
|||
var view = directory.directory("WITH_JOBS"); |
|||
assertEquals(before + 1, view.unmappedPublishedJobCount()); |
|||
assertEquals(2, view.items().stream().filter(i -> i.code().equals("FINANCE_BANKING")).findFirst().orElseThrow().publishedJobCount()); |
|||
assertTrue(view.items().stream().anyMatch(i -> i.code().equals("FINANCE") && i.publishedJobCount() >= 2)); |
|||
assertTrue(view.items().stream().allMatch(i -> i.publishedJobCount() > 0)); |
|||
} |
|||
@Test void codeFilteringUsesCompanyIndustryIncludesDescendantsAndLegacy() throws Exception { |
|||
job(company("编码银行", "FINANCE_BANKING", "名称以后可变"), "PUBLISHED"); |
|||
job(company("历史银行", null, "银行"), "PUBLISHED"); |
|||
job(company("制造业", "MANUFACTURING", "制造业"), "PUBLISHED"); |
|||
var result = data("/jobs/search?city=行业测试城&industryCode=FINANCE"); |
|||
assertEquals(2, result.path("total").asInt()); |
|||
assertEquals(2, data("/jobs/search?city=行业测试城&industryCode=FINANCE_BANKING").path("total").asInt()); |
|||
mvc.perform(get("/jobs/search?industryCode=UNKNOWN")).andExpect(status().isBadRequest()); |
|||
mvc.perform(get("/jobs/search?industryCode=FINANCE,,EDUCATION")).andExpect(status().isBadRequest()); |
|||
} |
|||
@Test void companyWritesStoreCodeAndRejectConflictingNamesAtomically() { |
|||
var owner = user(User.Role.COMPANY); |
|||
var req = new CompanyCreateRequest(); req.setName("编码创建"); req.setIndustryCode("AUTOMOTIVE"); |
|||
var c = companies.createCompany(owner.getId(), req); |
|||
assertEquals("AUTOMOTIVE", c.getIndustryCode()); assertEquals("汽车", c.getIndustry()); |
|||
var update = new CompanyUpdateRequest(); update.setName("不可写入"); update.setIndustryCode("FINANCE"); update.setIndustry("教育"); |
|||
assertThrows(com.mydreamjob.common.exception.BusinessException.class, () -> companies.updateCompany(owner.getId(), update)); |
|||
assertEquals("编码创建", c.getName()); |
|||
} |
|||
@Test void unknownLegacyCompanyNameIsPreservedAndNotBackfilled() { |
|||
var c = company("旧资料", null, "待业务确认的行业"); |
|||
assertNull(c.getIndustryCode()); assertEquals("待业务确认的行业", c.getIndustry()); |
|||
em.clear(); assertNull(em.find(Company.class, c.getId()).getIndustryCode()); |
|||
} |
|||
@Test void preferenceCodesRoundTripToOnlineResumeAndClear() { |
|||
var student = user(User.Role.STUDENT); |
|||
var req = new StudentPreferenceRequest(); req.setPreferredIndustryCodes(List.of("FINANCE_BANKING")); |
|||
assertEquals(List.of("FINANCE_BANKING"), preferences.savePreferences(student.getId(), req).get("preferredIndustryCodes")); |
|||
assertEquals(List.of("FINANCE_BANKING"), resumes.get(student.getId()).content().getJobIntention().getPreferredIndustryCodes()); |
|||
req.setPreferredIndustryCodes(List.of()); preferences.savePreferences(student.getId(), req); |
|||
assertEquals(List.of(), resumes.get(student.getId()).content().getJobIntention().getPreferredIndustryCodes()); |
|||
} |
|||
@Test void unknownLegacyPreferencesRemainPendingAndConflictsRejectBeforeMutation() { |
|||
var student = user(User.Role.STUDENT); |
|||
var req = new StudentPreferenceRequest(); req.setPreferredIndustries(List.of("金融", "未知旧行业")); |
|||
var view = preferences.savePreferences(student.getId(), req); |
|||
assertEquals(List.of("FINANCE"), view.get("preferredIndustryCodes")); |
|||
assertEquals(List.of("未知旧行业"), view.get("unmappedPreferredIndustries")); |
|||
req.setPreferredIndustryCodes(List.of("EDUCATION")); req.setExpectedSalaryMin(90000); |
|||
assertThrows(com.mydreamjob.common.exception.BusinessException.class, () -> preferences.savePreferences(student.getId(), req)); |
|||
assertNull(preferences.getPreferences(student.getId()).get("expectedSalaryMin")); |
|||
} |
|||
@Test void bankCanRecruitSoftwareEngineerWithoutChangingOccupationCodes() { |
|||
assertDoesNotThrow(() -> ReflectionTestUtils.invokeMethod(recommendations, "validateTaxonomy", "FINANCE_BANKING", "IT_SOFTWARE")); |
|||
assertThrows(RuntimeException.class, () -> ReflectionTestUtils.invokeMethod(recommendations, "validateTaxonomy", "IT_SOFTWARE", "FINANCE")); |
|||
assertTrue(recommendations.taxonomies().stream().filter(t -> "JOB_FAMILY".equals(t.getType())).allMatch(t -> t.getParentCode() == null)); |
|||
} |
|||
@Test void recommendationNormalizesBothSidesAndKeepsUnknownLegacyCriterion() { |
|||
var preference = StudentPreference.builder().preferredIndustries("[\"IT\",\"待确认旧行业\"]").build(); |
|||
List<String> normalized = ReflectionTestUtils.invokeMethod(recommendations, "normalizedIndustries", preference); |
|||
assertTrue(normalized.containsAll(List.of("INTERNET", "INTERNET_SOFTWARE", "待确认旧行业"))); |
|||
var profile = com.mydreamjob.recommendation.JobRecommendationProfile.builder().industryCode("IT").jobFamilyCode("IT_SOFTWARE").build(); |
|||
java.math.BigDecimal score = ReflectionTestUtils.invokeMethod(recommendations, "preferenceScore", preference, profile, Job.builder().build()); |
|||
assertEquals(0, new java.math.BigDecimal("100.00").compareTo(score)); |
|||
} |
|||
@Test void skillsExposeOnlyExistingDatasetMappingNotInventedCoverage() { |
|||
var items = directory.directory("ALL").items(); |
|||
assertTrue(items.stream().anyMatch(i -> "INTERNET".equals(i.skillIndustryCode()))); |
|||
assertNull(items.stream().filter(i -> i.code().equals("AUTOMOTIVE")).findFirst().orElseThrow().skillIndustryCode()); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue