diff --git a/docs/cms-statistics-api.md b/docs/cms-statistics-api.md new file mode 100644 index 0000000..b9ce705 --- /dev/null +++ b/docs/cms-statistics-api.md @@ -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` | 按区域统计已录用和新入职确认数据 | diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/AreaAnalysisController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/AreaAnalysisController.java new file mode 100644 index 0000000..b3206e8 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/AreaAnalysisController.java @@ -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)); + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java index a6b851a..77afd65 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java @@ -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: diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/StaticsController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/StaticsController.java index 44c8b7d..d8f8214 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/StaticsController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/StaticsController.java @@ -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) diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/AreaAnalysisQuery.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/AreaAnalysisQuery.java new file mode 100644 index 0000000..172c67f --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/AreaAnalysisQuery.java @@ -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; +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/BusinessStatisticsQuery.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/BusinessStatisticsQuery.java new file mode 100644 index 0000000..d64412b --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/BusinessStatisticsQuery.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaCompanyDetailVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaCompanyDetailVO.java new file mode 100644 index 0000000..8f934b3 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaCompanyDetailVO.java @@ -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 byIndustry; + + @ApiModelProperty("按规模统计") + private Map byScale; + + @ApiModelProperty("按性质统计") + private Map byNature; +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaEmploymentDetailVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaEmploymentDetailVO.java new file mode 100644 index 0000000..6b4422b --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaEmploymentDetailVO.java @@ -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 byHireSource; + + @ApiModelProperty("新入职确认人数") + private Long newEmployeeConfirmed; +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaJobDetailVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaJobDetailVO.java new file mode 100644 index 0000000..7a9ee03 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/AreaJobDetailVO.java @@ -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 byCategory; + + @ApiModelProperty("按薪资区间统计") + private Map bySalary; + + @ApiModelProperty("按学历要求统计") + private Map byEducation; + + @ApiModelProperty("按岗位类型统计") + private Map byType; +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsSeriesVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsSeriesVO.java new file mode 100644 index 0000000..85daf93 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsSeriesVO.java @@ -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 data = new ArrayList<>(); + + public BusinessStatisticsSeriesVO() { + } + + public BusinessStatisticsSeriesVO(String code, String name, List data) { + this.code = code; + this.name = name; + this.data = data; + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsVO.java new file mode 100644 index 0000000..089f5b0 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/BusinessStatisticsVO.java @@ -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 months = new ArrayList<>(); + + @ApiModelProperty("岗位来源统计序列:本地石河子模板导入、本地录入、互联网岗位上传") + private List jobSourceSeries = new ArrayList<>(); + + @ApiModelProperty("新增注册人数序列") + private BusinessStatisticsSeriesVO newRegisterSeries; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsItemVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsItemVO.java new file mode 100644 index 0000000..8947efb --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsItemVO.java @@ -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; + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsVO.java new file mode 100644 index 0000000..26a57b8 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyStatisticsVO.java @@ -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 scale = new ArrayList<>(); + + @ApiModelProperty("按企业性质统计,使用 company_nature 字典配置") + private List nature = new ArrayList<>(); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsItemVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsItemVO.java new file mode 100644 index 0000000..862b142 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsItemVO.java @@ -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; + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsVO.java new file mode 100644 index 0000000..84bc30f --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/HotJobStatisticsVO.java @@ -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 items = new ArrayList<>(); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsItemVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsItemVO.java new file mode 100644 index 0000000..88549da --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsItemVO.java @@ -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; + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsVO.java new file mode 100644 index 0000000..b53018d --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/JobSeekerStatisticsVO.java @@ -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 age = new ArrayList<>(); + + @ApiModelProperty("按工作经验统计") + private List workExperience = new ArrayList<>(); + + @ApiModelProperty("按专业/期望岗位统计") + private List profession = new ArrayList<>(); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/AreaAnalysisMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/AreaAnalysisMapper.java new file mode 100644 index 0000000..91901ea --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/AreaAnalysisMapper.java @@ -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> selectCompanySummary(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询企业按行业统计 */ + List> selectCompanyByIndustry(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询企业按规模统计 */ + List> selectCompanyByScale(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询企业按性质统计 */ + List> selectCompanyByNature(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询指定区划的岗位汇总 */ + List> selectJobSummary(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询岗位按分类统计 */ + List> selectJobByCategory(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询岗位按薪资区间统计 */ + List> selectJobBySalary(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询岗位按学历要求统计 */ + List> selectJobByEducation(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询岗位按类型统计 */ + List> selectJobByType(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询指定区划的就业汇总 */ + List> selectEmploymentSummary(@Param("areaCode") String areaCode, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** 查询就业按录用来源统计 */ + List> 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); +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/BusinessStatisticsMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/BusinessStatisticsMapper.java new file mode 100644 index 0000000..1cf861a --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/BusinessStatisticsMapper.java @@ -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> selectJobSourceTrend(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按月统计新增注册用户数量。 */ + List> selectNewRegisterTrend(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按年龄统计求职者。 */ + List> selectJobSeekerByAge(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按工作经验统计求职者。 */ + List> selectJobSeekerByWorkExperience(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按专业/期望岗位统计求职者。 */ + List> selectJobSeekerByProfession(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按 job_hot 字典配置统计热门岗位。 */ + List> selectHotJobStatistics(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** 按企业规模、企业性质统计单位数量和发布岗位数量。 */ + List> selectCompanyStatistics(@Param("startTime") Date startTime, + @Param("endTime") Date endTime); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAreaAnalysisService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAreaAnalysisService.java new file mode 100644 index 0000000..d527074 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAreaAnalysisService.java @@ -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); +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IBusinessStatisticsService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IBusinessStatisticsService.java new file mode 100644 index 0000000..bd59257 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IBusinessStatisticsService.java @@ -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); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AreaAnalysisServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AreaAnalysisServiceImpl.java new file mode 100644 index 0000000..5e76476 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AreaAnalysisServiceImpl.java @@ -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 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 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> summaryList = areaAnalysisMapper.selectCompanySummary(areaCode, startDate, endDate); + List> byIndustry = areaAnalysisMapper.selectCompanyByIndustry(areaCode, startDate, endDate); + List> byScale = areaAnalysisMapper.selectCompanyByScale(areaCode, startDate, endDate); + List> 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 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> byCategory = areaAnalysisMapper.selectJobByCategory(areaCode, startDate, endDate); + List> bySalary = areaAnalysisMapper.selectJobBySalary(areaCode, startDate, endDate); + List> byEducation = areaAnalysisMapper.selectJobByEducation(areaCode, startDate, endDate); + List> byType = areaAnalysisMapper.selectJobByType(areaCode, startDate, endDate); + List> 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 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> summaryList = areaAnalysisMapper.selectEmploymentSummary(areaCode, startDate, endDate); + List> 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 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 translateMap(List> list, String keyField, String valueField, String dictType) { + Map raw = toMap(list, keyField, valueField); + Map result = new LinkedHashMap<>(); + for (Map.Entry 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 translateStaticMap(Map raw, Map mapping) { + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : raw.entrySet()) { + String key = mapping.getOrDefault(entry.getKey(), entry.getKey()); + result.put(key, entry.getValue()); + } + return result; + } + + private Map toMap(List> list, String keyField, String valueField) { + Map result = new LinkedHashMap<>(); + if (list == null) { + return result; + } + for (Map 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; + } + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/BusinessStatisticsServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/BusinessStatisticsServiceImpl.java new file mode 100644 index 0000000..6471036 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/BusinessStatisticsServiceImpl.java @@ -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 monthRange = buildMonthRange(range.start, range.end); + List months = new ArrayList<>(monthRange.size()); + for (YearMonth month : monthRange) { + months.add(month.format(MONTH_FORMATTER)); + } + + Map> jobRows = indexByMonth( + businessStatisticsMapper.selectJobSourceTrend(range.startTime, range.endTime)); + Map> registerRows = indexByMonth( + businessStatisticsMapper.selectNewRegisterTrend(range.startTime, range.endTime)); + + List localTemplateImport = new ArrayList<>(monthRange.size()); + List localEntry = new ArrayList<>(monthRange.size()); + List internetUpload = new ArrayList<>(monthRange.size()); + List newRegister = new ArrayList<>(monthRange.size()); + + for (String month : months) { + Map 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 registerRow = registerRows.get(month); + newRegister.add(toLong(value(registerRow, "register_count"))); + } + + BusinessStatisticsVO result = new BusinessStatisticsVO(); + result.setMonths(months); + List 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> ageRows = businessStatisticsMapper.selectJobSeekerByAge( + range.startTime, range.endTime); + List> experienceRows = businessStatisticsMapper.selectJobSeekerByWorkExperience( + range.startTime, range.endTime); + List> 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> rows = businessStatisticsMapper.selectHotJobStatistics( + range.startTime, range.endTime); + + HotJobStatisticsVO result = new HotJobStatisticsVO(); + if (rows == null) { + return result; + } + + List items = new ArrayList<>(rows.size()); + for (Map 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> rows = businessStatisticsMapper.selectCompanyStatistics( + range.startTime, range.endTime); + + CompanyStatisticsVO result = new CompanyStatisticsVO(); + if (rows == null) { + return result; + } + + for (Map 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 buildMonthRange(LocalDate start, LocalDate end) { + List 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> indexByMonth(List> rows) { + Map> result = new LinkedHashMap<>(); + if (rows == null) { + return result; + } + for (Map row : rows) { + Object month = value(row, "month"); + if (month != null) { + result.put(String.valueOf(month), row); + } + } + return result; + } + + private Object value(Map 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 toDimensionItems(List> rows) { + List result = new ArrayList<>(); + if (rows == null) { + return result; + } + for (Map 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; + } + } +} diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/AreaAnalysisMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/AreaAnalysisMapper.xml new file mode 100644 index 0000000..54e8ce8 --- /dev/null +++ b/ruoyi-bussiness/src/main/resources/mapper/app/AreaAnalysisMapper.xml @@ -0,0 +1,397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/BusinessStatisticsMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/BusinessStatisticsMapper.xml new file mode 100644 index 0000000..03b69d3 --- /dev/null +++ b/ruoyi-bussiness/src/main/resources/mapper/app/BusinessStatisticsMapper.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + +