Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Job Hunting(职位猎人) - 一款协助找工作的浏览器插件

logo

为什么要做这个项目

当前国内使用率较高的招聘平台(排名不分先后)分别有 BOOS 直聘,前程无忧,智联招聘,猎聘网,拉勾网,其提供了各个行业的职位招聘信息的展示。但在实际使用过程中发现其展示职位信息的策略对于求职者有诸多不便,包括不仅限于:职位发布时间久远(俗称僵尸岗),不能简单识别普通职位,职位发布时间被隐藏或乱序显示,职位的公司名不是全称,没有职位公司的风险提示。

项目做了什么

为了提高使用这些招聘平台找工作的用户体验,项目会对目标平台网站页面进行增强展示;对出现过的职位进行永久存储,以便进行全网职位的个性化快速搜索,职位数据的共享;对职位数据进行多维度的分析,并以可视化的手段呈现;通过内置的讨论区,对职位进行评论,为职位打上标签等方式进行中立的职位交流;

运行截图

招聘/企业信息网站页面

搜索页(前程无忧)

51job

推荐页(BOSS 直聘)

51job

详情页

job-snapshot-51job

职位快照

job-snapshot-history-51job

爱企查

aiqicha

管理页面

打开管理页面

chrome_extension_sidepanel_open

管理页面(需点击插件图标打开)

sidepanel_admin_home

最近主要改动/新增特性

  1. 新增内置大模型引擎web-llm

招聘平台支持列表

招聘平台访问地址备注
BOSS 直聘https://www.zhipin.com/web/geek/jobs推荐页/搜索页[账号未登录]
https://www.zhipin.com/web/geek/jobs推荐页/搜索页[账号已登录]
前程无忧https://we.51job.com/pc/search搜索页
智联招聘https://sou.zhaopin.com/搜索页
拉钩网https://www.lagou.com/wn/zhaopin搜索页
猎聘网https://www.liepin.com/zhaopin搜索页,需点击搜索按钮才有效果
就业在线https://www.jobonline.cn/position搜索页
广东公共求职招聘服务平台https://ggfw.hrss.gd.gov.cn/recruitment/internet/main/#/search?type=1搜索页

企业搜索平台支持列表

企业搜索平台访问地址备注
爱企查https://aiqicha.baidu.com/s

浏览器支持

Edge
Edge
Chrome
Chrome
last versionlast version

教程

快速开始

职位猎人插件

  1. 打开 Release 页 或 直接访问 最新发布
  2. 点击下载 Assets 下的 job-hunting-extension-chrome-xxx.zip
  3. 打开浏览器,安装插件,下面是针对不同浏览器的安装步骤
    1. chrome:地址栏输入 chrome://extensions/,打开开发者模式,将 zip 文件拖进页面里
    2. edge,地址栏输入 edge://extensions/,打开开发人员模式,将 zip 文件拖进页面里
  4. 打开页面

开发

浏览器插件

Important

项目根目录: apps/extension

编译

  1. 安装,编译
    pnpm i
    pnpm run build
  1. 打开 chrome,选择加载已解压的扩展程序,选择当前项目的 .output/chrome-mv3 目录

  2. 打开页面

开发

  1. 安装,编译

    pnpm i
    pnpm run dev
    
  2. chrome 浏览器打开 chrome://extensions/ 页面

  3. 点击加载已解压的扩展程序

  4. 选择项目中生成的 .output/chrome-mv3-dev 文件夹即可

  5. 每次保存都会重新编译,扩展程序需要**重新点一次刷新按钮**才生效

测试

https://vitalets.github.io/playwright-bdd/

深入开发

架构

block
  columns 1
  招聘网站
  桌面浏览器
  职位猎人浏览器插件
  block
    数据同步服务
    公司信息查询服务
    LLM服务
    Oauth服务
    BBS服务
  end
  1. 招聘网站

    • 支持桌面版的招聘网站版面
    • 提供职位信息列表
  2. 桌面浏览器

    • Chrome,Edge
  3. 职位猎人浏览器插件

  4. 外部服务

    1. 数据同步服务

      Git
      
      • 基础信息
        • 职位信息
        • 公司信息
        • 职位标签
        • 公司标签
      • 公开信息
        • 职位信息(首次扫描时间)
      • 数据源
    2. 公司信息查询服务

      • 基础信息
      • 风评
    3. 公司评论查询服务

    4. LLM 服务

      内嵌 LLM(Web llm),远程 LLM
      
      • 职位分析
    5. Oauth 服务

      Github Oauth2
      
    6. BBS 服务

      Github Issues API
      
      • 讨论区
      • 评论(职位,公司)

职位猎人浏览器插件

职位展示页面

  1. 数据获取
    • 职位
    • 公司
  2. 数据持久化
  3. 自定义职位卡片信息
    • 职位发布/初见时间
    • 公司特性检测(外包,培训机构等…)
    • 年龄限制检测
    • 职位浏览/展示次数
    • 职位匹配度
    • HR 活跃状态
    • 标签
      • 职位
      • 公司
    • 评论
      • 职位
      • 公司
    • 公司风评检测
    • 公司官网
      • 可达性检测
      • 备案信息检测

后台管理

  1. 职位搜索
    • 列表
    • 地图
  2. 职位浏览历史
  3. 职位详情快照
  4. 自动化
    • 职位列表自动浏览
  5. 讨论区
  6. 数据
    • 职位
    • 公司
    • 标签
    • 职位标签
    • 公司标签
    • 职位公开数据
    • 公司评论
  7. 数据源
    • 列表
    • 元数据
  8. 任务
    • 统计
    • 详情
  9. 系统
    • 设置
      • 数据云备份和分享
    • 数据管理
      • 数据导入
      • 数据导出
    • 数据库
      • 概况
      • 调试
    • 文件

核心逻辑

从网站获取数据和渲染额外信息

职位数据

sequenceDiagram
  participant browser as 浏览器
  participant extension as 插件
  autonumber
  extension ->> browser: 注册待侦测目标页面
  browser ->> browser: 打开职位列表页面
  browser ->> extension: 触发侦测目标页面事件
  extension ->> browser: 注入脚本(拦截XMLHttpRequest)到目标页面
  extension ->> extension: 初始化Extension Bridge
  browser ->> extension: 发送职位列表数据
  extension ->> extension: 监听查找职位列表界面元素
  extension ->> extension: 解析职位列表数据
  extension ->> extension: 保存职位信息到持久层
  extension ->> extension: 从持久层获取保存的职位信息
  extension ->> browser: 在职位项界面上进行自定义职位信息渲染
  extension ->> browser: 在职位项界面上进行自定义公司信息界面框架渲染
  extension ->> browser: 渲染职位评论框架
  opt
    browser ->> extension: 获取和渲染公司信息
    alt 持久层含有指定未过期的公司信息
      extension ->> extension: 从持久层获取保存的公司信息
    else
      extension ->> extension: 向公司信息服务查询公司信息
      extension ->> extension: 保存公司信息到持久层
      extension ->> extension: 从持久层获取保存的公司信息
    end
    extension ->> browser: 渲染自定义公司信息
    extension ->> browser: 渲染公司评论框架
    opt
      browser ->> extension: 获取和渲染公司评论信息
      extension ->> browser: 渲染公司评论信息
    end
  end
  opt
    browser ->> extension: 获取和渲染职位评论信息
    extension ->> browser: 渲染职位评论信息
  end

