政府端统计分析(企业、岗位、就业分析)数据分析(岗位,招聘会,求职,热门岗位,单位规模/性质及其发布岗位数量)

This commit is contained in:
戈经莹
2026-07-30 21:02:48 +08:00
parent b1d1d71c85
commit 546d8b9285
25 changed files with 2603 additions and 2 deletions

769
docs/cms-statistics-api.md Normal file
View File

@@ -0,0 +1,769 @@
# 统计分析接口文档
本文档面向 PC 管理端前端,覆盖业务统计、招聘会统计、求职统计、热门岗位统计和用人单位统计。
## 1. 通用约定
### 1.1 基础路径
```text
/cms
```
以下接口均为后台管理接口,实际请求时需要按项目现有登录机制携带登录态/Token。
### 1.2 响应包装
接口统一使用项目的 `AjaxResult` 响应包装,前端通常从 `data` 读取业务数据:
```json
{
"code": 200,
"msg": "操作成功",
"data": {}
}
```
### 1.3 统计时间参数
`/cms/statics/*` 统计接口使用以下参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `startDate` | string | 否 | 开始日期,支持 `yyyy-MM-dd``yyyy-MM` |
| `endDate` | string | 否 | 结束日期,支持 `yyyy-MM-dd``yyyy-MM` |
| `startTime` | string | 否 | `startDate` 的兼容参数名 |
| `endTime` | string | 否 | `endDate` 的兼容参数名 |
时间规则:
- 不传开始时间时,默认从当前年度 `1 月 1 日` 开始。
- 不传结束时间时,默认到当前日期结束。
- 传入 `yyyy-MM` 时,开始月份按当月 1 日处理,结束月份按当月最后一天处理。
- 查询时间采用左闭右开区间,结束日期当天的数据会被包含。
- `startDate``endDate` 不能颠倒。
示例:
```http
GET /cms/statics/business?startDate=2026-01-01&endDate=2026-06-30
```
## 2. 业务统计
### 2.1 接口信息
```http
GET /cms/statics/business
```
### 2.2 统计口径
返回按月统计的数据:
- 岗位来源:从 `job` 表统计 `data_source`
- 本地石河子模板导入:`data_source = '5'`
- 本地录入:`data_source = '1'`
- 互联网岗位上传:`data_source = '2'`
- 新增注册人数:从 `app_user` 表按 `create_time` 统计。
岗位和用户均只统计 `del_flag = '0'` 的数据。
### 2.3 返回结构
```json
{
"months": [
"2026-01",
"2026-02",
"2026-03"
],
"jobSourceSeries": [
{
"code": "5",
"name": "本地石河子模板导入",
"data": [12, 18, 21]
},
{
"code": "1",
"name": "本地录入",
"data": [8, 10, 13]
},
{
"code": "2",
"name": "互联网岗位上传",
"data": [35, 42, 50]
}
],
"newRegisterSeries": {
"code": "register",
"name": "新增注册人数",
"data": [20, 31, 46]
}
}
```
### 2.4 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `months` | string[] | 横坐标月份,格式为 `yyyy-MM` |
| `jobSourceSeries` | object[] | 三种岗位来源序列 |
| `jobSourceSeries[].code` | string | 来源编码:`5``1``2` |
| `jobSourceSeries[].name` | string | 来源名称 |
| `jobSourceSeries[].data` | number[] | 与 `months` 按下标一一对应 |
| `newRegisterSeries` | object | 新增注册人数序列 |
| `newRegisterSeries.data` | number[] | 与 `months` 按下标一一对应 |
### 2.5 前端图表建议
- 折线图或柱状图横轴使用 `months`
- `jobSourceSeries` 可以绘制三条岗位来源曲线。
- `newRegisterSeries` 单独绘制新增注册人数曲线,或作为第二个图表的数据源。
## 3. 招聘会统计
### 3.1 接口信息
```http
GET /cms/outdoor-fair/statistics
```
### 3.2 请求参数
| 参数 | 类型 | 必填 | 可选值/说明 |
|---|---|---:|---|
| `granularity` | string | 否 | `day``week``month``quarter``year`,默认 `month` |
| `startTime` | string | 否 | 格式为 `yyyy-MM-dd HH:mm:ss` |
| `endTime` | string | 否 | 格式为 `yyyy-MM-dd HH:mm:ss` |
示例:
```http
GET /cms/outdoor-fair/statistics?granularity=quarter&startTime=2026-01-01%2000:00:00&endTime=2026-12-31%2023:59:59
```
### 3.3 时间规则
- `granularity=day`:按天统计。
- `granularity=week`:按周一作为一周开始统计。
- `granularity=month`:按月统计,默认值。
- `granularity=quarter`按季度统计Q1 为 1-3 月Q2 为 4-6 月Q3 为 7-9 月Q4 为 10-12 月。
- `granularity=year`:按年统计。
- 不传任何时间参数时,累计统计全部招聘会,曲线时间范围根据招聘会实际举办时间的最小值和最大值生成。
- 只传 `endTime` 时,默认向前推 12 个月作为开始范围。
- 只传 `startTime` 时,结束时间默认为当前日期。
### 3.4 返回结构
```json
{
"granularity": "quarter",
"totals": {
"fairCount": 12,
"companyCount": 86,
"jobCount": 430
},
"yoy": {
"fairCount": 12,
"previousFairCount": 9,
"rate": 0.3333333333
},
"mom": {
"fairCount": 12,
"previousFairCount": 10,
"rate": 0.2
},
"series": [
{
"time": "2026-Q1",
"fairCount": 3,
"companyCount": 20,
"jobCount": 105
},
{
"time": "2026-Q2",
"fairCount": 4,
"companyCount": 28,
"jobCount": 140
}
]
}
```
### 3.5 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `granularity` | string | 实际使用的统计粒度 |
| `totals.fairCount` | number | 区间内招聘会数量 |
| `totals.companyCount` | number | 区间内参会单位数量 |
| `totals.jobCount` | number | 区间内招聘会岗位数量 |
| `yoy.fairCount` | number | 当前区间招聘会数量 |
| `yoy.previousFairCount` | number | 去年同期招聘会数量 |
| `yoy.rate` | number/null | 同比变化率,前值为 0 时返回 `null` |
| `mom.fairCount` | number | 当前区间招聘会数量 |
| `mom.previousFairCount` | number | 上一个等长区间招聘会数量 |
| `mom.rate` | number/null | 环比变化率,前值为 0 时返回 `null` |
| `series` | object[] | 各时间桶的曲线数据 |
| `series[].time` | string | 时间桶名称 |
| `series[].fairCount` | number | 当前时间桶招聘会数量 |
| `series[].companyCount` | number | 当前时间桶参会单位数量 |
| `series[].jobCount` | number | 当前时间桶岗位数量 |
时间桶格式:
| 粒度 | `series[].time` 格式 | 示例 |
|---|---|---|
| `day` | `yyyy-MM-dd` | `2026-07-30` |
| `week` | `yyyy-Www` | `2026-W31` |
| `month` | `yyyy-MM` | `2026-07` |
| `quarter` | `yyyy-Qn` | `2026-Q3` |
| `year` | `yyyy` | `2026` |
同比、环比的 `rate` 是小数,不是百分数字符串。前端展示百分比时可乘以 100 后保留两位小数,例如 `0.3333` 展示为 `33.33%`
## 4. 求职统计
### 4.1 接口信息
```http
GET /cms/statics/jobSeeker
```
### 4.2 统计口径
只统计求职者:
```text
app_user.is_company_user = '1'
```
时间按 `app_user.create_time` 过滤。
统计维度:
- 年龄:`app_user.age`,使用 `dict_type='age'` 字典翻译名称。
- 工作经验:`app_user.work_experience`,使用 `dict_type='experience'` 字典翻译名称。
- 专业/求职方向:解析 `app_user.job_title_id`,关联 `job_title` 获取岗位名称。
### 4.3 返回结构
```json
{
"total": 156,
"age": [
{
"code": "18-25",
"label": "18-25岁",
"count": 58
},
{
"code": "26-35",
"label": "26-35岁",
"count": 72
}
],
"workExperience": [
{
"code": "0",
"label": "无经验",
"count": 40
},
{
"code": "1-3",
"label": "1-3年",
"count": 63
}
],
"profession": [
{
"code": "101",
"label": "行政管理",
"count": 25
},
{
"code": "102",
"label": "销售",
"count": 31
}
]
}
```
### 4.4 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `total` | number | 时间范围内求职者总数 |
| `age` | object[] | 年龄分组统计 |
| `workExperience` | object[] | 工作经验分组统计 |
| `profession` | object[] | 专业/期望岗位统计 |
| `*.code` | string | 字典值或岗位 ID |
| `*.label` | string | 前端显示名称 |
| `*.count` | number | 该分组人数 |
求职者选择多个期望岗位时,会分别计入多个专业/求职方向分组;因此 `profession` 各项之和可能大于 `total`
### 4.5 前端图表建议
- `age` 使用饼图或柱状图。
- `workExperience` 使用饼图或柱状图。
- `profession` 使用横向柱状图,岗位较多时建议按数量倒序显示 Top N。
## 5. 热门岗位统计
### 5.1 接口信息
```http
GET /cms/statics/hotJob
```
### 5.2 统计口径
热门岗位名称来自业务字典数据表:
```sql
bussiness_dict_data.dict_type = 'job_hot'
```
使用字典项的 `dict_label` 作为热门岗位名称,再按岗位名称前缀匹配 `job.job_title`
```sql
TRIM(job.job_title) LIKE TRIM(dict_label) || '%'
```
例如字典中配置 `洗碗工`,会匹配:
```text
洗碗工
洗碗工杂工
洗碗工服务员
洗碗工(包吃住)
```
只统计 `job.del_flag = '0'` 的岗位,并按 `job.create_time` 过滤时间范围。
### 5.3 返回结构
```json
{
"items": [
{
"code": "wash_worker",
"name": "洗碗工",
"jobCount": 8
},
{
"code": "sales",
"name": "销售",
"jobCount": 32
}
]
}
```
### 5.4 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `items` | object[] | 热门岗位统计列表 |
| `items[].code` | string | 字典 `dict_value` |
| `items[].name` | string | 字典 `dict_label`,前端图表显示名称 |
| `items[].jobCount` | number | 匹配到的岗位数量 |
字典中已经配置但时间范围内没有匹配岗位的热门岗位也会返回,`jobCount``0`,便于图表保持固定分类顺序。
### 5.5 前端图表建议
```javascript
const categories = data.items.map(item => item.name)
const values = data.items.map(item => item.jobCount)
```
建议使用柱状图或横向柱状图展示热门岗位数量。
## 6. 用人单位统计
### 6.1 接口信息
```http
GET /cms/statics/company
```
### 6.2 统计内容
接口返回两组统计:
- `scale`:按企业规模统计单位数量和发布岗位数量。
- `nature`:按企业性质统计单位数量和发布岗位数量。
单位数量按 `company.create_time` 过滤;发布岗位数量按 `job.create_time` 过滤。这样历史企业在查询时间范围内发布新岗位时,岗位数量仍然可以被统计。
有效单位条件:
```text
company.del_flag = '0'
company.status = 1
company.company_status = '0'
```
有效发布岗位条件:
```text
job.del_flag = '0'
job.is_publish = 1
job.job_status = '0'
job.review_status = '1'
```
### 6.3 企业规模分类
`dict_type='scale'` 的字典值读取企业规模,并按以下规则归并:
| 字典值 | 返回编码 | 返回名称 |
|---|---|---|
| `1` | `1` | 微型 |
| `2` | `2` | 小型 |
| `3` | `3` | 中型 |
| `4、5` | `4_5` | 大型 |
| `6、7` | `6_7` | 超大型 |
例如 `4``5` 的企业都会汇总到 `大型`,对应的 `jobCount` 也会合并统计。
### 6.4 企业性质分类
`dict_type='company_nature'` 且状态正常的字典数据读取企业性质,并使用字典 `dict_value` 匹配:
```text
company.company_nature
```
如果当前字典中只配置了国企,则 `nature` 只返回国企;如果后续新增规上企业等字典项,接口会自动返回新增分类。
### 6.5 返回结构
```json
{
"scale": [
{
"code": "1",
"name": "微型",
"companyCount": 3,
"jobCount": 5
},
{
"code": "2",
"name": "小型",
"companyCount": 20,
"jobCount": 46
},
{
"code": "3",
"name": "中型",
"companyCount": 8,
"jobCount": 31
},
{
"code": "4_5",
"name": "大型",
"companyCount": 4,
"jobCount": 22
},
{
"code": "6_7",
"name": "超大型",
"companyCount": 1,
"jobCount": 12
}
],
"nature": [
{
"code": "1",
"name": "国企",
"companyCount": 5,
"jobCount": 18
}
]
}
```
### 6.6 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `scale` | object[] | 企业规模统计列表 |
| `nature` | object[] | 企业性质统计列表 |
| `*.code` | string | 统计分类编码 |
| `*.name` | string | 前端显示名称 |
| `*.companyCount` | number | 该分类下单位数量 |
| `*.jobCount` | number | 该分类下有效发布岗位数量 |
### 6.7 前端图表建议
可以按 `scale``nature` 各绘制一个双系列柱状图:
```javascript
const categories = data.scale.map(item => item.name)
const companyData = data.scale.map(item => item.companyCount)
const jobData = data.scale.map(item => item.jobCount)
```
系列名称建议:
```text
单位数量
发布岗位数量
```
## 7. 区划企业分析
### 7.1 接口信息
```http
GET /cms/areaAnalysis/company
```
### 7.2 请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `areaCode` | string | 否 | 区划字典值,不传时查询师市整体 |
| `startDate` | string | 否 | 开始日期,格式 `yyyy-MM-dd` |
| `endDate` | string | 否 | 结束日期,格式 `yyyy-MM-dd` |
`areaCode` 使用业务字典 `dict_type='area'``dict_value`,前端不需要直接传 `sys_area.code`。例如区划选择器应使用区域字典接口返回的 `dict_value` 作为请求参数。
### 7.3 统计口径
企业统计条件:
```text
company.del_flag = '0'
company.status = 1
company.company_status = '0'
```
统计内容:
- 单位总数。
- 按行业统计单位数量。
- 按企业规模统计单位数量,名称使用 `scale` 字典翻译。
- 按企业性质统计单位数量,名称使用 `company_nature` 字典翻译。
### 7.4 返回结构
```json
{
"areaCode": "659001000000",
"areaName": "石河子市",
"totalCompanyCount": 120,
"byIndustry": {
"制造业": 35,
"批发和零售业": 28
},
"byScale": {
"小型": 60,
"中型": 42,
"大型": 18
},
"byNature": {
"国企": 12
}
}
```
### 7.5 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `areaCode` | string | 实际统计区域编码 |
| `areaName` | string | 实际统计区域名称 |
| `totalCompanyCount` | number | 有效单位总数 |
| `byIndustry` | object | 行业名称到单位数量的映射 |
| `byScale` | object | 企业规模名称到单位数量的映射 |
| `byNature` | object | 企业性质名称到单位数量的映射 |
对象类型字段的 key 为图表显示名称value 为数量。例如:
```javascript
const categories = Object.keys(data.byScale)
const values = Object.values(data.byScale)
```
## 8. 区划岗位分析
### 8.1 接口信息
```http
GET /cms/areaAnalysis/job
```
### 8.2 请求参数
参数与企业分析相同:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `areaCode` | string | 否 | `area` 字典的 `dict_value`,不传查询师市整体 |
| `startDate` | string | 否 | 格式 `yyyy-MM-dd` |
| `endDate` | string | 否 | 格式 `yyyy-MM-dd` |
### 8.3 统计口径
岗位统计条件:
```text
job.del_flag = '0'
job.is_publish = 1
job.job_status = '0'
job.review_status = '1'
```
统计内容:
- 岗位总数。
- 招聘总人数。
- 急招岗位数量。
- 重点人群岗位数量。
- 按岗位分类统计。
- 按薪资区间统计。
- 按学历要求统计。
- 按岗位类型统计。
岗位数量统计使用有效发布岗位。招聘总人数中,岗位 `vacancies` 为空或小于等于 0 时按 1 人计算。
### 8.4 返回结构
```json
{
"areaCode": "659001000000",
"areaName": "石河子市",
"totalJobCount": 360,
"totalVacancies": 780,
"urgentJobCount": 48,
"keyPopulationJobCount": 22,
"byCategory": {
"销售": 80,
"餐饮服务": 55
},
"bySalary": {
"3k以下": 30,
"3k-5k": 120,
"5k-8k": 150,
"8k-15k": 48,
"15k+": 12
},
"byEducation": {
"不限": 180,
"大专": 110,
"本科": 70
},
"byType": {
"全职": 300,
"兼职": 60
}
}
```
### 8.5 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `areaCode` | string | 实际统计区域编码 |
| `areaName` | string | 实际统计区域名称 |
| `totalJobCount` | number | 有效发布岗位总数 |
| `totalVacancies` | number | 招聘总人数,空值/非正数岗位按 1 人计算 |
| `urgentJobCount` | number | `is_urgent = 1` 的岗位数量 |
| `keyPopulationJobCount` | number | `is_key_populations = '0'` 的岗位数量 |
| `byCategory` | object | 岗位分类到岗位数量的映射 |
| `bySalary` | object | 薪资区间到岗位数量的映射 |
| `byEducation` | object | 学历名称到岗位数量的映射 |
| `byType` | object | 岗位类型名称到岗位数量的映射 |
薪资区间固定为:
```text
3k以下、3k-5k、5k-8k、8k-15k、15k+
```
## 9. 区划就业分析
### 9.1 接口信息
```http
GET /cms/areaAnalysis/employment
```
### 9.2 请求参数
参数与企业分析、岗位分析相同:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `areaCode` | string | 否 | `area` 字典的 `dict_value`,不传查询师市整体 |
| `startDate` | string | 否 | 格式 `yyyy-MM-dd` |
| `endDate` | string | 否 | 格式 `yyyy-MM-dd` |
### 9.3 统计口径
已就业人数统计有效且已录用的求职记录:
```text
job_apply.del_flag = '0'
job_apply.hire = '0'
app_user.del_flag = '0'
app_user.is_company_user = '1'
app_user.status = '0'
```
就业区域根据求职者 `app_user.area` 进行统计。
录用来源映射:
| `hire_source` | 返回名称 |
|---|---|
| `0` | 本系统 |
| `1` | 招聘会 |
| 其他值 | 其他 |
新入职确认人数来自 `employee_confirm` 表的有效确认记录。
### 9.4 返回结构
```json
{
"areaCode": "659001000000",
"areaName": "石河子市",
"totalHired": 86,
"byHireSource": {
"本系统": 52,
"招聘会": 26,
"其他": 8
},
"newEmployeeConfirmed": 31
}
```
### 9.5 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `areaCode` | string | 实际统计区域编码 |
| `areaName` | string | 实际统计区域名称 |
| `totalHired` | number | 有效已录用人数 |
| `byHireSource` | object | 录用来源名称到人数的映射 |
| `newEmployeeConfirmed` | number | 新入职确认人数 |
## 10. 接口汇总
| 统计模块 | 请求地址 | 主要用途 |
|---|---|---|
| 业务统计 | `GET /cms/statics/business` | 按月统计岗位来源和新增注册人数 |
| 招聘会统计 | `GET /cms/outdoor-fair/statistics` | 按日/周/月/季度/年度统计招聘会 |
| 求职统计 | `GET /cms/statics/jobSeeker` | 统计求职者年龄、工作经验、专业 |
| 热门岗位统计 | `GET /cms/statics/hotJob` | 统计 `job_hot` 字典配置的热门岗位数量 |
| 用人单位统计 | `GET /cms/statics/company` | 统计单位规模/性质及其发布岗位数量 |
| 区划企业分析 | `GET /cms/areaAnalysis/company` | 按区域统计企业总数、行业、规模、性质 |
| 区划岗位分析 | `GET /cms/areaAnalysis/job` | 按区域统计岗位、招聘人数、薪资、学历等 |
| 区划就业分析 | `GET /cms/areaAnalysis/employment` | 按区域统计已录用和新入职确认数据 |

