[在招] 核心业务说明

在招的核心业务提供一套开源、轻量的软件解决方案, 帮助平台型企业或组织:

  1. 管理平台内多企业的岗位招聘信息发布;
  2. 面试进展跟进(简历投递、结果反馈、数据汇总);
  3. 招聘讯息推荐及营销活动管理;

平台型企业大部分处于某个垂直细分领域, 也有可能运营管理多领域的招聘信息.

亦可以作为单个企业的招聘管理系统来部署使用.

概念拆解

领域 Field

招聘平台运营方向的定义, 在大多数场景下与行业同含义, 例如: 新闻出版、人工智能、精密制造等等, 企业可以自行定制管理.

  • 每个领域下会有多个 岗位类型(PositionType) 数据;

企业 Company

平台入驻的企业主体定义, 除了基本信息外, 还会涉及两个关键环节:

  1. 企业申请入驻认证流程(名称冲突和保护政策, 实名认证)
  2. 团队成员及权限管理;

岗位 Position

  • 岗位需要关联某个具体的岗位类型, 这个初始数据由所在领域模块定制;
  • 岗位基础信息管理, 涉及多维度的硬性要求和限制 🚫;
  • 岗位的要求和职责会支持 AI 🤖 智能生成;
  • 职位会涉及状态管理, 上线/下线;

简历 Resume

有新想法, 待规划

营销活动管理

待整理.

账号体系

在招平台会有两大类账号 求职者用户 User 与管理账号 Account , 未来会有两个不同的应用入口:

账号类型应用入口
Userzaizhao.ren
Accountplatform.zaizhao.ren

但是两类账户会由同一个 Auth Service 提供用户验证服务及资源访问控制策略;

用户角色 User

用户可以访问前端应用, 但是无法登录管理后台, 与 Account 账号数据是独立的;

管理账号角色 Account

平台 Platform

系统初始化时自动生成唯一的平台角色账号, 提供重置账号的接口能力;

平台账号等同于超级管理员角色;

企业 Company

平台账号可以对企业账号申请入驻流程进行审核管理;

企业可自行进行发起注销企业流程;

账号 Employer

用户主动在平台注册账号即可以成为普通管理账号(员工👋);

可以申请进行企业账号认证, 审核通过后该账号可以升级为企业角色账号;

群组 Group

平台账号和企业账号均可以创建群组 Group, 邀请其他人加入为群组成员;

群组成员 Member 可以拥有不同的数据权限级别:

群组成员 Member数据权限级别
Admin高级权限
Normal普通权限

Access Policies 资源访问策略

账号类型角色 🎭资源访问控制
AccountPlatformCompanyRead,Edit
-CompanyCompanyRead,Edit
-EmployerCompanyRead
-EmployerCompanyRead
-Member(admin)CompanyRead
-Member(normal)CompanyRead
UserUserCompanyRead

其它

暂无.

快速入门 Gel 数据库

One data model and a unified query-layer, NO JOINS, NO ORM,

Gel 既继承 SQL 数据库的优势: 类型安全、高性能、可靠稳定以及支持事务处理等 (基于 Postgres 开发), 又充分利用面向对象的设计思路, 定义类型对象(包含属性和指向其它类型对象的链接) 来表示数据 (Graph-Relational Database 的核心思想), 解决 SQL 在易用性和深度查询时的性能问题.

  • Strict, strongly-typed Schema
  • Built-in support for schema migrations & branch manage
  • Powerful and clean query language: EdgeQL
  • Auth & AI Solutions
  • Full support for SQL, fully Open Source
  • 单机或集群轻松部署, 高性能和网络连接健壮稳定

对整个应用开发流程的意义:

  1. 简化在数据整理、领域知识梳理以及数据序列化方面的工作, 将逻辑和数据关联都变成数据库模式和查询的一部分, 应用程序的代码可以更专注于业务逻辑和问题领域的具体细节.
  2. Access Policies 作为安全后端提供对象级安全, 将大量的查询过滤及访问控制都放在数据库层, 避免意外的数据泄露.
  3. 内置简单强大的用户验证解决方案, 与数据库访问策略控制可以完全结合起来.
  4. 客户端 SDK, HTTP Query 与 GraphQL 查询支持, 方便集成扩展.
  5. 向量存储及 AI 能力的加持, 数据库扩展系统.

命令行工具 gel

如何安装和使用: https://docs.geldata.com/learn/cli

  1. Instance: 管理数据库实例
# Action: create | list | logs
gel instance create new_instance --version 6.1

# Manage: status | start | stop | restart | destroy
gel instance status -I new_instance

# 在指定实例运行查询 Query
gel query "select 3.14" -I new_instance

gel instance --help # 更多功能
  1. Project: 让本地开发更简单.