公司数据

sequenceDiagram
  participant browser as 浏览器
  participant extension as 插件
  autonumber
  extension ->> browser: 注册待侦测目标页面
  browser ->> browser: 打开公司列表页面
  browser ->> extension: 触发侦测目标页面事件
  extension ->> browser: 注入脚本(拦截XMLHttpRequest)到目标页面
  extension ->> extension: 初始化Extension Bridge
  browser ->> extension: 发送公司列表数据
  extension ->> extension: 监听查找公司列表界面元素
  extension ->> extension: 解析公司列表数据
  alt 持久层含有指定未过期的公司信息
    extension ->> extension: 从持久层获取保存的公司信息
  else
    extension ->> extension: 向公司信息服务查询公司信息
    extension ->> extension: 保存公司信息到持久层
    extension ->> extension: 从持久层获取保存的公司信息
  end
  extension ->> browser: 在公司项界面上进行自定义公司信息渲染
  extension ->> browser: 渲染自定义公司信息
  extension ->> browser: 渲染公司评论框架
  opt
    browser ->> extension: 获取和渲染公司评论信息
    extension ->> browser: 渲染公司评论信息
  end

自定义职位卡片渲染

┌────────────────────────────────────────────────────────────────┐
│                                                │ Job extra info│
│────────────────────────────────────────────────────────────────│
│                     Job info                                   │
│                                                                │
│────────────────────────────────────────────────────────────────│
│                     Company info                               │
│                                                                │
│                                                                │
│                                                                │
│────────────────────────────────────────────────────────────────│
│ Company evaluation checking                                    │
│────────────────────────────────────────────────────────────────│
│ Company tag                                                    │
│────────────────────────────────────────────────────────────────│
│                   Othre|Company comment| Online company comment│
│────────────────────────────────────────────────────────────────│
│ Job tag                                                        │
│────────────────────────────────────────────────────────────────│
│ Job browse statistics ===                           Job Comment│
└────────────────────────────────────────────────────────────────┘
  1. 监听职位列表元素 ▲

    平台职位列表元素定位备注
    前程无忧.joblist
    BOSS 直聘.rec-job-list
    猎聘网.job-list-box
    智联招聘.positionlist__list
    拉勾网.list__YibNq
    就业在线.position-wrap
    广东公共求职招聘服务平台.ant-list-items
  2. 为职位列表容器设置 flex 布局(使得在不改变 dom 结构的情况下,通过设置 css 的 order 属性来达到改变职位项前后位置)

  3. 渲染加载中元素

  4. 渲染职位额外信息

    1. 进行年龄限制的检测渲染
    2. 对职位时间进行处理渲染(初见时间/发布时间)
    3. Hr 活跃时间渲染
    4. 职位的公司信息(外包/培训机构)渲染
    5. 对职位时间进行染色
    6. 职位分析检测和渲染
  5. 隐藏加载中元素

  6. 修改职位卡片 css 的 order 属性,对职位卡片进行排序

  7. 渲染底部功能栏

    1. 渲染 logo
    2. 公司信息渲染
      1. 查询公司信息按钮渲染
      2. 对公司名处理
      3. 公司详情渲染
      4. 公司标签渲染(其他人/我)
      5. 公司风评渲染
      6. 其他途径查询弹窗渲染
      7. 在线公司评论按钮渲染
    3. 职位标签渲染(其他人/我)
    4. 职位查看和展示次数渲染
  8. 最终渲染

    1. 职位评论按钮渲染
    2. 职位卡片渲染完成标识

内部 API

内部 API 注册与使用

1. Service 注册(worker.js)

const ACTION_FUNCTION = new Map();

export const WorkerBridge = {
  ping: function (message, param) {
    postSuccessMessage(message, 'pong');
  },
};

const { mergeServiceMethod } = useService();

mergeServiceMethod(ACTION_FUNCTION, WorkerBridge);
mergeServiceMethod(ACTION_FUNCTION, DataSourceMetadataService);

2. API 注册(api/index.js)

export const DataSourceMetadataApi = {
  dataSourceMetadataSearch: mockFunction,
  dataSourceMetadataAddOrUpdate: mockFunction,
  dataSourceMetadataBatchAddOrUpdate: mockFunction,
  dataSourceMetadataGetById: mockFunction,
  dataSourceMetadataGetByIds: mockFunction,
  dataSourceMetadataDeleteById: mockFunction,
  dataSourceMetadataDeleteByIds: mockFunction,
};
fillBridgeApi({ api: DataSourceMetadataApi });

3. API 使用

Important

通过 API 方法来调用 Service 方法

const param = new DataSourceMetadataSearchBO();
param.enable = true;
param.orderByColumn = 'seq';
param.orderBy = 'ASC';
const result = await DataSourceMetadataApi.dataSourceMetadataSearch(param);

约束

  1. 方法名约定: ClassName+MethodName,如 DataSourceMetadataApi
    • ClassName: DataSourceMetadata
    • MethodName: Search
    • 结果为: dataSourceMetadataSearch
  2. 方法传入参数类型约定: JSONObject 或其他基本数据类型
  3. 方法都为 async function

原理

  • 利用 JSONObject 的 keys,value 进行 Service 方法的绑定
const fillBridgeApi = ({ api = {} } = {}) => {
  const keys = Object.keys(api);
  keys.forEach((invokeName) => {
    api[invokeName] = async (param) => {
      const result = await invoke(invokeName, param);
      return result.data;
    };
  });
  return api;
};
  • 手动声明进行 Service 方法绑定
export const JobSnapshotApi = {
  jobSnapshotDeleteByIds: async function (param) {
    const result = await invoke(this.jobSnapshotDeleteByIds.name, param);
    return result.data;
  },
};
  • 利用方法名来定位 Service 和 Service 方法: className+MethodName

内部 API 调用原理

Important

action = className+MethodName

从 ContentScript 调用

sequenceDiagram
  participant Api
  participant ContentScript
  participant Background
  participant Offscreen
  participant WebWorker
  autonumber
  ContentScript ->>  Api: 调用Api方法
  Api ->> ContentScript: 调用invoke方法,传递action,param
  ContentScript ->> ContentScript: 生成callbackId和Promise
  ContentScript ->> ContentScript: 关联callbackId和当前生成的Promise
  ContentScript ->> Background: 发送Message
  alt 如果Background可以处理该Message
    Background ->> Background: 处理Message
    Background -->> ContentScript: 返回处理后的Message
  else
    Background ->> Offscreen: 转发Message
    Offscreen ->> WebWorker: 转发Message
    WebWorker ->> WebWorker: 处理Message
    WebWorker -->> Offscreen: 返回处理后的Message
    Offscreen -->> Background: 转发处理后的Message
    Background -->> ContentScript: 转发处理后的Message
  end
  ContentScript ->> ContentScript:根据返回Message的callbadkId查找Promise
  alt 如果Message含有error
    ContentScript ->> Api:调用Promise.reject返回错误
  else
    ContentScript ->> Api:调用Promise.resolve返回结果
  end