View File

@@ -0,0 +1,72 @@
package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.query.AreaAnalysisQuery;
import com.ruoyi.cms.domain.vo.AreaCompanyDetailVO;
import com.ruoyi.cms.domain.vo.AreaEmploymentDetailVO;
import com.ruoyi.cms.domain.vo.AreaJobDetailVO;
import com.ruoyi.cms.service.IAreaAnalysisService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 区划数据分析 Controller
*/
@RestController
@RequestMapping("/cms/areaAnalysis")
@Api(tags = "后台:区划数据分析")
public class AreaAnalysisController extends BaseController {
@Autowired
private IAreaAnalysisService areaAnalysisService;
@ApiOperation("企业分析")
@PreAuthorize("@ss.hasPermi('cms:areaAnalysis:query')")
@GetMapping("/company")
public AjaxResult companyAnalysis(
@ApiParam("区划编码,不传则查师市整体") @RequestParam(required = false) String areaCode,
@ApiParam("开始日期,格式 yyyy-MM-dd") @RequestParam(required = false) String startDate,
@ApiParam("结束日期,格式 yyyy-MM-dd") @RequestParam(required = false) String endDate) {
AreaAnalysisQuery query = new AreaAnalysisQuery();
query.setAreaCode(areaCode);
query.setStartDate(startDate);
query.setEndDate(endDate);
return success(areaAnalysisService.companyAnalysis(query));
}
@ApiOperation("岗位分析")
@PreAuthorize("@ss.hasPermi('cms:areaAnalysis:query')")
@GetMapping("/job")
public AjaxResult jobAnalysis(
@ApiParam("区划编码,不传则查师市整体") @RequestParam(required = false) String areaCode,
@ApiParam("开始日期,格式 yyyy-MM-dd") @RequestParam(required = false) String startDate,
@ApiParam("结束日期,格式 yyyy-MM-dd") @RequestParam(required = false) String endDate) {
AreaAnalysisQuery query = new AreaAnalysisQuery();
query.setAreaCode(areaCode);
query.setStartDate(startDate);
query.setEndDate(endDate);
return success(areaAnalysisService.jobAnalysis(query));
}
@ApiOperation("就业分析")
@PreAuthorize("@ss.hasPermi('cms:areaAnalysis:query')")
@GetMapping("/employment")
public AjaxResult employmentAnalysis(
@ApiParam("区划编码,不传则查师市整体") @RequestParam(required = false) String areaCode,
@ApiParam("开始日期,格式 yyyy-MM-dd") @RequestParam(required = false) String startDate,
@ApiParam("结束日期,格式 yyyy-MM-dd") @RequestParam(required = false) String endDate) {
AreaAnalysisQuery query = new AreaAnalysisQuery();
query.setAreaCode(areaCode);
query.setStartDate(startDate);
query.setEndDate(endDate);
return success(areaAnalysisService.employmentAnalysis(query));
}
}

