feat: add recruitment fair statistics API
This commit is contained in:
473
docs/recruitment-fair-statistics-proposals.html
Normal file
473
docs/recruitment-fair-statistics-proposals.html
Normal file
@@ -0,0 +1,473 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>招聘会数据统计 · 三套界面方案</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #1f2a44;
|
||||
--muted: #74809a;
|
||||
--line: #e7ebf2;
|
||||
--blue: #2468f2;
|
||||
--blue-soft: #eef4ff;
|
||||
--mint: #0aa678;
|
||||
--amber: #e58a00;
|
||||
--violet: #7659df;
|
||||
--card: #ffffff;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at 82% 0%, #e7efff 0, transparent 24rem),
|
||||
#f6f8fc;
|
||||
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
button, input, select { font: inherit; }
|
||||
|
||||
.page-shell { max-width: 1540px; margin: 0 auto; padding: 44px 28px 64px; }
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
color: #1959d1;
|
||||
background: #eaf1ff;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
h1, h2, h3, p { margin: 0; }
|
||||
h1 { margin-top: 11px; font-size: 30px; line-height: 1.2; letter-spacing: -.03em; }
|
||||
.page-intro { max-width: 600px; color: var(--muted); }
|
||||
|
||||
.rule-note {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 17px;
|
||||
border: 1px solid #dce7ff;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,.72);
|
||||
color: #52617b;
|
||||
}
|
||||
|
||||
.rule-dot { flex: 0 0 auto; width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; background: var(--blue); }
|
||||
.rule-note strong { color: #34425e; }
|
||||
|
||||
.proposal-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
|
||||
|
||||
.proposal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e4e9f1;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 30px rgba(38, 61, 104, .06);
|
||||
}
|
||||
|
||||
.proposal-top { padding: 21px 22px 18px; border-bottom: 1px solid #edf0f5; }
|
||||
.proposal-kicker { color: var(--blue); font-weight: 800; font-size: 12px; letter-spacing: .08em; }
|
||||
.proposal-top h2 { margin-top: 5px; font-size: 20px; }
|
||||
.proposal-summary { min-height: 42px; margin-top: 7px; color: var(--muted); font-size: 13px; }
|
||||
|
||||
.mockup {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 646px;
|
||||
padding: 18px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.ui-window {
|
||||
min-height: 610px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e3e8f0;
|
||||
border-radius: 10px;
|
||||
background: var(--card);
|
||||
box-shadow: 0 5px 15px rgba(23, 44, 78, .06);
|
||||
}
|
||||
|
||||
.ui-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 53px;
|
||||
padding: 0 17px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.ui-title { font-size: 16px; font-weight: 700; }
|
||||
.quiet { color: #9aa6ba; font-size: 12px; }
|
||||
|
||||
.segment {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
border-radius: 7px;
|
||||
background: #f1f4f9;
|
||||
}
|
||||
|
||||
.segment button {
|
||||
height: 28px;
|
||||
padding: 0 11px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: #66738c;
|
||||
background: transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.segment button.active { color: #1459df; background: #fff; font-weight: 700; box-shadow: 0 1px 3px rgba(29, 58, 100, .12); }
|
||||
.segment button:focus-visible, .choose-button:focus-visible, .selection-button:focus-visible { outline: 3px solid #9ec1ff; outline-offset: 2px; }
|
||||
|
||||
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 16px; border-bottom: 1px solid #edf0f5; }
|
||||
.field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 31px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid #dde4ef;
|
||||
border-radius: 5px;
|
||||
color: #56647c;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.field.primary { border-color: #8eb4ff; color: #1f60d5; background: #f8fbff; }
|
||||
.field .chevron { margin-left: 10px; color: #a2afc2; }
|
||||
.filter-label { align-self: center; color: #8390a5; font-size: 12px; }
|
||||
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; }
|
||||
.stat-card { min-width: 0; padding: 13px 12px; border: 1px solid #ebeff5; border-radius: 7px; background: #fff; }
|
||||
.stat-card dt { overflow: hidden; color: #7c89a0; font-size: 12px; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.stat-card dd { margin: 7px 0 0; color: #263757; font-weight: 800; font-size: 24px; line-height: 1; letter-spacing: -.04em; }
|
||||
.stat-card dd small { margin-left: 2px; color: #8995a9; font-weight: 500; font-size: 11px; letter-spacing: 0; }
|
||||
.stat-card.accent { border-color: #d7e5ff; background: #f6f9ff; }
|
||||
.stat-card.accent dd { color: #1b61e7; }
|
||||
|
||||
.data-block { margin: 14px 16px 0; border: 1px solid #e9edf4; border-radius: 7px; }
|
||||
.block-head { display: flex; align-items: center; justify-content: space-between; min-height: 40px; padding: 0 12px; border-bottom: 1px solid #edf0f4; font-size: 13px; font-weight: 700; }
|
||||
.block-head a { color: #3975e8; font-size: 12px; font-weight: 500; text-decoration: none; }
|
||||
.rows { padding: 1px 12px; }
|
||||
.data-row { display: flex; align-items: center; justify-content: space-between; min-height: 36px; border-bottom: 1px solid #f0f2f6; color: #59667e; font-size: 12px; }
|
||||
.data-row:last-child { border: 0; }
|
||||
.rank { display: inline-flex; justify-content: center; width: 17px; margin-right: 8px; border-radius: 3px; color: #fff; background: #aab6c8; font-weight: 800; font-size: 10px; }
|
||||
.rank.top { background: #f19a22; }
|
||||
.row-value { color: #2f405e; font-weight: 700; }
|
||||
|
||||
/* Plan A */
|
||||
.plan-a .ui-window { border-radius: 8px; }
|
||||
.plan-a .filter-row { align-items: center; }
|
||||
.plan-a .stat-wrap { padding: 17px 16px 2px; }
|
||||
.hint-line { padding: 9px 16px 0; color: #8995a7; font-size: 11px; }
|
||||
.hint-line b { color: #2564db; }
|
||||
|
||||
/* Plan B */
|
||||
.plan-b .mockup { padding: 14px; background: #f1f5fb; }
|
||||
.plan-b .ui-window { display: grid; grid-template-columns: 126px minmax(0, 1fr); min-height: 610px; border-radius: 12px; }
|
||||
.side-nav { padding: 16px 10px; border-right: 1px solid #ebeff5; background: #fbfcfe; }
|
||||
.brand { display: flex; align-items: center; gap: 7px; margin: 0 5px 23px; color: #34425b; font-weight: 800; font-size: 12px; }
|
||||
.brand-mark { width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, #416ff0, #7e75ea); }
|
||||
.nav-group { margin: 17px 5px 6px; color: #9aa6ba; font-size: 10px; letter-spacing: .08em; }
|
||||
.nav-item { display: flex; align-items: center; gap: 7px; padding: 8px 7px; border-radius: 5px; color: #69758a; font-size: 12px; }
|
||||
.nav-item.active { color: #245cda; background: #ebf2ff; font-weight: 700; }
|
||||
.nav-icon { width: 11px; height: 11px; border: 1.5px solid currentColor; border-radius: 3px; opacity: .75; }
|
||||
.work-area { min-width: 0; }
|
||||
.plan-b .ui-titlebar { padding: 0 14px; }
|
||||
.plan-b .ui-title { font-size: 15px; }
|
||||
.plan-b .filter-row { padding: 12px 13px; gap: 6px; }
|
||||
.plan-b .field { padding: 0 7px; font-size: 11px; }
|
||||
.plan-b .scope-label { width: 100%; margin-top: 3px; color: #8490a3; font-size: 11px; }
|
||||
.scope-pills { display: inline-flex; gap: 5px; }
|
||||
.scope-pill { padding: 4px 7px; border-radius: 999px; color: #647188; background: #f2f5f9; font-size: 11px; }
|
||||
.scope-pill.active { color: #1762d7; background: #eaf2ff; font-weight: 700; }
|
||||
.plan-b .stat-wrap { padding: 14px 13px 0; }
|
||||
.plan-b .stat-grid { grid-template-columns: repeat(2, 1fr); gap: 7px; }
|
||||
.plan-b .stat-card { padding: 10px; }
|
||||
.plan-b .stat-card dd { font-size: 21px; }
|
||||
.mini-chart { display: flex; align-items: flex-end; gap: 5px; height: 102px; padding: 18px 12px 12px; }
|
||||
.bar { flex: 1; min-width: 10px; border-radius: 3px 3px 0 0; background: linear-gradient(180deg, #78a4ff, #3e73ef); }
|
||||
.bar.muted { background: #dbe6fd; }
|
||||
.chart-note { display: flex; justify-content: space-between; padding: 0 12px 11px; color: #929eb1; font-size: 10px; }
|
||||
|
||||
/* Plan C */
|
||||
.plan-c .mockup { padding: 15px; background: #f5f6fb; }
|
||||
.plan-c .ui-window { border: 0; border-radius: 14px; box-shadow: 0 8px 24px rgba(41, 53, 89, .08); }
|
||||
.plan-c .ui-titlebar { min-height: 58px; padding: 0 17px; border: 0; }
|
||||
.plan-c .ui-title { font-size: 17px; }
|
||||
.plan-c .filter-row { padding: 0 17px 15px; border: 0; }
|
||||
.mode-card { display: flex; align-items: center; justify-content: space-between; margin: 0 17px; padding: 12px 13px; border-radius: 9px; background: linear-gradient(102deg, #eff5ff, #f7f3ff); }
|
||||
.mode-card .mode-title { color: #435373; font-size: 12px; }
|
||||
.mode-card .mode-title strong { display: block; margin-top: 2px; color: #263858; font-size: 13px; }
|
||||
.calendar-chip { padding: 5px 7px; border: 1px solid #d8e4f9; border-radius: 5px; color: #4770b7; background: rgba(255,255,255,.6); font-size: 11px; }
|
||||
.plan-c .stat-wrap { padding: 16px 17px 1px; }
|
||||
.plan-c .stat-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.plan-c .stat-card { min-height: 77px; padding: 11px; border: 0; border-radius: 9px; background: #f7f8fb; }
|
||||
.plan-c .stat-card.accent { background: #edf4ff; }
|
||||
.plan-c .stat-card dd { font-size: 23px; }
|
||||
.insight { display: flex; gap: 10px; align-items: flex-start; margin: 14px 17px; padding: 11px 12px; border-radius: 8px; color: #55709d; background: #f5f8fe; font-size: 11px; }
|
||||
.insight-mark { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 18px; height: 18px; border-radius: 50%; color: #fff; background: #5c85e8; font-weight: 800; font-size: 11px; }
|
||||
.plan-c .data-block { margin: 0 17px; }
|
||||
|
||||
.proposal-footer { display: flex; gap: 9px; align-items: center; justify-content: space-between; padding: 15px 22px 19px; border-top: 1px solid #edf0f5; }
|
||||
.pros { color: #65728a; font-size: 12px; }
|
||||
.pros strong { color: #3c4960; }
|
||||
.choose-button { min-width: 104px; height: 34px; cursor: pointer; border: 1px solid #cdddfb; border-radius: 6px; color: #225fd6; background: #f6f9ff; font-weight: 700; font-size: 12px; }
|
||||
.proposal.is-chosen { border-color: #81a9f6; box-shadow: 0 14px 34px rgba(41, 93, 194, .14); }
|
||||
.proposal.is-chosen .choose-button { border-color: var(--blue); color: #fff; background: var(--blue); }
|
||||
.selection-button { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px; cursor: pointer; border: 0; border-radius: 8px; color: #fff; background: #263858; font-weight: 700; }
|
||||
.selection-button .status { display: none; width: 6px; height: 6px; border-radius: 50%; background: #92edc8; }
|
||||
.selection-button.has-choice .status { display: block; }
|
||||
|
||||
.online-only, .outdoor-only { transition: opacity .16s ease; }
|
||||
.outdoor-only { display: none; }
|
||||
.is-outdoor .online-only { display: none; }
|
||||
.is-outdoor .outdoor-only { display: block; }
|
||||
.is-outdoor .outdoor-only.inline { display: inline; }
|
||||
|
||||
@media (max-width: 1160px) {
|
||||
.proposal-grid { grid-template-columns: 1fr; max-width: 620px; margin: 0 auto; }
|
||||
.mockup { min-height: auto; }
|
||||
.ui-window { min-height: 540px; }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.page-shell { padding: 28px 16px 40px; }
|
||||
.page-header { display: block; }
|
||||
.page-intro { margin-top: 12px; }
|
||||
.rule-note { margin-top: 16px; }
|
||||
.proposal-top, .proposal-footer { padding-right: 16px; padding-left: 16px; }
|
||||
.mockup { padding: 10px; }
|
||||
.stat-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.plan-b .ui-window { grid-template-columns: 102px minmax(0, 1fr); }
|
||||
.side-nav { padding-right: 7px; padding-left: 7px; }
|
||||
.brand { margin-left: 2px; margin-right: 2px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page-shell">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<span class="eyebrow">UI PROPOSAL · 01–03</span>
|
||||
<h1>招聘会数据统计</h1>
|
||||
<p class="page-intro">三套后台菜单界面方案。每套均支持按单场招聘会统计,或按招聘会起始时间统计时间段汇总。</p>
|
||||
</div>
|
||||
<button class="selection-button" id="selectionStatus" type="button" aria-live="polite"><i class="status"></i><span>尚未选择方案</span></button>
|
||||
</header>
|
||||
|
||||
<aside class="rule-note" aria-label="统计口径说明">
|
||||
<i class="rule-dot"></i>
|
||||
<div><strong>统一统计口径:</strong>需求人数取岗位“招聘数”;招聘数填写“若干”时按 <strong>1 人</strong>计算。线上招聘会展示「投递岗位数」,户外招聘会展示「签到数」;户外招聘会不提供报名投递简历。</div>
|
||||
</aside>
|
||||
|
||||
<section class="proposal-grid" aria-label="招聘会数据统计界面方案">
|
||||
<article class="proposal plan-a" data-plan="方案一:标准后台统计页">
|
||||
<header class="proposal-top">
|
||||
<div class="proposal-kicker">方案一 / 标准清晰</div>
|
||||
<h2>标准后台统计页</h2>
|
||||
<p class="proposal-summary">筛选区、指标卡、明细区层次分明,最贴近常规管理后台的使用习惯。</p>
|
||||
</header>
|
||||
<div class="mockup is-online" data-mockup>
|
||||
<div class="ui-window">
|
||||
<div class="ui-titlebar">
|
||||
<div class="ui-title">招聘会数据统计</div>
|
||||
<div class="segment" aria-label="招聘会类型">
|
||||
<button class="active" type="button" data-type="online">线上招聘会</button>
|
||||
<button type="button" data-type="outdoor">户外招聘会</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">统计范围</span>
|
||||
<span class="field primary">单场招聘会 <span class="chevron">⌄</span></span>
|
||||
<span class="field">2026 年春风行动线上招聘会 <span class="chevron">⌄</span></span>
|
||||
<span class="field">查询</span>
|
||||
</div>
|
||||
<div class="hint-line">也可切换为 <b>按起始时间统计</b>,选择起始日期与结束日期后汇总多场招聘会。</div>
|
||||
<div class="stat-wrap">
|
||||
<dl class="stat-grid">
|
||||
<div class="stat-card"><dt>参会单位数</dt><dd>128<small>家</small></dd></div>
|
||||
<div class="stat-card"><dt>职位数</dt><dd>356<small>个</small></dd></div>
|
||||
<div class="stat-card"><dt>需求人数</dt><dd>1,024<small>人</small></dd></div>
|
||||
<div class="stat-card accent online-only"><dt>投递岗位数</dt><dd>687<small>个</small></dd></div>
|
||||
<div class="stat-card accent outdoor-only"><dt>签到数</dt><dd>932<small>人次</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<section class="data-block" aria-label="招聘会明细">
|
||||
<div class="block-head"><span>本次统计涉及招聘会</span><a href="javascript:void(0)">查看列表</a></div>
|
||||
<div class="rows">
|
||||
<div class="data-row"><span>2026 年春风行动<span class="outdoor-only inline">户外</span><span class="online-only inline">线上</span>招聘会</span><span class="row-value">128 家 / 356 职位</span></div>
|
||||
<div class="data-row"><span>起始时间</span><span class="row-value">2026-03-07</span></div>
|
||||
<div class="data-row online-only"><span>投递最热岗位</span><span class="row-value">行政专员 · 54 次</span></div>
|
||||
<div class="data-row outdoor-only"><span>签到高峰时段</span><span class="row-value">10:00–11:00 · 328 人次</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="proposal-footer">
|
||||
<p class="pros"><strong>适合:</strong>快速上线、表单操作优先</p>
|
||||
<button class="choose-button" type="button">选择方案一</button>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article class="proposal plan-b" data-plan="方案二:数据工作台">
|
||||
<header class="proposal-top">
|
||||
<div class="proposal-kicker">方案二 / 数据工作台</div>
|
||||
<h2>侧栏式数据工作台</h2>
|
||||
<p class="proposal-summary">把“统计范围”作为核心入口,兼顾当场查看与时间段分析。</p>
|
||||
</header>
|
||||
<div class="mockup is-online" data-mockup>
|
||||
<div class="ui-window">
|
||||
<aside class="side-nav">
|
||||
<div class="brand"><i class="brand-mark"></i>就业服务</div>
|
||||
<div class="nav-group">招聘会管理</div>
|
||||
<div class="nav-item"><i class="nav-icon"></i>招聘会列表</div>
|
||||
<div class="nav-item active"><i class="nav-icon"></i>数据统计</div>
|
||||
<div class="nav-group">常用功能</div>
|
||||
<div class="nav-item"><i class="nav-icon"></i>单位管理</div>
|
||||
</aside>
|
||||
<div class="work-area">
|
||||
<div class="ui-titlebar"><div class="ui-title">招聘会数据统计</div><span class="quiet">数据更新时间 10:30</span></div>
|
||||
<div class="filter-row">
|
||||
<div class="segment" aria-label="招聘会类型">
|
||||
<button class="active" type="button" data-type="online">线上</button>
|
||||
<button type="button" data-type="outdoor">户外</button>
|
||||
</div>
|
||||
<span class="field">招聘会名称 <span class="chevron">⌄</span></span>
|
||||
<span class="scope-label">统计方式</span>
|
||||
<div class="scope-pills"><span class="scope-pill active">统计单场</span><span class="scope-pill">按起始时间段</span></div>
|
||||
<span class="field">2026-03-01 至 2026-03-31</span>
|
||||
</div>
|
||||
<div class="stat-wrap">
|
||||
<dl class="stat-grid">
|
||||
<div class="stat-card"><dt>参会单位</dt><dd>128<small>家</small></dd></div>
|
||||
<div class="stat-card"><dt>职位数</dt><dd>356<small>个</small></dd></div>
|
||||
<div class="stat-card"><dt>需求人数</dt><dd>1,024<small>人</small></dd></div>
|
||||
<div class="stat-card accent online-only"><dt>投递岗位</dt><dd>687<small>个</small></dd></div>
|
||||
<div class="stat-card accent outdoor-only"><dt>签到数</dt><dd>932<small>人次</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<section class="data-block" aria-label="趋势概览">
|
||||
<div class="block-head"><span class="online-only">投递岗位趋势</span><span class="outdoor-only">现场签到趋势</span><a href="javascript:void(0)">按日查看</a></div>
|
||||
<div class="mini-chart" aria-label="近七天统计柱状图">
|
||||
<i class="bar muted" style="height:31%"></i><i class="bar muted" style="height:48%"></i><i class="bar" style="height:72%"></i><i class="bar muted" style="height:53%"></i><i class="bar" style="height:88%"></i><i class="bar" style="height:63%"></i><i class="bar muted" style="height:41%"></i>
|
||||
</div>
|
||||
<div class="chart-note"><span>03/01</span><span>03/07</span><span>03/14</span><span>03/21</span><span>03/31</span></div>
|
||||
</section>
|
||||
<section class="data-block" aria-label="招聘会排行">
|
||||
<div class="block-head"><span>招聘会数据排行</span><a href="javascript:void(0)">更多</a></div>
|
||||
<div class="rows">
|
||||
<div class="data-row"><span><i class="rank top">1</i>春风行动专场</span><span class="row-value online-only">154 次投递</span><span class="row-value outdoor-only">246 人签到</span></div>
|
||||
<div class="data-row"><span><i class="rank">2</i>民营企业专场</span><span class="row-value online-only">122 次投递</span><span class="row-value outdoor-only">211 人签到</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="proposal-footer">
|
||||
<p class="pros"><strong>适合:</strong>后续扩展趋势、排行分析</p>
|
||||
<button class="choose-button" type="button">选择方案二</button>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article class="proposal plan-c" data-plan="方案三:概览看板">
|
||||
<header class="proposal-top">
|
||||
<div class="proposal-kicker">方案三 / 概览看板</div>
|
||||
<h2>轻量概览看板</h2>
|
||||
<p class="proposal-summary">将范围、类型和核心结果集中在首屏,更适合高频查数和汇报浏览。</p>
|
||||
</header>
|
||||
<div class="mockup is-online" data-mockup>
|
||||
<div class="ui-window">
|
||||
<div class="ui-titlebar">
|
||||
<div class="ui-title">招聘会数据统计</div>
|
||||
<div class="segment" aria-label="招聘会类型">
|
||||
<button class="active" type="button" data-type="online">线上招聘会</button>
|
||||
<button type="button" data-type="outdoor">户外招聘会</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<span class="field primary">统计单场 <span class="chevron">⌄</span></span>
|
||||
<span class="field">选择招聘会 <span class="chevron">⌄</span></span>
|
||||
</div>
|
||||
<section class="mode-card" aria-label="统计范围">
|
||||
<div class="mode-title">当前统计范围<strong>2026 年春风行动<span class="online-only">线上</span><span class="outdoor-only">户外</span>招聘会</strong></div>
|
||||
<span class="calendar-chip">起始 2026-03-07</span>
|
||||
</section>
|
||||
<div class="stat-wrap">
|
||||
<dl class="stat-grid">
|
||||
<div class="stat-card"><dt>参会单位数</dt><dd>128<small>家</small></dd></div>
|
||||
<div class="stat-card"><dt>职位数</dt><dd>356<small>个</small></dd></div>
|
||||
<div class="stat-card"><dt>需求人数</dt><dd>1,024<small>人</small></dd></div>
|
||||
<div class="stat-card accent online-only"><dt>投递岗位数</dt><dd>687<small>个</small></dd></div>
|
||||
<div class="stat-card accent outdoor-only"><dt>签到数</dt><dd>932<small>人次</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="insight online-only"><i class="insight-mark">i</i><span>本场招聘会共有 <b>356</b> 个岗位获得投递,其中 687 个岗位投递记录已去重汇总。</span></div>
|
||||
<div class="insight outdoor-only"><i class="insight-mark">i</i><span>户外招聘会不提供报名投递简历,现场到场数据以签到记录累计为准。</span></div>
|
||||
<section class="data-block" aria-label="统计口径">
|
||||
<div class="block-head"><span>统计说明</span><span class="quiet">自动汇总</span></div>
|
||||
<div class="rows">
|
||||
<div class="data-row"><span>单位与职位</span><span>按有效参会单位及岗位统计</span></div>
|
||||
<div class="data-row"><span>需求人数</span><span>“若干”按 1 人计算</span></div>
|
||||
<div class="data-row online-only"><span>投递岗位数</span><span>按投递岗位汇总</span></div>
|
||||
<div class="data-row outdoor-only"><span>签到数</span><span>按现场签到累计</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="proposal-footer">
|
||||
<p class="pros"><strong>适合:</strong>领导查数、手机端兼容</p>
|
||||
<button class="choose-button" type="button">选择方案三</button>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('[data-mockup]').forEach(function (mockup) {
|
||||
mockup.querySelectorAll('[data-type]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var outdoor = button.dataset.type === 'outdoor';
|
||||
mockup.classList.toggle('is-outdoor', outdoor);
|
||||
mockup.classList.toggle('is-online', !outdoor);
|
||||
mockup.querySelectorAll('[data-type]').forEach(function (item) {
|
||||
item.classList.toggle('active', item === button);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var selectionStatus = document.getElementById('selectionStatus');
|
||||
document.querySelectorAll('.choose-button').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var proposal = button.closest('.proposal');
|
||||
document.querySelectorAll('.proposal').forEach(function (item) {
|
||||
item.classList.toggle('is-chosen', item === proposal);
|
||||
item.querySelector('.choose-button').textContent = item === proposal ? '已选择' : '选择' + item.dataset.plan.match(/方案[一二三]/)[0];
|
||||
});
|
||||
selectionStatus.classList.add('has-choice');
|
||||
selectionStatus.querySelector('span').textContent = '已选择:' + proposal.dataset.plan;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.ruoyi.cms.controller.cms;
|
||||
|
||||
import com.ruoyi.cms.domain.vo.FairStatisticsFairItem;
|
||||
import com.ruoyi.cms.mapper.FairStatisticsMapper;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
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;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 后台招聘会数据统计。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/cms/fair-statistics")
|
||||
@Api(tags = "后台:招聘会数据统计")
|
||||
public class FairStatisticsController extends BaseController
|
||||
{
|
||||
private static final String ONLINE = "online";
|
||||
|
||||
private static final String OUTDOOR = "outdoor";
|
||||
|
||||
@Autowired
|
||||
private FairStatisticsMapper fairStatisticsMapper;
|
||||
|
||||
/**
|
||||
* 按单场招聘会或按招聘会起始时间范围统计。
|
||||
*
|
||||
* @param fairType 招聘会类型:online(线上)或 outdoor(户外)
|
||||
* @param fairId 单场招聘会 ID;传该参数时不接受时间范围
|
||||
* @param startDate 按时间统计的起始日期(含当天)
|
||||
* @param endDate 按时间统计的结束日期(含当天)
|
||||
*/
|
||||
@ApiOperation("招聘会数据汇总统计")
|
||||
@PreAuthorize("@ss.hasAnyPermi('cms:publicJobFair:list,cms:outdoorFair:list')")
|
||||
@GetMapping("/summary")
|
||||
public AjaxResult summary(@RequestParam String fairType,
|
||||
@RequestParam(required = false) String fairId,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate)
|
||||
{
|
||||
String normalizedType = fairType == null ? "" : fairType.trim().toLowerCase();
|
||||
if (!ONLINE.equals(normalizedType) && !OUTDOOR.equals(normalizedType))
|
||||
{
|
||||
return AjaxResult.error("招聘会类型仅支持线上招聘会或户外招聘会");
|
||||
}
|
||||
|
||||
boolean hasFairId = StringUtils.isNotEmpty(fairId);
|
||||
boolean hasStartDate = startDate != null;
|
||||
boolean hasEndDate = endDate != null;
|
||||
if (hasFairId && (hasStartDate || hasEndDate))
|
||||
{
|
||||
return AjaxResult.error("单场统计与时间范围统计不能同时使用");
|
||||
}
|
||||
if (!hasFairId && (!hasStartDate || !hasEndDate))
|
||||
{
|
||||
return AjaxResult.error("请选择一场招聘会,或完整选择起始日期和结束日期");
|
||||
}
|
||||
|
||||
Date rangeStart = null;
|
||||
Date rangeEnd = null;
|
||||
if (!hasFairId)
|
||||
{
|
||||
LocalDate start = toLocalDate(startDate);
|
||||
LocalDate end = toLocalDate(endDate);
|
||||
if (end.isBefore(start))
|
||||
{
|
||||
return AjaxResult.error("结束日期不能早于起始日期");
|
||||
}
|
||||
rangeStart = toStartOfDay(start);
|
||||
// SQL 使用左闭右开区间,确保结束日期当天的招聘会被计入。
|
||||
rangeEnd = toStartOfDay(end.plusDays(1));
|
||||
}
|
||||
|
||||
List<FairStatisticsFairItem> fairs;
|
||||
if (ONLINE.equals(normalizedType))
|
||||
{
|
||||
fairs = fairStatisticsMapper.selectOnlineFairStatistics(fairId, rangeStart, rangeEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
Long outdoorFairId = null;
|
||||
if (hasFairId)
|
||||
{
|
||||
try
|
||||
{
|
||||
outdoorFairId = Long.valueOf(fairId);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
return AjaxResult.error("户外招聘会 ID 格式不正确");
|
||||
}
|
||||
}
|
||||
fairs = fairStatisticsMapper.selectOutdoorFairStatistics(outdoorFairId, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
long companyCount = 0L;
|
||||
long jobCount = 0L;
|
||||
long demandCount = 0L;
|
||||
long interactionCount = 0L;
|
||||
for (FairStatisticsFairItem fair : fairs)
|
||||
{
|
||||
companyCount += numberOrZero(fair.getCompanyCount());
|
||||
jobCount += numberOrZero(fair.getJobCount());
|
||||
demandCount += numberOrZero(fair.getDemandCount());
|
||||
interactionCount += numberOrZero(fair.getInteractionCount());
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("fairType", normalizedType);
|
||||
result.put("fairCount", fairs.size());
|
||||
result.put("companyCount", companyCount);
|
||||
result.put("jobCount", jobCount);
|
||||
result.put("demandCount", demandCount);
|
||||
result.put("interactionCount", interactionCount);
|
||||
result.put("fairs", fairs);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Date value)
|
||||
{
|
||||
return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
private Date toStartOfDay(LocalDate value)
|
||||
{
|
||||
return Date.from(value.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
private long numberOrZero(Long value)
|
||||
{
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ruoyi.cms.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 招聘会数据统计中的单场汇总项。
|
||||
*/
|
||||
public class FairStatisticsFairItem implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String fairId;
|
||||
|
||||
private String fairTitle;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startTime;
|
||||
|
||||
private Long companyCount;
|
||||
|
||||
private Long jobCount;
|
||||
|
||||
private Long demandCount;
|
||||
|
||||
/** 线上为投递岗位数,户外为签到数。 */
|
||||
private Long interactionCount;
|
||||
|
||||
public String getFairId()
|
||||
{
|
||||
return fairId;
|
||||
}
|
||||
|
||||
public void setFairId(String fairId)
|
||||
{
|
||||
this.fairId = fairId;
|
||||
}
|
||||
|
||||
public String getFairTitle()
|
||||
{
|
||||
return fairTitle;
|
||||
}
|
||||
|
||||
public void setFairTitle(String fairTitle)
|
||||
{
|
||||
this.fairTitle = fairTitle;
|
||||
}
|
||||
|
||||
public Date getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(Date startTime)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public Long getCompanyCount()
|
||||
{
|
||||
return companyCount;
|
||||
}
|
||||
|
||||
public void setCompanyCount(Long companyCount)
|
||||
{
|
||||
this.companyCount = companyCount;
|
||||
}
|
||||
|
||||
public Long getJobCount()
|
||||
{
|
||||
return jobCount;
|
||||
}
|
||||
|
||||
public void setJobCount(Long jobCount)
|
||||
{
|
||||
this.jobCount = jobCount;
|
||||
}
|
||||
|
||||
public Long getDemandCount()
|
||||
{
|
||||
return demandCount;
|
||||
}
|
||||
|
||||
public void setDemandCount(Long demandCount)
|
||||
{
|
||||
this.demandCount = demandCount;
|
||||
}
|
||||
|
||||
public Long getInteractionCount()
|
||||
{
|
||||
return interactionCount;
|
||||
}
|
||||
|
||||
public void setInteractionCount(Long interactionCount)
|
||||
{
|
||||
this.interactionCount = interactionCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.cms.mapper;
|
||||
|
||||
import com.ruoyi.cms.domain.vo.FairStatisticsFairItem;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 招聘会数据统计查询。
|
||||
*/
|
||||
public interface FairStatisticsMapper
|
||||
{
|
||||
/** 查询线上招聘会的逐场统计。 */
|
||||
List<FairStatisticsFairItem> selectOnlineFairStatistics(@Param("fairId") String fairId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/** 查询户外招聘会的逐场统计。 */
|
||||
List<FairStatisticsFairItem> selectOutdoorFairStatistics(@Param("fairId") Long fairId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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.FairStatisticsMapper">
|
||||
|
||||
<!--
|
||||
线上招聘会:
|
||||
- 只统计招聘会类型为“1-线上”的有效公共招聘会;
|
||||
- 参会单位为审核通过的企业;
|
||||
- 职位、需求人数、投递岗位均限定为有效招聘会岗位;
|
||||
- vacancies 为空或非正数时按“若干”口径计为 1 人。
|
||||
-->
|
||||
<select id="selectOnlineFairStatistics" resultType="com.ruoyi.cms.domain.vo.FairStatisticsFairItem">
|
||||
SELECT
|
||||
f.job_fair_id AS "fairId",
|
||||
f.job_fair_title AS "fairTitle",
|
||||
f.job_fair_start_time AS "startTime",
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT company_relation.company_id)
|
||||
FROM public_job_fair_company company_relation
|
||||
WHERE company_relation.job_fair_id = f.job_fair_id
|
||||
AND company_relation.del_flag = '0'
|
||||
AND COALESCE(company_relation.review_status, '1') = '1'
|
||||
), 0) AS "companyCount",
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT fair_job.job_id)
|
||||
FROM public_job_fair_job fair_job
|
||||
INNER JOIN job job_info ON job_info.job_id = fair_job.job_id
|
||||
AND job_info.del_flag = '0'
|
||||
WHERE fair_job.job_fair_id = f.job_fair_id
|
||||
AND fair_job.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public_job_fair_company company_relation
|
||||
WHERE company_relation.job_fair_id = fair_job.job_fair_id
|
||||
AND company_relation.company_id = fair_job.company_id
|
||||
AND company_relation.del_flag = '0'
|
||||
AND COALESCE(company_relation.review_status, '1') = '1'
|
||||
)
|
||||
), 0) AS "jobCount",
|
||||
COALESCE((
|
||||
SELECT SUM(CASE WHEN job_info.vacancies IS NULL OR job_info.vacancies <= 0 THEN 1 ELSE job_info.vacancies END)
|
||||
FROM (
|
||||
SELECT DISTINCT fair_job.job_id
|
||||
FROM public_job_fair_job fair_job
|
||||
WHERE fair_job.job_fair_id = f.job_fair_id
|
||||
AND fair_job.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public_job_fair_company company_relation
|
||||
WHERE company_relation.job_fair_id = fair_job.job_fair_id
|
||||
AND company_relation.company_id = fair_job.company_id
|
||||
AND company_relation.del_flag = '0'
|
||||
AND COALESCE(company_relation.review_status, '1') = '1'
|
||||
)
|
||||
) fair_job
|
||||
INNER JOIN job job_info ON job_info.job_id = fair_job.job_id
|
||||
AND job_info.del_flag = '0'
|
||||
), 0) AS "demandCount",
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT fair_job.job_id)
|
||||
FROM public_job_fair_job fair_job
|
||||
INNER JOIN job job_info ON job_info.job_id = fair_job.job_id
|
||||
AND job_info.del_flag = '0'
|
||||
INNER JOIN job_apply apply_record ON apply_record.job_id = fair_job.job_id
|
||||
AND apply_record.del_flag = '0'
|
||||
WHERE fair_job.job_fair_id = f.job_fair_id
|
||||
AND fair_job.del_flag = '0'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public_job_fair_company company_relation
|
||||
WHERE company_relation.job_fair_id = fair_job.job_fair_id
|
||||
AND company_relation.company_id = fair_job.company_id
|
||||
AND company_relation.del_flag = '0'
|
||||
AND COALESCE(company_relation.review_status, '1') = '1'
|
||||
)
|
||||
), 0) AS "interactionCount"
|
||||
FROM public_job_fair f
|
||||
WHERE f.del_flag = '0'
|
||||
AND f.job_fair_type = '1'
|
||||
<choose>
|
||||
<when test="fairId != null and fairId != ''">
|
||||
AND f.job_fair_id = #{fairId}
|
||||
</when>
|
||||
<otherwise>
|
||||
AND f.job_fair_start_time <![CDATA[ >= ]]> #{startTime}
|
||||
AND f.job_fair_start_time <![CDATA[ < ]]> #{endTime}
|
||||
</otherwise>
|
||||
</choose>
|
||||
ORDER BY f.job_fair_start_time DESC NULLS LAST, f.job_fair_id DESC
|
||||
</select>
|
||||
|
||||
<!--
|
||||
户外招聘会没有投递入口,因此 interactionCount 只汇总签到人员记录。
|
||||
户外岗位也使用 job.vacancies;为空或非正数时按“若干=1 人”处理。
|
||||
-->
|
||||
<select id="selectOutdoorFairStatistics" resultType="com.ruoyi.cms.domain.vo.FairStatisticsFairItem">
|
||||
SELECT
|
||||
CAST(f.id AS VARCHAR) AS "fairId",
|
||||
f.title AS "fairTitle",
|
||||
f.hold_time AS "startTime",
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT company_relation.company_id)
|
||||
FROM fair_company company_relation
|
||||
WHERE company_relation.job_fair_id = f.id
|
||||
AND COALESCE(company_relation.del_flag, '0') = '0'
|
||||
), 0) AS "companyCount",
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT fair_job.job_id)
|
||||
FROM cms_outdoor_fair_job fair_job
|
||||
INNER JOIN job job_info ON job_info.job_id = fair_job.job_id
|
||||
AND job_info.del_flag = '0'
|
||||
WHERE fair_job.fair_id = f.id
|
||||
AND COALESCE(fair_job.del_flag, '0') = '0'
|
||||
), 0) AS "jobCount",
|
||||
COALESCE((
|
||||
SELECT SUM(CASE WHEN job_info.vacancies IS NULL OR job_info.vacancies <= 0 THEN 1 ELSE job_info.vacancies END)
|
||||
FROM (
|
||||
SELECT DISTINCT fair_job.job_id
|
||||
FROM cms_outdoor_fair_job fair_job
|
||||
WHERE fair_job.fair_id = f.id
|
||||
AND COALESCE(fair_job.del_flag, '0') = '0'
|
||||
) fair_job
|
||||
INNER JOIN job job_info ON job_info.job_id = fair_job.job_id
|
||||
AND job_info.del_flag = '0'
|
||||
), 0) AS "demandCount",
|
||||
COALESCE((
|
||||
SELECT COUNT(1)
|
||||
FROM cms_outdoor_fair_attendee attendee
|
||||
WHERE attendee.fair_id = f.id
|
||||
AND COALESCE(attendee.del_flag, '0') = '0'
|
||||
), 0) AS "interactionCount"
|
||||
FROM cms_outdoor_fair f
|
||||
WHERE COALESCE(f.del_flag, '0') = '0'
|
||||
<choose>
|
||||
<when test="fairId != null">
|
||||
AND f.id = #{fairId}
|
||||
</when>
|
||||
<otherwise>
|
||||
AND f.hold_time <![CDATA[ >= ]]> #{startTime}
|
||||
AND f.hold_time <![CDATA[ < ]]> #{endTime}
|
||||
</otherwise>
|
||||
</choose>
|
||||
ORDER BY f.hold_time DESC NULLS LAST, f.id DESC
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.cms.controller.cms;
|
||||
|
||||
import com.ruoyi.cms.domain.vo.FairStatisticsFairItem;
|
||||
import com.ruoyi.cms.mapper.FairStatisticsMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FairStatisticsControllerTest
|
||||
{
|
||||
@Test
|
||||
void onlineSingleFairAggregatesAllRequiredMetrics() throws Exception
|
||||
{
|
||||
FairStatisticsMapper mapper = mock(FairStatisticsMapper.class);
|
||||
when(mapper.selectOnlineFairStatistics(eq("online-100"), isNull(), isNull()))
|
||||
.thenReturn(Arrays.asList(
|
||||
item("online-100", 3L, 4L, 12L, 2L),
|
||||
item("online-101", 2L, 5L, 8L, 3L)));
|
||||
FairStatisticsController controller = controllerWith(mapper);
|
||||
|
||||
AjaxResult result = controller.summary("online", "online-100", null, null);
|
||||
|
||||
assertEquals(200, result.get(AjaxResult.CODE_TAG));
|
||||
Map<String, Object> data = dataOf(result);
|
||||
assertEquals("online", data.get("fairType"));
|
||||
assertEquals(2, data.get("fairCount"));
|
||||
assertEquals(5L, data.get("companyCount"));
|
||||
assertEquals(9L, data.get("jobCount"));
|
||||
assertEquals(20L, data.get("demandCount"));
|
||||
assertEquals(5L, data.get("interactionCount"));
|
||||
verify(mapper).selectOnlineFairStatistics("online-100", null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void outdoorSingleFairRejectsNonNumericIdBeforeQuerying() throws Exception
|
||||
{
|
||||
FairStatisticsMapper mapper = mock(FairStatisticsMapper.class);
|
||||
FairStatisticsController controller = controllerWith(mapper);
|
||||
|
||||
AjaxResult result = controller.summary("outdoor", "not-a-number", null, null);
|
||||
|
||||
assertEquals(500, result.get(AjaxResult.CODE_TAG));
|
||||
assertEquals("户外招聘会 ID 格式不正确", result.get(AjaxResult.MSG_TAG));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateRangeUsesOutdoorMapperAndReturnsEmptyTotals() throws Exception
|
||||
{
|
||||
FairStatisticsMapper mapper = mock(FairStatisticsMapper.class);
|
||||
when(mapper.selectOutdoorFairStatistics(isNull(), any(Date.class), any(Date.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
FairStatisticsController controller = controllerWith(mapper);
|
||||
|
||||
AjaxResult result = controller.summary("outdoor", null, new Date(1_700_000_000_000L), new Date(1_700_086_400_000L));
|
||||
|
||||
assertEquals(200, result.get(AjaxResult.CODE_TAG));
|
||||
Map<String, Object> data = dataOf(result);
|
||||
assertEquals("outdoor", data.get("fairType"));
|
||||
assertEquals(0, data.get("fairCount"));
|
||||
assertEquals(0L, data.get("companyCount"));
|
||||
assertEquals(0L, data.get("interactionCount"));
|
||||
verify(mapper).selectOutdoorFairStatistics(isNull(), any(Date.class), any(Date.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void incompleteScopeReturnsValidationError() throws Exception
|
||||
{
|
||||
FairStatisticsController controller = controllerWith(mock(FairStatisticsMapper.class));
|
||||
|
||||
AjaxResult result = controller.summary("online", null, new Date(), null);
|
||||
|
||||
assertEquals(500, result.get(AjaxResult.CODE_TAG));
|
||||
assertTrue(String.valueOf(result.get(AjaxResult.MSG_TAG)).contains("完整选择"));
|
||||
}
|
||||
|
||||
private FairStatisticsFairItem item(String fairId, Long companyCount, Long jobCount,
|
||||
Long demandCount, Long interactionCount)
|
||||
{
|
||||
FairStatisticsFairItem item = new FairStatisticsFairItem();
|
||||
item.setFairId(fairId);
|
||||
item.setCompanyCount(companyCount);
|
||||
item.setJobCount(jobCount);
|
||||
item.setDemandCount(demandCount);
|
||||
item.setInteractionCount(interactionCount);
|
||||
return item;
|
||||
}
|
||||
|
||||
private FairStatisticsController controllerWith(FairStatisticsMapper mapper) throws Exception
|
||||
{
|
||||
FairStatisticsController controller = new FairStatisticsController();
|
||||
Field field = FairStatisticsController.class.getDeclaredField("fairStatisticsMapper");
|
||||
field.setAccessible(true);
|
||||
field.set(controller, mapper);
|
||||
return controller;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> dataOf(AjaxResult result)
|
||||
{
|
||||
return (Map<String, Object>) result.get(AjaxResult.DATA_TAG);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user