从 Background 调用

sequenceDiagram
  participant Api
  participant Background
  participant Offscreen
  participant WebWorker
  autonumber
  Background ->>  Api: 调用Api方法
  Api ->> Background: 调用invoke方法,传递action,param
  Background ->> Background: 生成callbackId和Promise
  Background ->> Background: 关联callbackId和当前生成的Promise
  Background ->> Offscreen: 发送Message
  Offscreen ->> WebWorker: 转发Message
  WebWorker ->> WebWorker: 处理Message
  WebWorker -->> Offscreen: 返回处理后的Message
  Offscreen -->> Background: 转发处理后的Message
  Background ->> Background:根据返回Message的callbadkId查找Promise
  alt 如果Message含有error
    Background ->> Api:调用Promise.reject返回错误
  else
    Background ->> Api:调用Promise.resolve返回结果
  end

从 WebWorker 调用

Important

  • Webworker 发起的 Api 调用不会经过 ContentScript,Background,Offscreen 这些模块的处理流程
  • 调用的方法仅限于 Github Api (仅执行网络访问)

对大 Message 传递的处理

Important

https://github.com/lastsunday/job-hunting/commit/ae0cee1

内嵌数据库

SQL Class

---
title: Database
---
classDiagram
    class Database
    Database: +initDb$({ dataDir } = {})
    Database: +getOne$(sql, bind, obj, { connection = null } = {})
    Database: +getAll$(sql, bind, obj, { connection = null } = {})
    Database: +batchInsert$(obj, tableName, params, { overrideCreateDatetime = false, overrideUpdateDatetime = false, connection = null } = {})
    Database: +batchInsertOrReplace$(obj, tableName, tableIdColumn, params, { replace = true, overrideCreateDatetime = false, overrideUpdateDatetime = false, connection = null } = {})
    Database: +one$(entity, tableName, idColumn, id, { connection = null } = {})
    Database: +all$(entity, tableName, orderBy, { connection = null } = {})
    Database: +batchGet$(obj, tableName, idColumnName, ids, { connection = null } = {})
    Database: +del$(tableName, idColumn, id, { otherCondition = null, connection = null } = {})
    Database: +batchDel$(tableName, idColumn, ids, { otherCondition = null, connection = null } = {})
    Database: +search$(entity, tableName, param, whereConditionFunction, { connection = null } = {})
    Database: +searchCount$(entity, tableName, param, whereConditionFunction, { connection = null } = {})
    Database: +sort$(tableName, idColumnName, param, { connection = null } = {})
    Database: +innerInit({ dataDir } = {})
    Database: +dbExport(message, param)
    Database: +dbImport(message, param)
    Database: +dbClose(message, param)
    Database: +dbDelete(message, param)
    Database: +dbSize(message, param)
    Database: +dbSchemaVersion(message, param)
    Database: +dbExec(message, param)
    Database: +dbGetAllTableName(message, param)

    class BaseService
    BaseService: +constructor(tableName, tableIdColumn, entityClassCreateFunction, searchDTOCreateFunction, whereConditionFunction)
    BaseService: +search(message, param, { detailInjectAsyncCallback = null, entityClassCreateFunction = null } = {})
    BaseService: +count(message, param)
    BaseService: +getOne(message, param, column)
    BaseService: +getById(message, param)
    BaseService: +getByIds(message, param)
    BaseService: +addOrUpdate(message, param)
    BaseService: +deleteById(message, id, column)
    BaseService: +deleteByIds(message, ids, column)
    BaseService: #_search(param, { detailInjectAsyncCallback = null, connection = null, entityClassCreateFunction = null } = {})
    BaseService: #_count()
    BaseService: #_getOne(param, column)
    BaseService: #_getById(param, { connection = null } = {})
    BaseService: #_getByIds(param, { connection = null } = {})
    BaseService: #_deleteById(id, column, { otherCondition, connection = null } = {})
    BaseService: #_deleteByIds(ids, column, { connection = null, otherCondition = null } = {})
    BaseService: #_updateByIds(ids, column, { otherCondition })
    BaseService: #_addOrUpdate(param, { overrideUpdateDatetime = false, overrideCreateDatetime = false, connection = null } = {})
    BaseService: #_batchAddOrUpdate(params, { connection = null, overrideCreateDatetime = false, overrideUpdateDatetime = false, genIdFunction = null, entityClassCreateFunction = null } = {})

    class BaseBridgeService
    BaseBridgeService: +constructor(baseServiceInstance, serviceName)
    BaseBridgeService: +getMethodName(name)
    BaseBridgeService: +getMethodNameMap()
    BaseBridgeService: +addServiceMethod$({ bridgeService = null, methodName = null, methodFunction = async ({ param = null } = {}) => { } } = {})
    BaseBridgeService: +addTransactionServiceMethod$({ bridgeService = null, methodName = null, methodFunction = async ({ param = null, tx = null } = {}) => { } } = {})
    BaseBridgeService: +fillBaseServiceMethod$({ bridgeService = null, overrideUpdateDatetime = false, overrideCreateDatetime = false } = {})

    BaseService ..> Database
    BaseBridgeService ..> BaseService

Service 实现

import { DataSourceMetadataSearchBO } from '@/common/data/bo/dataSourceMetadataSearchBO';
import { DataSourceMetadata } from '@/common/data/domain/dataSourceMetadata';
import BaseBridgeService, { fillBaseServiceMethod } from './baseBridgeService';
import { BaseService } from './baseService';
import {
  genEqValueConditionSql,
  genInTextSql,
  genLikeSql,
  genRangeDatetimeConditionSql,
} from './sqlUtil';
const TABLE_NAME = 'data_source_metadata';
const TABLE_ID_COLUMN = 'id';
const SERVICE_NAME = 'dataSourceMetadata';
export const SERVICE_INSTANCE = new BaseService(
  TABLE_NAME,
  TABLE_ID_COLUMN,
  () => {
    return new DataSourceMetadata();
  },
  () => {
    return new DataSourceMetadataSearchBO();
  },
  (param) => {
    let whereCondition = ''.concat(
      genInTextSql(param.id, 'id'),
      genLikeSql(param.name, 'name'),
      genEqValueConditionSql(param.enable, 'enable'),
      genInTextSql(param.type, 'type'),
      genEqValueConditionSql(param.autoUpdateEnable, 'auto_update_enable'),
      genRangeDatetimeConditionSql(
        param.startDatetimeForCreate,
        param.endDatetimeForCreate,
        'create_datetime'
      ),
      genRangeDatetimeConditionSql(
        param.startDatetimeForUpdate,
        param.endDatetimeForUpdate,
        'update_datetime'
      )
    );
    return whereCondition;
  }
);
const DataSourceMetadataService = new BaseBridgeService(
  SERVICE_INSTANCE,
  SERVICE_NAME
);
fillBaseServiceMethod({
  bridgeService: DataSourceMetadataService,
  overrideCreateDatetime: true,
  overrideUpdateDatetime: true,
});

export default DataSourceMetadataService;

ORM 机制

  1. 例子
const company = new Company();
await CompanyApi.addOrUpdateCompany(company);

//companyService
const SERVICE_INSTANCE = new BaseService(
  'company',
  'company_id',
  () => {
    return new Company();
  },
  () => {
    return new SearchCompanyDTO();
  },
  null
);