Project 可以把本地的项目文件夹和数据库实例关联起来.

gel project init # 初始化项目: link instance
# Generate: gem.toml、dbschema(*.gel & migrations)

# 可以手动指定关联数据库实例, 支持远程实例
gel instance link
gel project unlink # 取消关联, -D 参数可以同时删除实例

gel project info    # 查看项目信息
gel project upgrade --help # 升级实例版本

# 进入数据库查询 REPL
gel     # 等同于 gel -I {instance_name}
  1. 数据库可视化管理: gel ui

📄 数据模型 Schema, Migration & Branches

数据模型定义是业务系统的基石, Gel 提供强类型、高级的 SDL(Schema definition language)语言, 以在招的业务系统为例:

Login to Gel WebUI for dev server

基础概念

数据都用 类型对象(type Object) 来定义:

# 可以省略的关键字: property、optional、single、link
abstract property email_prop {      # Abstract property
    readonly := true;                 # 只读属性,不允许修改
}

type Player {
    property required email: str {  # str is a Scalar Type
        extending email_prop;
    }; 
    # link 代表类型对象之间的关系, 是有方向的, 1-n 或 m-n 
    # Player(source) -> multi Game(target)
    optional multi link games: Game {
        # ensures a one-to-many relationship
        constraint exclusive;       # 约束条件: 唯一
        on source delete allow;     # deletion policy 
    };
}

type Game {
    required name: str {            # required 指必填属性
        default := 'Unknown';       # 设置默认值
    };
    index on (.name) {              # 设置索引
        annotation description := 'Indexing all users by name.';
    };                  
}

gel describe object [options] namegel describe schema 命令可以辅助我们在命令行查看 schema 定义.

Abstract 抽象与 Inheritance 继承

# declare reusable, user-defined constraint types.
abstract constraint in_range(min: anyreal, max: anyreal) {
  errmessage :=
    'Value must be in range [{min}, {max}].';
  using (min <= __subject__ and __subject__ < max);
}
abstract link link_with_strength {
  strength: int64 {
    constraint in_range(0, 100);
  };
  index on (__subject__@strength);
}
abstract type Person {
  name: str;
  multi friends: Person {
    extending link_with_strength;
  };
}

abstract annotation admin_note; # 自定义注释
type Student extending Person {
  overloaded name: str {    # overloaded property
    constraint exclusive;
  }
  overloaded multi friends: Student;
  annotation admin_note := 'system-critical';
}

Triggers 触发器Mutation rewrites

用审计日志(Audit Log)的例子来学习使用触发器:

type Log {
    action: str;
    timestamp: datetime {
        default := datetime_current();
    }
    target_name: str;
    change: str;
}

type Person {
    required name: str;
    # also for insert or delete
    trigger log_update after update for each
    when (<json>__old__ {**} != <json>__new__ {**})
    do (insert Log {
            action := 'update',
            target_name := __new__.name,
            change := __old__.name ++ '->' ++ __new__.name
        });
}
  • Triggers may also be used for validation.

操作重写可以方便我们在数据插入和更新时进行拦截, 对数据进行修改;

type Post {
  required title: str;
  required body: str;
  modified: datetime {
    rewrite insert, update using (datetime_of_statement())
  }
}

Access Policies 安全策略: Object-level Security

Access policies can greatly simplify your backend code, centralizing access control logic in a single place.

global current_user: uuid;
type User {
  required email: str { constraint exclusive; }
}
type Post {
  required title: str;
  required author: User;

  access policy author_has_full_access
    allow all
    using (global current_user ?= .author.id) {
      errmessage := "User does not have full access";
    }
}
# Policy types: all | select | insert | update | delete | update read | update write

工具能力: AliasesGlobalsFunctionsAnnotations

# Alias
alias digits := {0,1,2,3,4,5,6,7,8,9};
alias Account := User;

# Globals
required global now := datetime_of_transaction();
global current_user_id: uuid;
global current_user := (
  select User filter .id = global current_user_id
);

# Functions
function add_user(name: str) -> User
  using (
    insert User {
      name := name,
      joined_at := std::datetime_current(),
    }
  );

# Annotations 
type Status {
  annotation title := 'Activity status';
  annotation description := 'All possible user activities';

  required name: str {
    constraint exclusive
  }
}

数据库扩展 Extensions

gel extension list

Migrations

可以记录数据库 Schema 的变更历史, 随时回滚, 并将模型 SDL 自动转换为数据库可以理解的 DDL(data definition language) 序列, 方便在开发过程中轻松的改进数据模型.

👉 Guide to Gel migrations

gel migration create    # 生成迁移记录
gel migrate             # 应用迁移到数据库

Branch

代码仓库分支的概念同理, 可以使用 gel branch 命令方便地创建、删除和切换分支, 简化开发协作流程.