View File

@@ -244,11 +244,11 @@ public class OutdoorFairController extends BaseController
/**
* 招聘会数据统计dashboard
* 按年/周/月/维度,统计举办时间落在区间内的招聘会:
* 按周/月/季度/年维度,统计举办时间落在区间内的招聘会:
* - 累计发布招聘会数、参会单位数、职位数;
* - 招聘会数同比(去年同期等长区间)、环比(紧邻的上一等长区间);
* - 各时间桶的招聘会数/参会单位数/职位数序列(用于曲线图)。
* granularity 取值day/week/month/year默认 month。未传时间时统计全部招聘会
* granularity 取值day/week/month/quarter/year默认 month。未传时间时统计全部招聘会
* (“累计”口径),曲线图区间按数据实际举办时间的最小/最大值生成。
*/
@ApiOperation("招聘会数据统计")
@@ -400,6 +400,9 @@ public class OutdoorFairController extends BaseController
return ld;
case "week":
return ld.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
case "quarter":
int firstMonthOfQuarter = ((ld.getMonthValue() - 1) / 3) * 3 + 1;
return ld.withMonth(firstMonthOfQuarter).withDayOfMonth(1);
case "year":
return ld.withDayOfYear(1);
default:
@@ -421,6 +424,9 @@ public class OutdoorFairController extends BaseController
case "week":
cur = cur.plusWeeks(1);
break;
case "quarter":
cur = cur.plusMonths(3);
break;
case "year":
cur = cur.plusYears(1);
break;
@@ -438,6 +444,8 @@ public class OutdoorFairController extends BaseController
return DateTimeFormatter.ofPattern("yyyy-MM-dd");
case "week":
return DateTimeFormatter.ofPattern("yyyy-'W'ww");
case "quarter":
return DateTimeFormatter.ofPattern("yyyy-'Q'Q");
case "year":
return DateTimeFormatter.ofPattern("yyyy");
default:

View File