export const CompanyService = {
  addOrUpdateCompany: async function (message, param) {
    try {
      await SERVICE_INSTANCE._batchAddOrUpdate([param]);
      postSuccessMessage(message, {});
    } catch (e) {
      postErrorMessage(
        message,
        '[worker] addOrUpdateCompany error : ' + e.message
      );
    }
  },
};
  1. 当前实现的特性

    1. 根据 JSONObject 进行 SQL 查询,新增,更新,删除
  2. 底层原理

    1. 利用 JSONObject 的 keys,value 来识别列名,字段类型和内容。通过此来进行 SQL 的生成和返回结果 JSONObject 的生成
    2. JSONObject 属性名采用小驼峰命名法(lowerCamelCase)
    3. 表字段名采用下划线命名法(Snake Case)
    4. SQL 插入字段的值位置采用下标定位的方式,如$1
    5. 分页采用 LIMIT,OFFSET

Schema Changes

const changelogList = getChangeLogList();
let oldVersion = 0;
const newVersion = changelogList.length;
try {
  await db.transaction(async (tx) => {
    const SQL_CREATE_TABLE_VERSION = `
          CREATE TABLE IF NOT EXISTS version(
          num INTEGER
        )
      `;
    await tx.exec(SQL_CREATE_TABLE_VERSION);
    const SQL_QUERY_VERSION = 'SELECT num FROM version';
    const result = await tx.query(SQL_QUERY_VERSION);
    const rows = result.rows;
    if (rows.length > 0) {
      oldVersion = rows[0].num;
    } else {
      const SQL_INSERT_VERSION = `INSERT INTO version(num) values($1)`;
      await tx.query(SQL_INSERT_VERSION, [0]);
    }
    infoLog(
      '[DB] schema oldVersion = ' + oldVersion + ', newVersion = ' + newVersion
    );
    if (newVersion > oldVersion) {
      infoLog('[DB] schema upgrade start');
      for (let i = oldVersion; i < newVersion; i++) {
        const currentVersion = i + 1;
        const changelog = changelogList[i];
        const sqlList = changelog.getSqlList();
        infoLog(
          '[DB] schema upgrade changelog version = ' +
            currentVersion +
            ', sql total = ' +
            sqlList.length
        );
        for (let seq = 0; seq < sqlList.length; seq++) {
          infoLog(
            '[DB] schema upgrade changelog version = ' +
              currentVersion +
              ', execute sql = ' +
              (seq + 1) +
              '/' +
              sqlList.length
          );
          const sql = sqlList[seq];
          await tx.exec(sql);
        }
      }
      const SQL_UPDATE_VERSION = `UPDATE version SET num = $1`;
      await tx.query(SQL_UPDATE_VERSION, [newVersion]);
      infoLog('[DB] schema upgrade finish to version = ' + newVersion);
      infoLog('[DB] current schema version = ' + newVersion);
    } else {
      infoLog('[DB] skip schema upgrade');
      infoLog('[DB] current schema version = ' + oldVersion);
    }
  });
} catch (e) {
  errorLog('[DB] schema upgrade fail,' + e.message);
}
  1. ChangeLog 文件

    1. 文件格式: ChangeLog+V+版本号 = ChangeLogVxxx.js

    2. 例子

      export class ChangeLogV1 extends ChangeLog {
        getSqlList() {
          let sqlList = [
            SQL_CREATE_TABLE_JOB,
            SQL_CREATE_TABLE_JOB_BROWSE_HISTORY,
          ];
          return sqlList;
        }
      }
      
  2. ChangeLog 执行规则

    1. 将需要执行的 ChangeLog 存放到列表中
    2. 开启事务,以确保 ChangeLog 的执行的原子性
    3. 通过计算 ChangeLog 列表的总数与当前数据库版本号(版本号为上一次执行 ChangLog 的数量)进行对比计算,来获取当前需要执行的 ChangeLog
    4. 执行完成后,更新数据库版本号(版本号即为已执行 ChangeLog 的数量)

备份

Important

当前使用 pglite dump 备份大量数据的数据库会出错

TODO

内部任务系统

---
title: Task System
---
sequenceDiagram
  participant Service
  participant Database
  participant GitHubApi
  participant GitService

  Note right of Service: 计算和保存下载和上传任务
  Service ->> Database: 获取数据同步配置
  Database -->> Service: 返回数据同步配置
  opt if 开启私有数据同步
    Service ->> Database: 获取用户信息
    Database -->> Service: 返回用户信息
    Service ->> Service: 获取私有数据上传任务类型
    opt if 需要同步私有的数据
      Service ->> Service: 获取私有数据仓库名
      Service ->> GitHubApi: 尝试根据用户名和仓库名创建私有数据仓库
      loop 私有数据上传任务类型
        rect rgb(191, 223, 255)
          Note right of Service: 计算并保存上传任务
          Service ->> Database: 根据用户名,仓库名获取指定任务类型最近上传任务截至时间
          Database -->> Service: 返回最近上传任务截至时间
          opt if 最近上传任务截至时间不为今天
            Service ->> GitService: 根据用户名,仓库名和获取指定任务类型最近上传文件时间
            GitService -->> Service: 返回最近上传文件时间
            Service ->> Service: 在最近上传任务截至时间和最近上传文件时间取最小值,作为任务开始时间
            Service ->> Database: 根据用户名,仓库名,任务类型,任务开始时间,任务结束时间(今天)保存任务记录
          end
        end
      end
      Service ->> Service: 获取私有数据下载任务类型
      Service ->> Service: 将私有数据下载任务保存到下载列表
    end
  end
  alt if 是否开启公开数据同步
    Service ->> Database: 获取用户信息
    Database -->> Service: 返回用户信息
    Service ->> Service: 获取公开数据上传任务类型
    opt if 有需要同步公开的数据
      Service ->> Service: 获取公开数据仓库名
      Service ->> GitHubApi: 尝试根据用户名和仓库名创建公开数据仓库
      loop 公开数据上传任务类型
        rect rgb(191, 223, 255)
          Note right of Service: 计算并保存上传任务
        end
      end
      Service ->> Service: 获取公开数据下载任务类型
      Service ->> Service: 将公开数据下载任务保存到下载列表
    end
  end
  Service ->> Database: 获取数据共享伙伴列表
  Database -->> Service: 返回数据共享伙伴列表
  Service ->> Service: 将数据共享伙伴数据的下载任务保存到下载列表
  loop 下载列表
    Service ->> Service: 根据下载列表获取伙伴信息
    Service ->> Service: 根据下载列表获取下载任务类型
    loop 下载任务类型
      rect rgb(191, 255, 223)
        Note right of Service: 计算并保存下载任务
        Service ->> GitService: 根据任务类型或文件名查询日期目录列表
        GitService -->> Service: 返回日期目录列表
        Service ->> Service: 根据下载历史文件保留天数和当前日期过滤和升序日期列表,获得仓库文件日期列表
        Service ->> Service: 根据仓库文件日期列表,获取开始时间(列表中最小时间)和结束时间(列表中最大时间)
        Service ->> Database: 根据开始时间和结束时间查询指定类型的下载任务列表
        Database -->> Service: 返回下载任务列表
        Service ->> Service: 根据仓库文件日期列表和下载任务列表的日期计算得出本地缺失日期列表
        Service ->> Database: 根据本地缺失日期列表,新增指定类型的下载任务
      end
    end
  end
  Service ->> Database: 获取数据源元数据自动更新列表
  Database -->> Service: 返回数据源元数据自动更新列表
  loop 数据源元数据自动更新列表
      rect rgb(255, 223,191)
        Note right of Service: 计算并保存元数据下载任务
        Service ->> Database: 根据任务类型编号获取最近下载任务列表
        Database -->> Service: 返回下载任务列表
        opt if 任务列表中含有非今天未完成的任务
          Service ->> Database: 将未完成的任务状态设置为取消
        end
        opt if 任务列表中没有今天未完成的任务
          Service ->> Database: 新增数据源元数据下载任务
        end
      end
  end
  Note right of Service: 执行下载和上传任务
  Service ->> Database: 查询需要执行的任务
  Database -->> Service: 返回需要执行的任务
  Service ->> Service: 执行查询到的需要执行的任务
  Note right of Service: 执行定时任务
  Service ->> Database: 查询已合并但未删除的文件
  Database -->> Service: 返回已合并但未删除的文件
  Service ->> Service: 根据历史文件保留数量和最大历史文件保留容量计算需要删除的文件列表
  Service ->> Database: 根据需要删除的文件列表删除文件