👉 Branches references

Module

可以理解为一个命名空间, 复杂项目可以用 module 更好的划分 Schema 定义.

👉 Module references

查询语言: EdgeQL

EdgeQL 是比 SQL 强大的下一代查询语言, 设计原则:

  • Compatible with modern languages;
  • Strongly typed;
  • Designed for programmers;
  • Easy deep querying, NO JOINS;
  • Composable;

简单的查询示例:

# insert, select, filtering, ordering, pagination
with new_movie := (insert Movie { title := 'Ne Zha', release_year := 2021 })
select Movie {
  id, title, release_year, actors,
  title_upper := str_upper(.title),
  cast_size := count(.actors)
} 
filter .release_year > 2020 and .title ilike "ne zha%"
order by .title
offset 0 limit 10;

# update or delete action
update Movie
filter .title = "Doctor Strange 2"
set {
  actors += (select Person filter .name = "Rachel McAdams")
};

# group 语句
group Movie { title, actors: { name }}
by .release_year;

Everything is a set

All values in EdgeQL are actually sets 集合: a collection of values of a given type. All elements of a set must have the same type.

select distinct {'a', 'a', 'b', 'c'};   # 集合去重
select 'd' in {'a', 'b', 'c'};          # 是否存在于一个集合
select {1, 2, 3} union {3, 4};          # 合并集合
select {1, 2, 3} intersect {3, 4};      # 集合交集
select <str>{} ?? 'default';            # 集合为空时取默认值

事务处理 Atomic Transactions

https://docs.geldata.com/reference/edgeql/transactions

client.transaction(async tx => {
  await tx.execute(`update BankCustomer
    filter .name = 'Customer1'
    set { bank_balance := .bank_balance -10 }`);
  await tx.execute(`update BankCustomer
    filter .name = 'Customer2'
    set { bank_balance := .bank_balance +10 }`);
});

HTTP & Client Libraries

EdgeQL over HTTP

启用扩展, 记得创建并应用迁移:

# dbschema/default.gel
using extension edgeql_http;

# terminal
curl -G https://<cloud-instance-host>:<cloud-instance-port>/branch/main/edgeql \
 -H "Authorization: Bearer <secret-key>" \
 -H "X-EdgeDB-User: admin" \
 --data-urlencode "query=select Person {*};"

 # Support POST & GET Methods, fields:
 - query
 - variables
 - globals

Error Code at Response

GraphQL'

GraphQL Basic

启用扩展, 记得创建并应用迁移:

# dbschema/default.gel
using extension graphql;

# terminal
curl \
  -H "Content-Type: application/json" \
  -X POST http://localhost:10787/branch/main/graphql \
  -d '{ "query": "query getMovie($title: String!) { Movie(filter: {title:{eq: $title}}) { id title }}", "variables": { "title": "The Batman" }, "globals": {"default::current_user": "04e52807-6835-4eaa-999b-952804ab40a5"}}'

Explore the GraphQL schema and write queries: https://github.com/graphql/graphiql

  • Endpoint: http:/localhost:port/branch/main/graphql/explore

Client Libraries

不同语言的客户端 SDK 基本都会实现一个 Client 客户端类:

  • Manages a pool of physical connections to your Gel instance.
  • Resolving connections: GEL_DSN
  • Executing queries, Under the hood, this query is executed using Gel's efficient binary protocol.

Client Libraries: https://docs.geldata.com/reference/clients

Connection parameters: https://docs.geldata.com/reference/reference/connection#ref-reference-connection

其它

  • 快速学习: https://docs.geldata.com/learn/edgeql
  • Reference: https://docs.geldata.com/reference/edgeql
  • 查询分析: https://docs.geldata.com/reference/cli/gel_analyze#ref-cli-gel-analyze

内置的 Auth 解决方案 🔐

大多数应用都需某种形式的用户认证, 通常的办法是自己造轮子重复开发, 或选择 Auth0 或者 Clerk 这样的托管服务.

直接使用 Gel 内置、易于集成的 Auth 解决方案也是一个选择:

  • 利用内置的身份验证 UI 可以快速搭建应用, 可随时替换为定制的 UI;
  • 深度集成: 部署 Gel 意味着您同时部署了 Auth(启用扩展即可), 无需复杂的配置环节;
  • access policies for role-based access control
  • layer multiple authentication factors onto a single unified User type
  • full control over how your authentication data is stored and managed.
  • 多种用户验证方式支持(可自行扩展)
  • 相对于托管服务按照活跃用户或者验证次数收费, 成本更低;

using extension auth;