@@ -1,11 +1,14 @@
package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.BussinessDictType;
import com.ruoyi.cms.domain.query.BusinessStatisticsQuery;
import com.ruoyi.cms.domain.query.Staticsquery;
import com.ruoyi.cms.service.IBusinessStatisticsService;
import com.ruoyi.cms.service.StaticsqueryService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,6 +22,49 @@ import java.util.Map;
public class StaticsController extends BaseController {
@Autowired
private StaticsqueryService service;
@Autowired
private IBusinessStatisticsService businessStatisticsService;
/**
* 岗位来源和新增注册人数月趋势统计。
*/
@ApiOperation("岗位来源和新增注册人数月趋势统计")
@GetMapping("/business")
public AjaxResult business(BusinessStatisticsQuery query)
{
return success(businessStatisticsService.trend(query));
}
/**
* 按年龄、工作经验和专业统计求职者。
*/
@ApiOperation("求职者统计")
@GetMapping("/jobSeeker")
public AjaxResult jobSeeker(BusinessStatisticsQuery query)
{
return success(businessStatisticsService.jobSeeker(query));
}
/**
* 按 job_hot 业务字典配置统计热门岗位。
*/
@ApiOperation("热门岗位统计")
@GetMapping("/hotJob")
public AjaxResult hotJob(BusinessStatisticsQuery query)
{
return success(businessStatisticsService.hotJob(query));
}
/**
* 按企业规模、企业性质统计单位数量和发布岗位数量。
*/
@ApiOperation("用人单位统计")
@GetMapping("/company")
public AjaxResult company(BusinessStatisticsQuery query)
{
return success(businessStatisticsService.company(query));
}
//分行业趋势分析
@GetMapping("/industry")
public AjaxResult industry(Staticsquery staticsquery)

View File

@@ -0,0 +1,19 @@
package com.ruoyi.cms.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("区划分析查询参数")
public class AreaAnalysisQuery {
@ApiModelProperty("区划编码,不传则查师市整体")
private String areaCode;
@ApiModelProperty("开始日期,格式 yyyy-MM-dd")
private String startDate;
@ApiModelProperty("结束日期,格式 yyyy-MM-dd")
private String endDate;
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.cms.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 业务统计查询参数。
*/
@Data
@ApiModel("业务统计查询参数")
public class BusinessStatisticsQuery {
@ApiModelProperty("开始日期,支持 yyyy-MM 或 yyyy-MM-dd不传时默认为本年度 1 月 1 日")
private String startDate;
@ApiModelProperty("结束日期,支持 yyyy-MM 或 yyyy-MM-dd不传时默认为当前日期")
private String endDate;
/** 兼容现有 /cms/statics 接口使用的参数命名。 */
@ApiModelProperty(hidden = true)
private String startTime;
/** 兼容现有 /cms/statics 接口使用的参数命名。 */
@ApiModelProperty(hidden = true)
private String endTime;
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
@Data
public class AreaCompanyDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("区划编码")
private String areaCode;
@ApiModelProperty("区划名称")
private String areaName;
@ApiModelProperty("企业总数")
private Long totalCompanyCount;
@ApiModelProperty("按行业统计")
private Map<String, Long> byIndustry;
@ApiModelProperty("按规模统计")
private Map<String, Long> byScale;
@ApiModelProperty("按性质统计")
private Map<String, Long> byNature;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
@Data
public class AreaEmploymentDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("区划编码")
private String areaCode;
@ApiModelProperty("区划名称")
private String areaName;
@ApiModelProperty("已录用人数")
private Long totalHired;
@ApiModelProperty("按录用来源统计:本系统/招聘会")
private Map<String, Long> byHireSource;
@ApiModelProperty("新入职确认人数")
private Long newEmployeeConfirmed;
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
@Data
public class AreaJobDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("区划编码")
private String areaCode;
@ApiModelProperty("区划名称")
private String areaName;
@ApiModelProperty("岗位总数")
private Long totalJobCount;
@ApiModelProperty("招聘总人数")
private Long totalVacancies;
@ApiModelProperty("急招岗位数")
private Long urgentJobCount;
@ApiModelProperty("重点人群岗位数")
private Long keyPopulationJobCount;
@ApiModelProperty("按岗位分类统计")
private Map<String, Long> byCategory;
@ApiModelProperty("按薪资区间统计")
private Map<String, Long> bySalary;
@ApiModelProperty("按学历要求统计")
private Map<String, Long> byEducation;
@ApiModelProperty("按岗位类型统计")
private Map<String, Long> byType;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 业务统计图表序列。
*/
@Data
@ApiModel("业务统计图表序列")
public class BusinessStatisticsSeriesVO {
@ApiModelProperty("序列编码")
private String code;
@ApiModelProperty("序列名称")
private String name;
@ApiModelProperty("按月份排列的数据,和 months 一一对应")
private List<Long> data = new ArrayList<>();
public BusinessStatisticsSeriesVO() {
}
public BusinessStatisticsSeriesVO(String code, String name, List<Long> data) {
this.code = code;
this.name = name;
this.data = data;
}
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 业务统计图表数据。
*/
@Data
@ApiModel("业务统计")
public class BusinessStatisticsVO {
@ApiModelProperty("统计月份,按时间正序")
private List<String> months = new ArrayList<>();
@ApiModelProperty("岗位来源统计序列:本地石河子模板导入、本地录入、互联网岗位上传")
private List<BusinessStatisticsSeriesVO> jobSourceSeries = new ArrayList<>();
@ApiModelProperty("新增注册人数序列")
private BusinessStatisticsSeriesVO newRegisterSeries;
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 用人单位统计项。
*/
@Data
@ApiModel("用人单位统计项")
public class CompanyStatisticsItemVO {
@ApiModelProperty("字典值")
private String code;
@ApiModelProperty("统计名称")
private String name;
@ApiModelProperty("单位数量")
private Long companyCount = 0L;
@ApiModelProperty("发布岗位数量")
private Long jobCount = 0L;
public CompanyStatisticsItemVO() {
}
public CompanyStatisticsItemVO(String code, String name, Long companyCount, Long jobCount) {
this.code = code;
this.name = name;
this.companyCount = companyCount;
this.jobCount = jobCount;
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 用人单位及其发布岗位统计结果。
*/
@Data
@ApiModel("用人单位统计")
public class CompanyStatisticsVO {
@ApiModelProperty("按企业规模统计:微型、小型、中型、大型、超大型")
private List<CompanyStatisticsItemVO> scale = new ArrayList<>();
@ApiModelProperty("按企业性质统计,使用 company_nature 字典配置")
private List<CompanyStatisticsItemVO> nature = new ArrayList<>();
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 热门岗位统计项。
*/
@Data
@ApiModel("热门岗位统计项")
public class HotJobStatisticsItemVO {
@ApiModelProperty("热门岗位字典值")
private String code;
@ApiModelProperty("热门岗位名称")
private String name;
@ApiModelProperty("岗位数量")
private Long jobCount = 0L;
public HotJobStatisticsItemVO() {
}
public HotJobStatisticsItemVO(String code, String name, Long jobCount) {
this.code = code;
this.name = name;
this.jobCount = jobCount;
}
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 热门岗位统计结果。
*/
@Data
@ApiModel("热门岗位统计")
public class HotJobStatisticsVO {
@ApiModelProperty("热门岗位明细")
private List<HotJobStatisticsItemVO> items = new ArrayList<>();
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 求职者统计维度项。
*/
@Data
@ApiModel("求职者统计维度项")
public class JobSeekerStatisticsItemVO {
@ApiModelProperty("维度编码")
private String code;
@ApiModelProperty("维度名称")
private String label;
@ApiModelProperty("求职者人数")
private Long count;
public JobSeekerStatisticsItemVO() {
}
public JobSeekerStatisticsItemVO(String code, String label, Long count) {
this.code = code;
this.label = label;
this.count = count;
}
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.cms.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 求职者统计结果。
*/
@Data
@ApiModel("求职者统计")
public class JobSeekerStatisticsVO {
@ApiModelProperty("时间范围内求职者总数")
private Long total = 0L;
@ApiModelProperty("按年龄统计")
private List<JobSeekerStatisticsItemVO> age = new ArrayList<>();
@ApiModelProperty("按工作经验统计")
private List<JobSeekerStatisticsItemVO> workExperience = new ArrayList<>();
@ApiModelProperty("按专业/期望岗位统计")
private List<JobSeekerStatisticsItemVO> profession = new ArrayList<>();
}

View File

@@ -0,0 +1,72 @@
package com.ruoyi.cms.mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 区划数据分析 Mapper
*/
public interface AreaAnalysisMapper {
/** 查询指定区划的企业汇总 */
List<Map<String, Object>> selectCompanySummary(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询企业按行业统计 */
List<Map<String, Object>> selectCompanyByIndustry(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询企业按规模统计 */
List<Map<String, Object>> selectCompanyByScale(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询企业按性质统计 */
List<Map<String, Object>> selectCompanyByNature(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询指定区划的岗位汇总 */
List<Map<String, Object>> selectJobSummary(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询岗位按分类统计 */
List<Map<String, Object>> selectJobByCategory(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询岗位按薪资区间统计 */
List<Map<String, Object>> selectJobBySalary(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询岗位按学历要求统计 */
List<Map<String, Object>> selectJobByEducation(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询岗位按类型统计 */
List<Map<String, Object>> selectJobByType(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询指定区划的就业汇总 */
List<Map<String, Object>> selectEmploymentSummary(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询就业按录用来源统计 */
List<Map<String, Object>> selectEmploymentByHireSource(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
/** 查询新入职确认人数 */
Long selectNewEmployeeConfirmed(@Param("areaCode") String areaCode,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.cms.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 业务统计 Mapper。
*/
@Mapper
public interface BusinessStatisticsMapper {
/** 按月统计三种来源的岗位新增数量。 */
List<Map<String, Object>> selectJobSourceTrend(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按月统计新增注册用户数量。 */
List<Map<String, Object>> selectNewRegisterTrend(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按年龄统计求职者。 */
List<Map<String, Object>> selectJobSeekerByAge(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按工作经验统计求职者。 */
List<Map<String, Object>> selectJobSeekerByWorkExperience(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按专业/期望岗位统计求职者。 */
List<Map<String, Object>> selectJobSeekerByProfession(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按 job_hot 字典配置统计热门岗位。 */
List<Map<String, Object>> selectHotJobStatistics(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/** 按企业规模、企业性质统计单位数量和发布岗位数量。 */
List<Map<String, Object>> selectCompanyStatistics(@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.cms.service;
import com.ruoyi.cms.domain.query.AreaAnalysisQuery;
import com.ruoyi.cms.domain.vo.AreaCompanyDetailVO;
import com.ruoyi.cms.domain.vo.AreaEmploymentDetailVO;
import com.ruoyi.cms.domain.vo.AreaJobDetailVO;
/**
* 区划数据分析 Service
*/
public interface IAreaAnalysisService {
/** 企业分析 */
AreaCompanyDetailVO companyAnalysis(AreaAnalysisQuery query);
/** 岗位分析 */
AreaJobDetailVO jobAnalysis(AreaAnalysisQuery query);
/** 就业分析 */
AreaEmploymentDetailVO employmentAnalysis(AreaAnalysisQuery query);
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.cms.service;
import com.ruoyi.cms.domain.query.BusinessStatisticsQuery;
import com.ruoyi.cms.domain.vo.JobSeekerStatisticsVO;
import com.ruoyi.cms.domain.vo.BusinessStatisticsVO;
import com.ruoyi.cms.domain.vo.HotJobStatisticsVO;
import com.ruoyi.cms.domain.vo.CompanyStatisticsVO;
/**
* 业务统计 Service。
*/
public interface IBusinessStatisticsService {
/** 查询岗位来源和新增注册人数的月趋势。 */
BusinessStatisticsVO trend(BusinessStatisticsQuery query);
/** 查询按年龄、工作经验和专业分组的求职者统计。 */
JobSeekerStatisticsVO jobSeeker(BusinessStatisticsQuery query);
/** 查询按 job_hot 字典配置的热门岗位统计。 */
HotJobStatisticsVO hotJob(BusinessStatisticsQuery query);
/** 查询按企业规模、企业性质统计的单位和发布岗位数量。 */
CompanyStatisticsVO company(BusinessStatisticsQuery query);
}

View File

@@ -0,0 +1,231 @@
package com.ruoyi.cms.service.impl;
import com.ruoyi.cms.domain.query.AreaAnalysisQuery;
import com.ruoyi.cms.domain.vo.*;
import com.ruoyi.cms.mapper.AreaAnalysisMapper;
import com.ruoyi.cms.service.IAreaAnalysisService;
import com.ruoyi.cms.util.DictUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class AreaAnalysisServiceImpl implements IAreaAnalysisService {
@Autowired
private AreaAnalysisMapper areaAnalysisMapper;
/**
* dict_value → sys_area.code 映射
* 前端传的 areaCode 是 bussiness_dict_data(dict_type='area') 的 dict_value
* Mapper 中需要 sys_area.code 来做下级区划匹配
*/
private static final Map<String, String> DICT_VALUE_TO_AREA_CODE = new LinkedHashMap<>();
static {
DICT_VALUE_TO_AREA_CODE.put("0", "659001000000"); // 石河子市
DICT_VALUE_TO_AREA_CODE.put("9", "659001100000"); // 石总场 → 北泉镇
DICT_VALUE_TO_AREA_CODE.put("21", "659001100000"); // 北泉镇
DICT_VALUE_TO_AREA_CODE.put("15", "659001003000"); // 红山街道
DICT_VALUE_TO_AREA_CODE.put("16", "659001001000"); // 新城街道
DICT_VALUE_TO_AREA_CODE.put("17", "659001005000"); // 东城街道
DICT_VALUE_TO_AREA_CODE.put("18", "659001004000"); // 老街街道
DICT_VALUE_TO_AREA_CODE.put("19", "659001002000"); // 向阳街道
DICT_VALUE_TO_AREA_CODE.put("20", "659001101000"); // 石河子镇
DICT_VALUE_TO_AREA_CODE.put("1", "654203503000"); // 121团
DICT_VALUE_TO_AREA_CODE.put("11", "654203501000"); // 148团
DICT_VALUE_TO_AREA_CODE.put("12", "654203502000"); // 149团
DICT_VALUE_TO_AREA_CODE.put("2", "654202501000"); // 133团
DICT_VALUE_TO_AREA_CODE.put("13", "654202504000"); // 150团
DICT_VALUE_TO_AREA_CODE.put("3", "654202502000"); // 134团
DICT_VALUE_TO_AREA_CODE.put("4", "654202503000"); // 136团
DICT_VALUE_TO_AREA_CODE.put("14", "659001500000"); // 152团
DICT_VALUE_TO_AREA_CODE.put("5", "654201501000"); // 141团
DICT_VALUE_TO_AREA_CODE.put("6", "654201502000"); // 142团
DICT_VALUE_TO_AREA_CODE.put("7", "654201503000"); // 143团
DICT_VALUE_TO_AREA_CODE.put("8", "654201504000"); // 144团
DICT_VALUE_TO_AREA_CODE.put("10", "654203504000"); // 147团
}
// ========== 录用来源编码 → 中文名映射 ==========
private static final Map<String, String> HIRE_SOURCE_MAP = new LinkedHashMap<>();
static {
HIRE_SOURCE_MAP.put("system", "本系统");
HIRE_SOURCE_MAP.put("fair", "招聘会");
HIRE_SOURCE_MAP.put("other", "其他");
}
/**
* 将前端传入的 dict_value 转换为 sys_area.code
*/
private String resolveAreaCode(String dictValue) {
if (dictValue == null || dictValue.isEmpty()) {
// 未传区域时默认统计石河子市整体,不能放开为全国所有顶级区域。
return DICT_VALUE_TO_AREA_CODE.get("0");
}
return DICT_VALUE_TO_AREA_CODE.getOrDefault(dictValue, dictValue);
}
@Override
public AreaCompanyDetailVO companyAnalysis(AreaAnalysisQuery query) {
String areaCode = resolveAreaCode(query.getAreaCode());
String startDate = query.getStartDate();
String endDate = query.getEndDate();
List<Map<String, Object>> summaryList = areaAnalysisMapper.selectCompanySummary(areaCode, startDate, endDate);
List<Map<String, Object>> byIndustry = areaAnalysisMapper.selectCompanyByIndustry(areaCode, startDate, endDate);
List<Map<String, Object>> byScale = areaAnalysisMapper.selectCompanyByScale(areaCode, startDate, endDate);
List<Map<String, Object>> byNature = areaAnalysisMapper.selectCompanyByNature(areaCode, startDate, endDate);
AreaCompanyDetailVO vo = new AreaCompanyDetailVO();
long totalCompany = 0L;
long keyCompany = 0L;
long hrsCompany = 0L;
String firstAreaCode = null;
String firstAreaName = null;
for (Map<String, Object> row : summaryList) {
if (firstAreaCode == null) {
firstAreaCode = String.valueOf(row.get("area_code"));
firstAreaName = String.valueOf(row.get("area_name"));
}
totalCompany += toLong(row.get("total_company_count"));
keyCompany += toLong(row.get("key_company_count"));
hrsCompany += toLong(row.get("hrs_company_count"));
}
vo.setAreaCode(firstAreaCode != null ? firstAreaCode : areaCode);
vo.setAreaName(firstAreaName != null ? firstAreaName : "");
vo.setTotalCompanyCount(totalCompany);
vo.setByIndustry(toMap(byIndustry, "industry_key", "count"));
vo.setByScale(translateMap(byScale, "scale_key", "count", "scale"));
vo.setByNature(translateMap(byNature, "nature_key", "count", "company_nature"));
return vo;
}
@Override
public AreaJobDetailVO jobAnalysis(AreaAnalysisQuery query) {
String areaCode = resolveAreaCode(query.getAreaCode());
String startDate = query.getStartDate();
String endDate = query.getEndDate();
List<Map<String, Object>> byCategory = areaAnalysisMapper.selectJobByCategory(areaCode, startDate, endDate);
List<Map<String, Object>> bySalary = areaAnalysisMapper.selectJobBySalary(areaCode, startDate, endDate);
List<Map<String, Object>> byEducation = areaAnalysisMapper.selectJobByEducation(areaCode, startDate, endDate);
List<Map<String, Object>> byType = areaAnalysisMapper.selectJobByType(areaCode, startDate, endDate);
List<Map<String, Object>> summaryList = areaAnalysisMapper.selectJobSummary(areaCode, startDate, endDate);
AreaJobDetailVO vo = new AreaJobDetailVO();
long totalJob = 0L;
long totalVacancies = 0L;
long urgentJob = 0L;
long keyPopJob = 0L;
String firstAreaCode = null;
String firstAreaName = null;
for (Map<String, Object> row : summaryList) {
if (firstAreaCode == null) {
firstAreaCode = String.valueOf(row.get("area_code"));
firstAreaName = String.valueOf(row.get("area_name"));
}
totalJob += toLong(row.get("total_job_count"));
totalVacancies += toLong(row.get("total_vacancies"));
urgentJob += toLong(row.get("urgent_job_count"));
keyPopJob += toLong(row.get("key_population_job_count"));
}
vo.setAreaCode(firstAreaCode != null ? firstAreaCode : areaCode);
vo.setAreaName(firstAreaName != null ? firstAreaName : "");
vo.setTotalJobCount(totalJob);
vo.setTotalVacancies(totalVacancies);
vo.setUrgentJobCount(urgentJob);
vo.setKeyPopulationJobCount(keyPopJob);
vo.setByCategory(toMap(byCategory, "category_key", "count"));
vo.setBySalary(toMap(bySalary, "salary_key", "count"));
vo.setByEducation(translateMap(byEducation, "education_key", "count", "education"));
vo.setByType(translateMap(byType, "type_key", "count", "position_type"));
return vo;
}
@Override
public AreaEmploymentDetailVO employmentAnalysis(AreaAnalysisQuery query) {
String areaCode = resolveAreaCode(query.getAreaCode());
String startDate = query.getStartDate();
String endDate = query.getEndDate();
List<Map<String, Object>> summaryList = areaAnalysisMapper.selectEmploymentSummary(areaCode, startDate, endDate);
List<Map<String, Object>> byHireSource = areaAnalysisMapper.selectEmploymentByHireSource(areaCode, startDate, endDate);
// Long newConfirmed = areaAnalysisMapper.selectNewEmployeeConfirmed(areaCode, startDate, endDate);
AreaEmploymentDetailVO vo = new AreaEmploymentDetailVO();
long totalHired = 0L;
String firstAreaCode = null;
String firstAreaName = null;
for (Map<String, Object> row : summaryList) {
if (firstAreaCode == null) {
firstAreaCode = String.valueOf(row.get("area_code"));
firstAreaName = String.valueOf(row.get("area_name"));
}
totalHired += toLong(row.get("total_hired"));
}
vo.setAreaCode(firstAreaCode != null ? firstAreaCode : areaCode);
vo.setAreaName(firstAreaName != null ? firstAreaName : "");
vo.setTotalHired(totalHired);
vo.setByHireSource(translateStaticMap(toMap(byHireSource, "hire_source_key", "count"), HIRE_SOURCE_MAP));
// vo.setNewEmployeeConfirmed(newConfirmed != null ? newConfirmed : 0L);
return vo;
}
// ========== 字典翻译辅助方法 ==========
private Map<String, Long> translateMap(List<Map<String, Object>> list, String keyField, String valueField, String dictType) {
Map<String, Long> raw = toMap(list, keyField, valueField);
Map<String, Long> result = new LinkedHashMap<>();
for (Map.Entry<String, Long> entry : raw.entrySet()) {
String label = DictUtils.getDictLabel(dictType, entry.getKey());
String key = (label != null && !label.isEmpty()) ? label : entry.getKey();
result.put(key, entry.getValue());
}
return result;
}
private Map<String, Long> translateStaticMap(Map<String, Long> raw, Map<String, String> mapping) {
Map<String, Long> result = new LinkedHashMap<>();
for (Map.Entry<String, Long> entry : raw.entrySet()) {
String key = mapping.getOrDefault(entry.getKey(), entry.getKey());
result.put(key, entry.getValue());
}
return result;
}
private Map<String, Long> toMap(List<Map<String, Object>> list, String keyField, String valueField) {
Map<String, Long> result = new LinkedHashMap<>();
if (list == null) {
return result;
}
for (Map<String, Object> row : list) {
Object keyObj = row.get(keyField);
if (keyObj == null) {
continue;
}
result.put(String.valueOf(keyObj), toLong(row.get(valueField)));
}
return result;
}
private long toLong(Object value) {
if (value == null) {
return 0L;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException e) {
return 0L;
}
}
}

View File

@@ -0,0 +1,298 @@
package com.ruoyi.cms.service.impl;
import com.ruoyi.cms.domain.query.BusinessStatisticsQuery;
import com.ruoyi.cms.domain.vo.BusinessStatisticsSeriesVO;
import com.ruoyi.cms.domain.vo.BusinessStatisticsVO;
import com.ruoyi.cms.domain.vo.JobSeekerStatisticsItemVO;
import com.ruoyi.cms.domain.vo.JobSeekerStatisticsVO;
import com.ruoyi.cms.domain.vo.HotJobStatisticsItemVO;
import com.ruoyi.cms.domain.vo.HotJobStatisticsVO;
import com.ruoyi.cms.domain.vo.CompanyStatisticsItemVO;
import com.ruoyi.cms.domain.vo.CompanyStatisticsVO;
import com.ruoyi.cms.mapper.BusinessStatisticsMapper;
import com.ruoyi.cms.service.IBusinessStatisticsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 业务统计 Service 实现。
*/
@Service
public class BusinessStatisticsServiceImpl implements IBusinessStatisticsService {
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String SOURCE_LOCAL_TEMPLATE = "5";
private static final String SOURCE_LOCAL_ENTRY = "1";
private static final String SOURCE_INTERNET = "2";
@Autowired
private BusinessStatisticsMapper businessStatisticsMapper;
@Override
public BusinessStatisticsVO trend(BusinessStatisticsQuery query) {
DateRange range = resolveDateRange(query);
List<YearMonth> monthRange = buildMonthRange(range.start, range.end);
List<String> months = new ArrayList<>(monthRange.size());
for (YearMonth month : monthRange) {
months.add(month.format(MONTH_FORMATTER));
}
Map<String, Map<String, Object>> jobRows = indexByMonth(
businessStatisticsMapper.selectJobSourceTrend(range.startTime, range.endTime));
Map<String, Map<String, Object>> registerRows = indexByMonth(
businessStatisticsMapper.selectNewRegisterTrend(range.startTime, range.endTime));
List<Long> localTemplateImport = new ArrayList<>(monthRange.size());
List<Long> localEntry = new ArrayList<>(monthRange.size());
List<Long> internetUpload = new ArrayList<>(monthRange.size());
List<Long> newRegister = new ArrayList<>(monthRange.size());
for (String month : months) {
Map<String, Object> jobRow = jobRows.get(month);
localTemplateImport.add(toLong(value(jobRow, "local_template_import_count")));
localEntry.add(toLong(value(jobRow, "local_entry_count")));
internetUpload.add(toLong(value(jobRow, "internet_upload_count")));
Map<String, Object> registerRow = registerRows.get(month);
newRegister.add(toLong(value(registerRow, "register_count")));
}
BusinessStatisticsVO result = new BusinessStatisticsVO();
result.setMonths(months);
List<BusinessStatisticsSeriesVO> jobSourceSeries = new ArrayList<>(3);
jobSourceSeries.add(new BusinessStatisticsSeriesVO(
SOURCE_LOCAL_TEMPLATE, "本地石河子模板导入", localTemplateImport));
jobSourceSeries.add(new BusinessStatisticsSeriesVO(
SOURCE_LOCAL_ENTRY, "本地录入", localEntry));
jobSourceSeries.add(new BusinessStatisticsSeriesVO(
SOURCE_INTERNET, "互联网岗位上传", internetUpload));
result.setJobSourceSeries(jobSourceSeries);
result.setNewRegisterSeries(new BusinessStatisticsSeriesVO(
"register", "新增注册人数", newRegister));
return result;
}
@Override
public JobSeekerStatisticsVO jobSeeker(BusinessStatisticsQuery query) {
DateRange range = resolveDateRange(query);
List<Map<String, Object>> ageRows = businessStatisticsMapper.selectJobSeekerByAge(
range.startTime, range.endTime);
List<Map<String, Object>> experienceRows = businessStatisticsMapper.selectJobSeekerByWorkExperience(
range.startTime, range.endTime);
List<Map<String, Object>> professionRows = businessStatisticsMapper.selectJobSeekerByProfession(
range.startTime, range.endTime);
JobSeekerStatisticsVO result = new JobSeekerStatisticsVO();
result.setAge(toDimensionItems(ageRows));
result.setWorkExperience(toDimensionItems(experienceRows));
result.setProfession(toDimensionItems(professionRows));
result.setTotal(result.getAge().stream()
.map(JobSeekerStatisticsItemVO::getCount)
.filter(java.util.Objects::nonNull)
.mapToLong(Long::longValue)
.sum());
return result;
}
@Override
public HotJobStatisticsVO hotJob(BusinessStatisticsQuery query) {
DateRange range = resolveDateRange(query);
List<Map<String, Object>> rows = businessStatisticsMapper.selectHotJobStatistics(
range.startTime, range.endTime);
HotJobStatisticsVO result = new HotJobStatisticsVO();
if (rows == null) {
return result;
}
List<HotJobStatisticsItemVO> items = new ArrayList<>(rows.size());
for (Map<String, Object> row : rows) {
HotJobStatisticsItemVO item = new HotJobStatisticsItemVO(
stringValue(value(row, "code")),
stringValue(value(row, "name")),
toLong(value(row, "job_count")));
items.add(item);
}
result.setItems(items);
return result;
}
@Override
public CompanyStatisticsVO company(BusinessStatisticsQuery query) {
DateRange range = resolveDateRange(query);
List<Map<String, Object>> rows = businessStatisticsMapper.selectCompanyStatistics(
range.startTime, range.endTime);
CompanyStatisticsVO result = new CompanyStatisticsVO();
if (rows == null) {
return result;
}
for (Map<String, Object> row : rows) {
CompanyStatisticsItemVO item = new CompanyStatisticsItemVO(
stringValue(value(row, "code")),
stringValue(value(row, "name")),
toLong(value(row, "company_count")),
toLong(value(row, "job_count")));
String dimension = stringValue(value(row, "dimension"));
if ("nature".equalsIgnoreCase(dimension)) {
result.getNature().add(item);
} else if ("scale".equalsIgnoreCase(dimension)) {
result.getScale().add(item);
}
}
return result;
}
private DateRange resolveDateRange(BusinessStatisticsQuery query) {
LocalDate today = LocalDate.now();
String startText = query == null ? null : firstNotBlank(query.getStartDate(), query.getStartTime());
String endText = query == null ? null : firstNotBlank(query.getEndDate(), query.getEndTime());
LocalDate start = isBlank(startText)
? today.withDayOfYear(1)
: parseDate(startText, false);
LocalDate end = isBlank(endText)
? today
: parseDate(endText, true);
if (start.isAfter(end)) {
throw new IllegalArgumentException("开始日期不能晚于结束日期");
}
LocalDate endExclusive = end.plusDays(1);
return new DateRange(
start,
end,
toDate(start.atStartOfDay()),
toDate(endExclusive.atStartOfDay()));
}
/**
* yyyy-MM 作为月份查询时开始日期取月初结束日期取月末yyyy-MM-dd 按日期本身处理。
*/
private LocalDate parseDate(String text, boolean endDate) {
String value = text == null ? "" : text.trim();
try {
if (value.matches("\\d{4}-\\d{2}")) {
YearMonth month = YearMonth.parse(value, MONTH_FORMATTER);
return endDate ? month.atEndOfMonth() : month.atDay(1);
}
return LocalDate.parse(value, DATE_FORMATTER);
} catch (DateTimeParseException ex) {
throw new IllegalArgumentException("日期格式错误,请使用 yyyy-MM 或 yyyy-MM-dd");
}
}
private List<YearMonth> buildMonthRange(LocalDate start, LocalDate end) {
List<YearMonth> result = new ArrayList<>();
YearMonth current = YearMonth.from(start);
YearMonth last = YearMonth.from(end);
while (!current.isAfter(last)) {
result.add(current);
current = current.plusMonths(1);
}
return result;
}
private Map<String, Map<String, Object>> indexByMonth(List<Map<String, Object>> rows) {
Map<String, Map<String, Object>> result = new LinkedHashMap<>();
if (rows == null) {
return result;
}
for (Map<String, Object> row : rows) {
Object month = value(row, "month");
if (month != null) {
result.put(String.valueOf(month), row);
}
}
return result;
}
private Object value(Map<String, Object> row, String key) {
if (row == null) {
return null;
}
Object value = row.get(key);
if (value == null) {
value = row.get(key.toUpperCase());
}
return value;
}
private List<JobSeekerStatisticsItemVO> toDimensionItems(List<Map<String, Object>> rows) {
List<JobSeekerStatisticsItemVO> result = new ArrayList<>();
if (rows == null) {
return result;
}
for (Map<String, Object> row : rows) {
Object code = value(row, "code");
Object label = value(row, "label");
if (label == null) {
continue;
}
result.add(new JobSeekerStatisticsItemVO(
code == null ? "unknown" : String.valueOf(code),
String.valueOf(label),
toLong(value(row, "count"))));
}
return result;
}
private long toLong(Object value) {
if (value == null) {
return 0L;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException ex) {
return 0L;
}
}
private String stringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String firstNotBlank(String first, String second) {
return !isBlank(first) ? first : second;
}
private Date toDate(LocalDateTime value) {
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
}
private static class DateRange {
private final LocalDate start;
private final LocalDate end;
private final Date startTime;
private final Date endTime;
private DateRange(LocalDate start, LocalDate end, Date startTime, Date endTime) {
this.start = start;
this.end = end;
this.startTime = startTime;
this.endTime = endTime;
}
}
}

View File

@@ -0,0 +1,397 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cms.mapper.AreaAnalysisMapper">
<!--
区划映射说明:
前端传入的 areaCode 是 bussiness_dict_data(dict_type='area') 的 dict_value
ServiceImpl 中已将 dict_value 转为 sys_area.codeMapper 中接收的 areaCode 是 sys_area.code
job.job_location_area_code 存的是 dict_value需要通过 bussiness_dict_data 映射到 sys_area.code
company.company_city 直接等于 sys_area.code
统计下级区划sys_area.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
-->
<!-- ========== 企业 ========== -->
<!-- 企业汇总含下级区划areaCode 已是 sys_area.code -->
<select id="selectCompanySummary" resultType="java.util.LinkedHashMap">
SELECT
sa.code AS area_code,
sa.name AS area_name,
COALESCE(t.total_company, 0) AS total_company_count,
COALESCE(t.key_company, 0) AS key_company_count,
COALESCE(t.hrs_company, 0) AS hrs_company_count
FROM sys_area sa
LEFT JOIN (
SELECT
c.company_city AS area_code,
COUNT(c.company_id) AS total_company,
COUNT(CASE WHEN c.is_imp_company = '1' THEN 1 END) AS key_company,
COUNT(CASE WHEN c.is_hrs = 'Y' THEN 1 END) AS hrs_company
FROM company c
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.company_city IS NOT NULL AND c.company_city != ''
<if test="startDate != null and startDate != ''">
AND c.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND c.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY c.company_city
) t ON t.area_code = sa.code
WHERE sa.del_flag = '0'
<choose>
<when test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</when>
<otherwise>
AND sa.parent_code IS NULL
</otherwise>
</choose>
ORDER BY sa.code
</select>
<!-- 企业按行业统计 -->
<select id="selectCompanyByIndustry" resultType="java.util.LinkedHashMap">
SELECT
c.industry AS industry_key,
COUNT(c.company_id) AS count
FROM company c
INNER JOIN sys_area sa ON sa.code = c.company_city AND sa.del_flag = '0'
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.company_city IS NOT NULL AND c.company_city != ''
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND c.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND c.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY c.industry
</select>
<!-- 企业按规模统计 -->
<select id="selectCompanyByScale" resultType="java.util.LinkedHashMap">
SELECT
c.scale AS scale_key,
COUNT(c.company_id) AS count
FROM company c
INNER JOIN sys_area sa ON sa.code = c.company_city AND sa.del_flag = '0'
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.company_city IS NOT NULL AND c.company_city != ''
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND c.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND c.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY c.scale
</select>
<!-- 企业按性质统计 -->
<select id="selectCompanyByNature" resultType="java.util.LinkedHashMap">
SELECT
c.company_nature AS nature_key,
COUNT(c.company_id) AS count
FROM company c
INNER JOIN sys_area sa ON sa.code = c.company_city AND sa.del_flag = '0'
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.company_city IS NOT NULL AND c.company_city != ''
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND c.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND c.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY c.company_nature
</select>
<!-- ========== 岗位 ========== -->
<!-- 岗位汇总含下级区划job_location_area_code 通过 dict_value 映射到 sys_area.code -->
<select id="selectJobSummary" resultType="java.util.LinkedHashMap">
SELECT
sa.code AS area_code,
sa.name AS area_name,
COALESCE(t.job_count, 0) AS total_job_count,
COALESCE(t.total_vacancies, 0) AS total_vacancies,
COALESCE(t.urgent_count, 0) AS urgent_job_count,
COALESCE(t.key_pop_count, 0) AS key_population_job_count
FROM sys_area sa
LEFT JOIN (
SELECT
sa2.code AS area_code,
COUNT(j.job_id) AS job_count,
COALESCE(SUM(CASE WHEN j.vacancies IS NULL OR j.vacancies <![CDATA[ <= ]]> 0 THEN 1 ELSE j.vacancies END), 0) AS total_vacancies,
SUM(CASE WHEN j.is_urgent = '1' THEN 1 ELSE 0 END) AS urgent_count,
SUM(CASE WHEN j.is_key_populations = '0' THEN 1 ELSE 0 END) AS key_pop_count
FROM job j
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND CAST(j.job_location_area_code AS VARCHAR) = bdd.dict_value
INNER JOIN sys_area sa2 ON sa2.del_flag = '0'
AND sa2.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND COALESCE(j.review_status, '1') = '1'
AND j.job_location_area_code IS NOT NULL
<if test="areaCode != null and areaCode != ''">
AND sa2.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND j.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND j.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY sa2.code
) t ON t.area_code = sa.code
WHERE sa.del_flag = '0'
<choose>
<when test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</when>
<otherwise>
AND sa.parent_code IS NULL
</otherwise>
</choose>
ORDER BY sa.code
</select>
<!-- 岗位按分类统计 -->
<select id="selectJobByCategory" resultType="java.util.LinkedHashMap">
SELECT
j.job_category AS category_key,
COUNT(j.job_id) AS count
FROM job j
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND CAST(j.job_location_area_code AS VARCHAR) = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND COALESCE(j.review_status, '1') = '1'
AND j.job_location_area_code IS NOT NULL
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND j.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND j.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY j.job_category
</select>
<!-- 岗位按薪资区间统计 -->
<select id="selectJobBySalary" resultType="java.util.LinkedHashMap">
SELECT
CASE
WHEN j.max_salary <![CDATA[ < ]]> 3000 THEN '3k以下'
WHEN j.min_salary >= 3000 AND j.max_salary <![CDATA[ <= ]]> 5000 THEN '3k-5k'
WHEN j.min_salary >= 5000 AND j.max_salary <![CDATA[ <= ]]> 8000 THEN '5k-8k'
WHEN j.min_salary >= 8000 AND j.max_salary <![CDATA[ <= ]]> 15000 THEN '8k-15k'
WHEN j.min_salary >= 15000 THEN '15k+'
ELSE '未知'
END AS salary_key,
COUNT(j.job_id) AS count
FROM job j
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND CAST(j.job_location_area_code AS VARCHAR) = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND COALESCE(j.review_status, '1') = '1'
AND j.job_location_area_code IS NOT NULL
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND j.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND j.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY CASE
WHEN j.max_salary <![CDATA[ < ]]> 3000 THEN '3k以下'
WHEN j.min_salary >= 3000 AND j.max_salary <![CDATA[ <= ]]> 5000 THEN '3k-5k'
WHEN j.min_salary >= 5000 AND j.max_salary <![CDATA[ <= ]]> 8000 THEN '5k-8k'
WHEN j.min_salary >= 8000 AND j.max_salary <![CDATA[ <= ]]> 15000 THEN '8k-15k'
WHEN j.min_salary >= 15000 THEN '15k+'
ELSE '未知'
END
</select>
<!-- 岗位按学历要求统计 -->
<select id="selectJobByEducation" resultType="java.util.LinkedHashMap">
SELECT
j.education AS education_key,
COUNT(j.job_id) AS count
FROM job j
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND CAST(j.job_location_area_code AS VARCHAR) = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND COALESCE(j.review_status, '1') = '1'
AND j.job_location_area_code IS NOT NULL
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND j.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND j.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY j.education
</select>
<!-- 岗位按类型统计 -->
<select id="selectJobByType" resultType="java.util.LinkedHashMap">
SELECT
j.type AS type_key,
COUNT(j.job_id) AS count
FROM job j
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND CAST(j.job_location_area_code AS VARCHAR) = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND COALESCE(j.review_status, '1') = '1'
AND j.job_location_area_code IS NOT NULL
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND j.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND j.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY j.type
</select>
<!-- ========== 就业(仅统计已录用 hire='0' ========== -->
<!-- 就业汇总(含下级区划,仅已录用) -->
<select id="selectEmploymentSummary" resultType="java.util.LinkedHashMap">
SELECT
sa.code AS area_code,
sa.name AS area_name,
COALESCE(t.total_hired, 0) AS total_hired
FROM sys_area sa
LEFT JOIN (
SELECT
sa2.code AS area_code,
COUNT(DISTINCT ja.id) AS total_hired
FROM app_user u
INNER JOIN job_apply ja ON ja.user_id = u.user_id AND ja.del_flag = '0' AND ja.hire = '0'
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND u.area = bdd.dict_value
INNER JOIN sys_area sa2 ON sa2.del_flag = '0'
AND sa2.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND u.status = '0'
AND u.area IS NOT NULL AND u.area != ''
<if test="startDate != null and startDate != ''">
AND ja.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND ja.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY sa2.code
) t ON t.area_code = sa.code
WHERE sa.del_flag = '0'
<choose>
<when test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</when>
<otherwise>
AND sa.parent_code IS NULL
</otherwise>
</choose>
ORDER BY sa.code
</select>
<!-- 就业按录用来源统计(仅已录用) -->
<select id="selectEmploymentByHireSource" resultType="java.util.LinkedHashMap">
SELECT
CASE
WHEN ja.hire_source = '0' THEN 'system'
WHEN ja.hire_source = '1' THEN 'fair'
ELSE 'other'
END AS hire_source_key,
COUNT(DISTINCT ja.id) AS count
FROM app_user u
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND u.area = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
INNER JOIN job_apply ja ON ja.user_id = u.user_id AND ja.del_flag = '0' AND ja.hire = '0'
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND u.status = '0'
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND ja.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND ja.create_time <![CDATA[ <= ]]> #{endDate}
</if>
GROUP BY CASE
WHEN ja.hire_source = '0' THEN 'system'
WHEN ja.hire_source = '1' THEN 'fair'
ELSE 'other'
END
</select>
<!-- 新入职确认人数(仅已录用) -->
<select id="selectNewEmployeeConfirmed" resultType="java.lang.Long">
SELECT COALESCE(COUNT(ec.id), 0)
FROM employee_confirm ec
INNER JOIN app_user u ON CAST(u.user_id AS VARCHAR) = ec.user_id AND u.del_flag = '0' AND u.is_company_user = '1' AND u.status = '0'
INNER JOIN bussiness_dict_data bdd ON bdd.dict_type = 'area' AND bdd.status = '0'
AND u.area = bdd.dict_value
INNER JOIN sys_area sa ON sa.del_flag = '0'
AND sa.name LIKE CONCAT('%', bdd.dict_label, '%')
WHERE ec.del_flag = '0'
<if test="areaCode != null and areaCode != ''">
AND sa.code LIKE CONCAT(CAST(#{areaCode} AS VARCHAR), '%')
</if>
<if test="startDate != null and startDate != ''">
AND ec.create_time >= #{startDate}
</if>
<if test="endDate != null and endDate != ''">
AND ec.create_time <![CDATA[ <= ]]> #{endDate}
</if>
</select>
</mapper>

View File

@@ -0,0 +1,246 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cms.mapper.BusinessStatisticsMapper">
<!--
create_time 表示数据入库/注册时间,而不是岗位 posting_date。
统计采用左闭右开时间范围,避免结束日期为当天 00:00:00 时漏掉当天数据。
-->
<select id="selectJobSourceTrend" resultType="java.util.HashMap">
SELECT TO_CHAR(j.create_time, 'YYYY-MM') AS month,
SUM(CASE WHEN j.data_source = '5' THEN 1 ELSE 0 END) AS local_template_import_count,
SUM(CASE WHEN j.data_source = '1' THEN 1 ELSE 0 END) AS local_entry_count,
SUM(CASE WHEN j.data_source = '2' THEN 1 ELSE 0 END) AS internet_upload_count
FROM job j
WHERE j.del_flag = '0'
AND j.data_source IN ('5', '1', '2')
AND j.create_time <![CDATA[ >= ]]> #{startTime}
AND j.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY TO_CHAR(j.create_time, 'YYYY-MM')
ORDER BY month
</select>
<select id="selectNewRegisterTrend" resultType="java.util.HashMap">
SELECT TO_CHAR(u.create_time, 'YYYY-MM') AS month,
COUNT(*) AS register_count
FROM app_user u
WHERE u.del_flag = '0'
AND u.create_time <![CDATA[ >= ]]> #{startTime}
AND u.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY TO_CHAR(u.create_time, 'YYYY-MM')
ORDER BY month
</select>
<!-- 求职者is_company_user=1create_time 按注册时间过滤。 -->
<select id="selectJobSeekerByAge" resultType="java.util.HashMap">
SELECT COALESCE(d.dict_value, 'unknown') AS code,
COALESCE(d.dict_label, '未填写') AS label,
COUNT(DISTINCT u.user_id) AS count
FROM app_user u
LEFT JOIN bussiness_dict_data d
ON d.dict_type = 'age'
AND d.dict_value = u.age
AND d.status = '0'
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND u.create_time <![CDATA[ >= ]]> #{startTime}
AND u.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY d.dict_value, d.dict_label, d.dict_sort
ORDER BY d.dict_sort NULLS LAST, d.dict_value NULLS LAST
</select>
<select id="selectJobSeekerByWorkExperience" resultType="java.util.HashMap">
SELECT COALESCE(d.dict_value, 'unknown') AS code,
COALESCE(d.dict_label, '未填写') AS label,
COUNT(DISTINCT u.user_id) AS count
FROM app_user u
LEFT JOIN bussiness_dict_data d
ON d.dict_type = 'experience'
AND d.dict_value = u.work_experience
AND d.status = '0'
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND u.create_time <![CDATA[ >= ]]> #{startTime}
AND u.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY d.dict_value, d.dict_label, d.dict_sort
ORDER BY d.dict_sort NULLS LAST, d.dict_value NULLS LAST
</select>
<!--
job_title_id 是逗号分隔的期望岗位 ID按岗位名称作为“专业/求职方向”统计。
一个求职者选择多个期望岗位时,会分别计入多个岗位项;使用 DISTINCT user_id
防止同一用户同一岗位 ID重复出现时被重复计数。
-->
<select id="selectJobSeekerByProfession" resultType="java.util.HashMap">
SELECT CAST(jt.job_id AS VARCHAR) AS code,
COALESCE(jt.job_name, '未填写') AS label,
COUNT(DISTINCT u.user_id) AS count
FROM app_user u
LEFT JOIN LATERAL unnest(string_to_array(COALESCE(u.job_title_id, ''), ','))
AS selected_title(job_id_text) ON TRUE
LEFT JOIN job_title jt
ON jt.job_id = CAST(NULLIF(TRIM(selected_title.job_id_text), '') AS BIGINT)
AND jt.status = '0'
AND jt.del_flag = '0'
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND u.create_time <![CDATA[ >= ]]> #{startTime}
AND u.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY jt.job_id, jt.job_name, jt.order_num
ORDER BY CASE WHEN jt.job_id IS NULL THEN 1 ELSE 0 END,
jt.order_num NULLS LAST,
jt.job_id NULLS LAST
</select>
<!--
从 bussiness_dict_data 读取 job_hot 的 dict_label直接按岗位名称前缀关联 job.job_title。
字典项没有匹配岗位时仍返回一行,岗位数量为 0。
-->
<select id="selectHotJobStatistics" resultType="java.util.HashMap">
SELECT d.dict_value AS code,
d.dict_label AS name,
COUNT(DISTINCT j.job_id) AS job_count
FROM bussiness_dict_data d
LEFT JOIN job j
ON j.del_flag = '0'
AND j.create_time <![CDATA[ >= ]]> #{startTime}
AND j.create_time <![CDATA[ < ]]> #{endTime}
AND TRIM(j.job_title) LIKE TRIM(d.dict_label) || '%'
WHERE d.dict_type = 'job_hot'
AND d.status = '0'
AND NULLIF(BTRIM(d.dict_label), '') IS NOT NULL
GROUP BY d.dict_value, d.dict_label, d.dict_sort
ORDER BY d.dict_sort NULLS LAST, d.dict_value NULLS LAST
</select>
<!--
企业规模按 scale 字典值归并1 微型2 小型3 中型4/5 大型6/7 超大型。
company_count 按 company.create_time 统计job_count 按 job.create_time 统计,
避免历史企业在查询期间发布新岗位时被企业注册时间过滤掉。
企业性质只读取 company_nature 字典中已配置的项目;当前只有国企时只返回国企。
-->
<select id="selectCompanyStatistics" resultType="java.util.HashMap">
WITH scale_definitions AS (
SELECT CASE
WHEN d.dict_value = '1' THEN '1'
WHEN d.dict_value = '2' THEN '2'
WHEN d.dict_value = '3' THEN '3'
WHEN d.dict_value IN ('4', '5') THEN '4_5'
WHEN d.dict_value IN ('6', '7') THEN '6_7'
END AS code,
CASE
WHEN d.dict_value = '1' THEN '微型'
WHEN d.dict_value = '2' THEN '小型'
WHEN d.dict_value = '3' THEN '中型'
WHEN d.dict_value IN ('4', '5') THEN '大型'
WHEN d.dict_value IN ('6', '7') THEN '超大型'
END AS name,
d.dict_value
FROM bussiness_dict_data d
WHERE d.dict_type = 'scale'
AND d.status = '0'
AND d.dict_value IN ('1', '2', '3', '4', '5', '6', '7')
),
scale_groups AS (
SELECT code, name
FROM scale_definitions
GROUP BY code, name
),
company_scale AS (
SELECT sd.code,
COUNT(DISTINCT c.company_id) AS company_count
FROM scale_definitions sd
INNER JOIN company c ON c.scale = sd.dict_value
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.create_time <![CDATA[ >= ]]> #{startTime}
AND c.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY sd.code
),
job_scale AS (
SELECT sd.code,
COUNT(DISTINCT j.job_id) AS job_count
FROM scale_definitions sd
INNER JOIN company c ON c.scale = sd.dict_value
INNER JOIN job j ON j.company_id = c.company_id
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND j.review_status = '1'
AND j.create_time <![CDATA[ >= ]]> #{startTime}
AND j.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY sd.code
),
scale_rows AS (
SELECT 'scale' AS dimension,
sg.code,
sg.name,
COALESCE(cs.company_count, 0) AS company_count,
COALESCE(js.job_count, 0) AS job_count
FROM scale_groups sg
LEFT JOIN company_scale cs ON cs.code = sg.code
LEFT JOIN job_scale js ON js.code = sg.code
),
nature_definitions AS (
SELECT DISTINCT d.dict_value AS code,
d.dict_label AS name,
d.dict_sort
FROM bussiness_dict_data d
WHERE d.dict_type = 'company_nature'
AND d.status = '0'
AND NULLIF(BTRIM(d.dict_label), '') IS NOT NULL
),
company_nature AS (
SELECT nd.code,
COUNT(DISTINCT c.company_id) AS company_count
FROM nature_definitions nd
INNER JOIN company c ON c.company_nature = nd.code
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND c.create_time <![CDATA[ >= ]]> #{startTime}
AND c.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY nd.code
),
job_nature AS (
SELECT nd.code,
COUNT(DISTINCT j.job_id) AS job_count
FROM nature_definitions nd
INNER JOIN company c ON c.company_nature = nd.code
INNER JOIN job j ON j.company_id = c.company_id
WHERE c.del_flag = '0'
AND c.status = 1
AND c.company_status = '0'
AND j.del_flag = '0'
AND j.is_publish = 1
AND j.job_status = '0'
AND j.review_status = '1'
AND j.create_time <![CDATA[ >= ]]> #{startTime}
AND j.create_time <![CDATA[ < ]]> #{endTime}
GROUP BY nd.code
),
nature_rows AS (
SELECT 'nature' AS dimension,
nd.code,
nd.name,
COALESCE(cn.company_count, 0) AS company_count,
COALESCE(jn.job_count, 0) AS job_count
FROM nature_definitions nd
LEFT JOIN company_nature cn ON cn.code = nd.code
LEFT JOIN job_nature jn ON jn.code = nd.code
)
SELECT dimension, code, name, company_count, job_count
FROM scale_rows
UNION ALL
SELECT dimension, code, name, company_count, job_count
FROM nature_rows
ORDER BY dimension, code
</select>
</mapper>