任务状态图

---
title: Task state
---
stateDiagram-v2
    [*] --> READY
    READY --> RUNNING
    READY --> CANCEL
    RUNNING --> FINISHED
    RUNNING --> FINISHED_BUT_ERROR
    RUNNING --> ERROR
    RUNNING --> CANCEL
    FINISHED --> [*]
    FINISHED_BUT_ERROR --> [*]
    ERROR --> [*]
    CANCEL --> [*]

数据关系图

---
title: Task ER
---
erDiagram
    task ||--|| task_data_upload: owns
    task {
      string(255) id PK "编号"
      string(255) type "任务类型"
      string(255) data_id FK "任务详情编号"
      string(255) status "任务状态"
      string error_reason "错误原因"
      int cost_time "耗时"
      int retry_count "重试次数"
      datetime create_datetime "创建时间"
      datetime update_datetime "更新时间"
    }
    task_data_upload {
      string(255) id PK "编号"
      string(255) type "任务类型"
      string(255) username "用户名"
      string(255) reponame "仓库名"
      datetime start_datetime "开始时间"
      datetime end_datetime "结束时间"
      int data_count "数据量"
      int data_page_num "页数"
      int data_page_size "页大小"
      datetime create_datetime "创建时间"
      datetime update_datetime "更新时间"
    }
    task ||--|| task_data_download: owns
    task_data_download {
      string(255) id PK "编号"
      string(255) type "任务类型"
      string(255) username "用户名"
      string(255) reponame "仓库名"
      datetime datetime "日期"
      int seq "序号"
      datetime create_datetime "创建时间"
      datetime update_datetime "更新时间"
      string(255) type_id "类型标识,用于识别特定文件"
      json config "配置"
      string(255) data_id FK "文件编号"
    }
    task ||--|| task_data_merge: owns
    task_data_merge {
      string(255) id PK "编号"
      string(255) type "任务类型"
      string(255) username "用户名"
      string(255) reponame "仓库名"
      datetime datetime "日期"
      string(255) data_id FK "文件编号"
      int data_count "数据量"
      int data_page_num "页数"
      int data_page_size "页大小"
      datetime create_datetime "创建时间"
      datetime update_datetime "更新时间"
      string(255) type_id "类型标识,用于识别特定文件"
      json config "配置"
    }
    task_data_merge |o--|| file: has
    file {
      string(255) id PK "编号"
      string(255) name "文件名"
      string(255) sha "散列值"
      string(255) encoding "编码"
      string content "文件内容"
      int size "文件尺寸"
      string type "文件类型"
      is_delete boolean "是否刪除"
      datetime create_datetime "创建时间"
      datetime update_datetime "更新时间"
    }

任务类型

export const TASK_TYPE_JOB_DATA_UPLOAD = 'JOB_DATA_UPLOAD';
export const TASK_TYPE_JOB_DATA_DOWNLOAD = 'JOB_DATA_DOWNLOAD';
export const TASK_TYPE_JOB_DATA_MERGE = 'JOB_DATA_MERGE';

export const TASK_TYPE_COMPANY_DATA_UPLOAD = 'COMPANY_DATA_UPLOAD';
export const TASK_TYPE_COMPANY_DATA_DOWNLOAD = 'COMPANY_DATA_DOWNLOAD';
export const TASK_TYPE_COMPANY_DATA_MERGE = 'COMPANY_DATA_MERGE';

export const TASK_TYPE_COMPANY_TAG_DATA_UPLOAD = 'COMPANY_TAG_DATA_UPLOAD';
export const TASK_TYPE_COMPANY_TAG_DATA_DOWNLOAD = 'COMPANY_TAG_DATA_DOWNLOAD';
export const TASK_TYPE_COMPANY_TAG_DATA_MERGE = 'COMPANY_TAG_DATA_MERGE';

export const TASK_TYPE_JOB_TAG_DATA_UPLOAD = 'JOB_TAG_DATA_UPLOAD';
export const TASK_TYPE_JOB_TAG_DATA_DOWNLOAD = 'JOB_TAG_DATA_DOWNLOAD';
export const TASK_TYPE_JOB_TAG_DATA_MERGE = 'JOB_TAG_DATA_MERGE';

export const TASK_TYPE_JOB_PUBLIC_DATA_UPLOAD = 'JOB_PUBLIC_DATA_UPLOAD';
export const TASK_TYPE_JOB_PUBLIC_DATA_DOWNLOAD = 'JOB_PUBLIC_DATA_DOWNLOAD';
export const TASK_TYPE_JOB_PUBLIC_DATA_MERGE = 'JOB_PUBLIC_DATA_MERGE';

export const TASK_TYPE_METADATA_DATA_DOWNLOAD = 'METADATA_DATA_DOWNLOAD';
export const TASK_TYPE_METADATA_DATA_MERGE = 'METADATA_DATA_MERGE';

export const TASK_TYPE_COMPANY_COMMENT_DATA_DOWNLOAD =
  'COMPANY_COMMENT_DATA_DOWNLOAD';
export const TASK_TYPE_COMPANY_COMMENT_DATA_MERGE =
  'COMPANY_COMMENT_DATA_MERGE';

数据同步

数据仓库文件结构

  ├── YYYY                              //年份,格式YYYY
  │   └── MM-DD                         //月日,格式MM-DD
  │       ├─── company.zip
  │       ├─── job.zip
  │       ├─── job_tag.zip
  │       ├─── company_tag.zip

Git 读取数据仓库文件结构

https://git-scm.com/docs/gitprotocol-v2