Custom Authentication API:

  • Login in: POST /authenticate
    • body(json): challenge | email | password | provider | redirect_to | redirect_to_on_signup
    • response: { code }
  • Register user: POST /register
    • body(json): challenge | email | password | provider | verify_url
    • response: { code, identity_id }
  • Get auth token: GET /token
    • params: code | verifier
    • response: { auth_token, id_token, provider_token }
  • Verify Email Link: POST /verify
    • body(json): verification_token | provider
    • response: { code }
  • Send Reset-Password Email: POST /send-reset-email
    • body(json): email | provider | reset_url | challenge
    • response: { email_sent }
  • Reset Password: POST /reset-password
    • body(json): reset_token | password | provider
    • response: { code }

Magic Link Auth:

  • Register user: POST /magic-link/register
    • body(json): challenge | email | provider | callback_url | redirect_on_failure
  • Sign In with email: POST /magic-link/email
    • body(json): challenge | email | provider | callback_url
  • Get auth token

WebAuthn:

  • Get the options for registering: GET /webauthn/register/options
    • params: email
  • GET /webauthn/authenticate/options
    • params: email
  • POST /webauthn/register
    • body(json): provider | email | credentials | verify_url | user_handle | challenge
  • POST /webauthn/authenticate
    • body(json): provider | email | assertion | challenge
    • response: { code }

用户注册成功后与用户进行绑定(1-1 or 1-m):

type User {
    required email: str;
    name: str;

    required identity: ext::auth::Identity {
        constraint exclusive;
    };
}
...

# After POST /register response
if ("identity_id" in registerJson) {
  await client.query(`
    with
      identity := <ext::auth::Identity><uuid>$identity_id,
      emailFactor := (
        select ext::auth::EmailFactor filter .identity = identity
      ),
    insert User {
      email := emailFactor.email,
      identity := identity
    };
  `, { identity_id: registerJson.identity_id });
} 

如何增加身份验证方式? 比如验证码或者扫码登录.

相关阅读:

  • OAuth 2 Explained: https://www.youtube.com/watch?v=ZV5yTm4pT8g
  • How PKCE Works: https://cloudentity.com/developers/basics/oauth-extensions/authorization-code-with-pkce/
  • System Design: https://bytebytego.com/
  • NextJS Template: https://github.com/geldata/nextjs-gel-auth-template
  • Rust Example: https://github.com/dustypomerleau/audit/blob/main/src/auth.rs

AI 集成方案

基于 OpenAI 构建一个 Ask Bot 的 RAG 应用.

  • https://docs.geldata.com/learn/guides/ai/edgeql
  • https://docs.geldata.com/learn/tutorials/ai_fastapi_searchbot
  • https://docs.geldata.com/resources/guides/tutorials/chatgpt_bot
  • https://www.geldata.com/blog/chit-chatting-with-edgedb-docs-via-chatgpt-and-pgvector

Embedding 向量嵌入

  • pgvector: https://docs.geldata.com/reference/stdlib/pgvector

🛠️ 系统管理

🎭 角色管理

# 创建角色, not possible to create non-superuser roles for now.
create superuser role project;
# 无密码登录(通常用于本地数据库开发)
configure instance insert Auth {
    comment := 'passwordless access',
    priority := 1,
    method := (insert Trust),
};

# 设置密码登录
alter role project
    set password := 'super-password';
# 增加一个优先级更高的 Auth 方法
configure instance insert Auth {
    comment := 'password is required',
    priority := 0,
    method := (insert SCRAM),
    user := 'project'
};

# 移除 Auth Method 方法
configure instance reset Auth filter Auth.method is Trust;

gel -I {INSTANCE_NAME} --user project --password

gel-server 及环境变量的数据

gel info

#  a catchall for logs and various caches.
| Cache   │ /root/.cache/edgedb/    
# contains auto-generated credentials          
│ Config  │ /root/.config/edgedb/   
│ Install │ /root/.local/bin/     
# contains the contents of all local Gel instances.          
│ Data    │ /root/.local/share/edgedb/data/
# the home for running processes/daemons. 
│ Service │ /root/.config/systemd/user/  

环境变量

GEL_SERVER_USER
# If set to anything other than the default username admin, the username specified will be created. The user defined here will be the one assigned the password set in GEL_SERVER_PASSWORD or the hash set in GEL_SERVER_PASSWORD_HASH.

GEL_SERVER_SECURITY
# When set to insecure_dev_mode, sets GEL_SERVER_DEFAULT_AUTH_METHOD to Trust, and GEL_SERVER_TLS_CERT_MODE to generate_self_signed (unless an explicit TLS certificate is specified). Finally, if this option is set, the server will accept plaintext HTTP connections.
GEL_SERVER_TLS_CERT_FILE/GEL_SERVER_TLS_KEY_FILE
GEL_SERVER_TLS_CERT_MODE
# require_file | generate_self_signed

More info: https://docs.geldata.com/reference/reference/environment

安全和性能层面的最佳实践