sequenceDiagram
    participant GitService
    participant GitServer

    GitService ->> GitServer: 发送ls-refs请求
    GitServer -->> GitService: 返回refs
    GitService ->> GitService: 根据refs获取commitHash
    GitService ->> GitServer: 根据commitHash获取treesIdx
    GitServer -->> GitService: 返回treesIdx
    GitService ->> GitServer: 根据treesIdx遍历仓库目录
    GitServer -->> GitService: 返回仓库目录
    GitService -->> GitService: 根据仓库目录过滤符合日期目录的指定文件的文件列表

数据合并文件

  1. 文件格式: excel

  2. 文件内容格式

    字段名字段类型备注
    编号string记录唯一标识
    xxxxxx
    __VERSION_int版本号,如果该字段不存在,则会被认为第 0 版
  3. 版本识别规则

    1. 通过版本号字段读取文件的文件字段列表
    2. 根据文件字段列表识别文件是否合法

数据递增规则

  1. 以记录创建时间为数据增量规则

  2. 各数据格式增量更新规则字段

    1. 职位

      1. createDatetime
      2. updateDatetime
      3. isFullCompanyName
    2. 职位公开数据

      1. createDatetime
    3. 职位快照

      1. updateDatetime
    4. 职位标签

      未正确实现覆盖更新

      1. updateDatetime
    5. 公司

      1. sourceRefreshDatetime
    6. 公司标签

      未正确实现覆盖更新

      1. updateDatetime
    7. 公司评论

      1. createDatetime

各数据类型版本

职位

字段类型生效版本
职位自编号string0
发布平台string0
职位访问地址string0
职位string0
公司string0
公司是否为全称bool0
地区string0
地址string0
经度number0
纬度number0
职位描述string0
学历string0
所需经验string0
技能string1
福利string1
最低薪资number0
最高薪资number0
首次发布时间datetime0
招聘人string0
招聘公司string0
招聘者职位string0
首次扫描日期datetime0
记录更新日期datetime0

职位标签

字段类型生效版本
职位编号string0
标签string0
记录更新日期datetime1

职位公开数据

字段类型生效版本
职位自编号string0
首次扫描日期datetime0
记录更新日期datetime0

职位快照

字段类型生效版本
编号string0
职位编号string0
职位链接string0
内容string0
招聘平台string0
创建日期datetime0
更新日期datetime0

公司

字段类型生效版本
公司string0
公司描述string0
成立时间string0
经营状态string0
法人string0
统一社会信用代码string0
官网string0
社保人数number0
自身风险数number0
关联风险数number0
地址string0
经营范围string0
纳税人识别号string0
所属行业string0
工商注册号string0
经度number0
纬度number0
注册资本string2
注册资本货币string2
数据来源地址string0
数据来源平台string0
数据来源记录编号string0
数据来源更新时间datetime0
记录创建日期datetime1
记录更新日期datetime1

公司标签

字段类型生效版本
公司string0
标签string0
记录更新日期datetime1

公司评论

字段类型生效版本
公司string0
评论string0
情感string1
数据集string1
创建日期datetime1
更新日期datetime1

数据导入与导出

Important

数据导入逻辑与数据同步逻辑一样

  1. 数据拆分导出

Oauth

Github Oauth2 流程

sequenceDiagram
  participant ContentScript
  participant Background
  participant GithubWebsite
  participant GithubServer

  ContentScript ->> Background: authOauth2Login
  activate GithubWebsite
  Background ->> GithubWebsite: chrome.tabs.create
  Note over GithubWebsite: https://github.com/login/oauth/authorize?client_id=
  GithubWebsite ->> GithubWebsite: github login
  GithubWebsite ->> GithubWebsite: redirect to callback url
  Note over GithubWebsite: https://github.com/lastsunday/job-hunting-github-app/blob/main/INSTALL?code=
  GithubWebsite -->> Background: notify url with code
  Background ->> Background: chrome.tabs.onUpdated
  Background ->> GithubServer: http request access_token
  Note over GithubWebsite: https://github.com/login/oauth/access_token?client_id=&client_secret=&code=
  GithubServer -->> Background: return oauth info
  Background ->> GithubWebsite: chrome.tabs.remove
  deactivate GithubWebsite

BBS 系统

  1. 采用 Github Issues 作为服务端

  2. 地区选择实现原理

    1. 利用标题作为搜索字段

    2. targetId = sha256 地区名: sha256(省)-sha256(市)-sha256(区)

         search(query:"${targetId} in:title sort:created-desc is:issue is:open repo:${GITHUB_APP_REPO}", type: ISSUE, first: ${first ?? null}, after: ${after ? "\"" + after + "\"" : null},last:${last ?? null},before:${before ? "\"" + before + "\"" : null})
      
  3. 使用的 Github API

    1. HQL 查询(可查询 Issues): https://api.github.com/graphql
    2. 新增 Issues: POST https://api.github.com/repos/lastsunday/job-hunting-github-app/issues
    3. 查询 Issues Comment: GET https://api.github.com/repos/lastsunday/job-hunting-github-app/issues/${issueNumber}/comments?per_page=${pageSize}&page=${pageNum}
    4. 新增 Issues Comment: POST https://api.github.com/repos/lastsunday/job-hunting-github-app/issues/${issueNumber}/comments

自动化

  1. 采用 puppeteer 类库在 debug 模式下运行
  2. 当前用于自动浏览职位页面

LLM

  1. 外部 LLM

    1. ollama

      https://docs.ollama.com/api/introduction

    2. openai

      https://platform.openai.com/docs/api-reference/chat

    3. siliconflow

      https://docs.siliconflow.com/en/api-reference/chat-completions/chat-completions

  2. 内嵌 LLM

    1. web-llm

      https://github.com/mlc-ai/web-llm

职位分析组件

Important

项目根目录: libs/analysis

编译

pnpm exec nx run analysis:build

文档

pnpm exec nx run analysis:storybook

数据源

数据源仓库说明

  1. 仓库目录结构
├── 2025                              //年份,格式YYYY
│   └── 07-01                         //月日,格式MM-DD
│       └── 深圳避雷公司.zip          //数据文件,格式xxx.zip(包含文件xxx.xlsx)备注:zip文件和xlsx文件的文件名必须相同
├── metadata.json                     //源数据,包含数据源元数据,可用的数据源列表
  1. metadata.json 文件格式
{
  "data": {
    "source": [
      {
        "name": ""    //数据源名称
        "config": {
          "taskTypeList": [
            {
              "name": "",                                 //名称
              "type": "COMPANY_COMMENT_DATA_DOWNLOAD",    //固定值,代表公司评论数据类型
              "emotion": "NEGATIVE",                      //情感导向,负面:NEGATIVE,积极:POSITIVE,正常:其他值
              "fileName": "深圳避雷公司",                             //数据文件名
              "description": "",                          //描述
              "retentionDay": 3650                      //数据保留时间,单位:天
            }
          ]
        },
        "repoType": "GITHUB",                         //固定值
        "reponame": "",                               //仓库名
        "username": "",                               //仓库用户名
        "description": ""                             //数据源描述
      }
    ]
  },
  "icon":"",                                          //元数据图标,格式为svg,注意转移字符
  "name": "",                                         //元数据名称
  "type": "GIT_METADATA",                             //固定值,GIT元数据类型
  "config": {
    "config": {
      "url": "",                                      //元数据git仓库地址,以http或https开头的地址
      "filePath": "metadata.json"                     //元数据文件名,一般取metadata.json
    }
  },
  "enable": true,                                   //是否开启
  "description": "",                                  //元数据描述
  "autoUpdateEnable": true                          //是否自动更新元数据
}
  1. 公司评论数据文件格式

    公司评论
    xxxxyyyy

    备注:

    1. 第一行的标题必须保留,插件会以标题来标识字段列的数据
    2. 公司:可以同时填写多家公司,换行隔开
    3. 公司或评论为空的行将被跳过
  2. Q&A

    1. 如何更新数据? 根据仓库目录结构,新建年月日目录,将整理后的数据文件zip 文件上传到新建的年月日目录里即可

    2. 如何快速测试文件格式是否正确 打开插件后台页面 ->系统 ->数据管理 ->公司评论数据导入

      如果能正常导入 xlsx 文件,则代表该文件正常(当前不支持导入 zip 文件)

    3. 如何使用?

      1. 新增元数据:打开插件后台页面 ->数据源 ->元数据 ->从网络导入

        1.从网络导入,填入 metadata.json 的访问地址: https://github.com/用户名/仓库名/raw/refs/heads/main/metadata.json

      2. 添加数据源: 打开插件后台页面 ->数据源 ->列表 ->搜寻数据源 ->选中新增的元数据 ->选中并添加数据源

    4. 数据同步的逻辑是怎样的? 插件会遍历整个仓库的年月日目录,根据 metadata.json 的规则进行数据的增量同步,公司评论信息采取 散列(公司名+评论)作为唯一标识进行去重逻辑。

Commit Message

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

提交说明包含了下面的结构化元素,以向类库使用者表明其意图:

    fix: 类型 为 fix 的提交表示在代码库中修复了一个 bug(这和语义化版本中的 PATCH 相对应)。
    feat: 类型 为 feat 的提交表示在代码库中新增了一个功能(这和语义化版本中的 MINOR 相对应)。
    BREAKING CHANGE: 在脚注中包含 BREAKING CHANGE: 或 <类型>(范围) 后面有一个 ! 的提交,表示引入了破坏性 API 变更(这和语义化版本中的 MAJOR 相对应)。 破坏性变更可以是任意 类型 提交的一部分。
    除 fix: 和 feat: 之外,也可以使用其它提交 类型 ,例如 @commitlint/config-conventional(基于 Angular 约定)中推荐的 build:、chore:、 ci:、docs:、style:、refactor:、perf:、test:,等等。
    build: 用于修改项目构建系统,例如修改依赖库、外部接口或者升级 Node 版本等;
    chore: 用于对非业务性代码进行修改,例如修改构建流程或者工具配置等;
    ci: 用于修改持续集成流程,例如修改 Travis、Jenkins 等工作流配置;
    docs: 用于修改文档,例如修改 README 文件、API 文档等;
    style: 用于修改代码的样式,例如调整缩进、空格、空行等;
    refactor: 用于重构代码,例如修改代码结构、变量名、函数名等但不修改功能逻辑;
    perf: 用于优化性能,例如提升代码的性能、减少内存占用等;
    test: 用于修改测试用例,例如添加、删除、修改代码的测试用例等。
    脚注中除了 BREAKING CHANGE: <description> ,其它条目应该采用类似 git trailer format 这样的惯例。
    其它提交类型在约定式提交规范中并没有强制限制,并且在语义化版本中没有隐式影响(除非它们包含 BREAKING CHANGE)。 可以为提交类型添加一个围在圆括号内的范围,以为其提供额外的上下文信息。例如 feat(parser): adds ability to parse arrays.。
    特别地:
        bump: v1.1.0,代表发布版本

Version

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

Reference

auto_explain

   db = new PGlite(`opfs-ahp://${JOB_DB_PATH}`, {
       extensions: { auto_explain },
       debug:1,
   });
 await db.exec(`
   LOAD 'auto_explain';
   SET auto_explain.log_min_duration = '0';
   SET auto_explain.log_analyze = 'true';
   `);

打开offscreen的console,可看到详细的分析日志

Transaction

Chrome Extension下的持久层事务问题(Transaction)

  1. 当前采用PGLite,其事务的调用采用异步调用下的在回调函数里进行,如
await pg.transaction(async (tx) => {
  await tx.query(
    'INSERT INTO test (name) VALUES ('$1');',
    [ 'test' ]
  );
  return await tx.query('SELECT * FROM test;');
});
  1. Chrome Extension的调用链为
ContentScript(或SidePanel) -> Background -> OffScreen -> WebWorker

而持久层(即PGLite)是在WebWorker进行调用,而运行逻辑部份是放在Background或ContentScript(或SidePanel)里,这样会出现一个问题,怎样使得逻辑的调用链处在事务的回调函数里?

可能的解决方案

  1. 传递tx对象

伪代码:

let idAndTxMap = new Map();
let idAndTxPromiseResolveReject = new Map()

async function startTransaction(){
    await pg.transaction(async (tx) => {
    let id = genId();
    idAndTxMap.set(id,tx);
    postMessage(message,id);
    return new Promise((resolve,reject)=>{
        idAndTxPromiseResolveReject.set(id,{resolve,reject});
    });
    });
}

async function endTransaction(id){
    idAndTxPromiseResolveReject.get(id).resolve();
}

async function update(param,{transactionId=null}={}){
    let connection = idAndTxMap.get(transactionId)??await getDb();
    connection.query("update....",param);
}

async main(){
    let id = await startTransaction();
    update(param,{transactionId:id})
    await endTransaction(id);
}

  1. 将逻辑都放到WebWorker里

选择的解决方案

  1. 选择将逻辑都放到WebWorker里。

Dump

数据库备份

https://github.com/electric-sql/pglite/tree/main/packages/pglite-tools

https://github.com/electric-sql/pglite/blob/6b60fbc55c2d59eb7642d1d3f560999ec5b4ec40/packages/pglite-tools/README.md

这里有pgDump的工具,但整合后报错(pg_dump failed with exit code 1),不能正常导出

  1. 导出

package.json

"@electric-sql/pglite-tools": "^0.2.2",

wxt.config.ts

copyFileSync(resolve(srcDir, "node_modules", "@electric-sql", "pglite-tools", "dist", "pg_dump.wasm"), resolve(outDir, "assets", "pg_dump.wasm"));

database.js

import { PGlite } from '@electric-sql/pglite'
import { pgDump } from '@electric-sql/pglite-tools/pg_dump'

const pg = await PGlite.create()

// Create a table and insert some data
await pg.exec(`
  CREATE TABLE test (
    id SERIAL PRIMARY KEY,
    name TEXT
  );
`)
await pg.exec(`
  INSERT INTO test (name) VALUES ('test');
`)

// Dump the database to a file
const dump = await pgDump({ pg })
  1. 导入

Data Share Plan 数据共享计划

文档版本:2024-10-10

背景

招聘网站展示给应聘者的岗位存在某些问题

  1. 职位并不是最新的,有可能是挂职几个月的,一页展示的只有大概三分之一是最近的
  2. 部分职位因为使用某种手段,使其展示的优先级往上靠
  3. 某些招聘网站没有提供按职位发布时间进行排序的功能
  4. 招聘网站提供的职位搜索条件较少,一般只通过职位关键字来进行搜索

基于上述的原因,实现数据共享计划并结合 JobHutting 内置的职位偏好功能是部分痛点的解决方案

数据字段

职位数据字段

  职位自编号
  发布平台
  职位访问地址
  职位
  公司
  公司是否为全称
  地区
  地址
  经度
  纬度
  职位描述
  学历
  所需经验
  最低薪资
  最高薪资
  首次发布时间
  招聘人
  招聘公司
  招聘者职位
  首次扫描日期
  记录更新日期

公司数据字段

  公司
  公司描述
  成立时间
  经营状态
  法人
  统一社会信用代码
  官网
  社保人数
  自身风险数
  关联风险数
  地址
  经营范围
  纳税人识别号
  所属行业
  工商注册号
  经度
  纬度
  数据来源地址
  数据来源平台
  数据来源记录编号
  数据来源更新时间

公司标签数据字段

  公司
  标签

数据流向

官方数据流(数据上传)

    招聘网站 -> JobHunting Extension -> Git(个人GitHub仓库)

共享数据流(数据下载)

    共享数据仓库列表 -> Git(个人GitHub仓库) -> JobHunting Extension

数据提交流程

最多每天提交一次,插件启动时开启定时检查任务
查询仓库最近一次提交时间
提交的记录的范围条件:记录更新时间 < 今天0点0分 和记录更新时间 >= 最近一次提交时间
    备注:针对公司标签,现在是全量提交
提交的目录
    提交时间(YYYY)
        提交时间(MM-DD)
            job.zip
            company.zip
            company_tag.zip

数据获取和同步流程

1. 查询仓库60天内的记录
2. 下载60天内缺失的数据文件
3. 根据数据文件进行数据同步
4. 针对不同类型数据进行处理
    职位数据
        如果是新数据
            新增记录
        如果是重复数据
            根据创建时间来处理,并且需要处理公司名全称问题
    公司数据
        如果是新数据
            新增记录
        如果是重复数据
            根据数据来源更新时间来处理
    公司标签数据
        如果是新数据
            新增记录
        如果是重复数据
            合并标签

关键组件及行为

相关表

task 任务表
    id 编号
    type 任务类型
    data_id 数据编号
    status 任务状态
    error_reason 异常原因
    cost_time 最近一次任务执行耗时
    retry_count 重试次数
    create_datetime 创建时间
    update_datetime 更新时间
task_data_upload 任务数据表(上传)
    id 编号
    type 任务类型
    username 用户名
    reponame 仓库名
    start_datetime 数据开始时间
    end_datetime 数据结束时间
    data_count 数据总量
    create_datetime 创建时间
    update_datetime 更新时间
task_data_download 任务数据表(下载)
    id 编号
    type 任务类型
    username 用户名
    reponame 仓库名
    datetime 文件日期
    create_datetime 创建时间
    update_datetime 更新时间
file 文件表
    id 编号
    name 文件名
    sha 散列值
    encoding 文件内容编码,
    content 文件内容,
    size 文件尺寸,
    type 类型(当前只有file)
    create_datetime 创建时间
    update_datetime 更新时间
task_data_merge 任务数据表(数据合并)
    id 编号
    type 任务类型
    username 用户名
    reponame 仓库名
    datetime 文件日期
    data_id 文件编号
    data_count 数据总量
    create_datetime 创建时间
    update_datetime 更新时间
data_share_partner 数据共享伙伴信息表
    id 编号
    username 用户名
    reponame 仓库名
    repo_type 仓库类型(当前只有GITHUB)
    create_datetime 创建时间
    update_datetime 更新时间

附表:
任务类型
    职位数据上传:TASK_TYPE_JOB_DATA_UPLOAD
    公司数据上传:TASK_TYPE_COMPANY_DATA_UPLOAD
    公司标签数据上传:TASK_TYPE_COMPANY_TAG_DATA_UPLOAD
    职位数据下载:TASK_TYPE_JOB_DATA_DOWNLOAD
    公司数据下载:TASK_TYPE_COMPANY_DATA_DOWNLOAD
    公司标签数据下载:TASK_TYPE_COMPANY_TAG_DATA_DOWNLOAD
    职位数据合并:TASK_TYPE_JOB_DATA_MERGE
    公司数据合并:TASK_TYPE_COMPANY_DATA_MERGE
    公司标签数据合并:TASK_TYPE_COMPANY_TAG_DATA_MERGE
状态
    准备:TASK_STATUS_READY
    运行中:TASK_STATUS_RUNNING
    完成:TASK_STATUS_FINISHED
    异常完成:TASK_STATUS_FINISHED_BUT_ERROR
    错误:TASK_STATUS_ERROR
    取消:TASK_STATUS_CANCEL

数据存储目录结构

提交时间(YYYY)
    提交时间(MM-DD)
        job.zip
        company.zip
        company_tag.zip

GitHub

    仓库的建立
        所需权限:"Administration" repository permissions (write)
        https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-for-the-authenticated-user
            /user/repos

    仓库目录的提交&数据文件的提交
        所需权限:"Contents" repository permissions (write)
        https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#create-or-update-file-contents
            /repos/{owner}/{repo}/contents/{path}

    仓库目录的查询&仓库文件的下载
        所需权限:无
        特别事项:This API has an upper limit of 1,000 files for a directory. If you need to retrieve more files,
        https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
            /repos/{owner}/{repo}/contents/{path}

FAQ

1. 报错 No more file handles available in the pool

如果在 Linux 下,请使用命令 ulimit -n 检查 soft file descriptor 的值,一般默认为 1024 或 2048,请设定一个较高的值如 9001

免责声明

1. 项目目的与性质

本项目(以下简称“本项目”)是作为一个技术研究与学习工具而创建的,旨在探索和学习网络数据采集技术。本项目专注于招聘平台的数据爬取与分析技术研究,旨在提供给学习者和研究者作为技术交流之用。

2. 法律合规性声明

本项目开发者(以下简称“开发者”)郑重提醒用户在下载、安装和使用本项目时,严格遵守中华人民共和国相关法律法规,包括但不限于《中华人民共和国网络安全法》、《中华人民共和国反间谍法》等所有适用的国家法律和政策。用户应自行承担一切因使用本项目而可能引起的法律责任。

3. 使用目的限制

本项目严禁用于任何非法目的或非学习、非研究的商业行为。本项目不得用于任何形式的非法侵入他人计算机系统,不得用于任何侵犯他人知识产权或其他合法权益的行为。用户应保证其使用本项目的目的纯属个人学习和技术研究,不得用于任何形式的非法活动。

4. 免责声明

开发者已尽最大努力确保本项目的正当性及安全性,但不对用户使用本项目可能引起的任何形式的直接或间接损失承担责任。包括但不限于由于使用本项目而导致的任何数据丢失、设备损坏、法律诉讼等。

5. 知识产权声明

本项目的知识产权归开发者所有。本项目受到著作权法和国际著作权条约以及其他知识产权法律和条约的保护。用户在遵守本声明及相关法律法规的前提下,可以下载和使用本项目。

6. 最终解释权

关于本项目的最终解释权归开发者所有。开发者保留随时更改或更新本免责声明的权利,恕不另行通知。