This commit is contained in:
冯辉
2026-06-27 23:03:40 +08:00
30 changed files with 1971 additions and 50 deletions

View File

@@ -0,0 +1,65 @@
# 户外招聘会企业 H5 公共接口
基础路径:`/app/outdoor-fair`
## 1. 扫码进入企业 H5
**请求方式:** `GET`
**请求路径:** `/company-page`
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceCode | String | 是 | 后台设备码绑定企业后生成的扫码参数 |
**响应字段:**
| 字段 | 说明 |
| --- | --- |
| fair | 户外招聘会信息 |
| company | 企业信息 |
| booth | 企业展位信息,未分配展位时为空 |
| jobs | 当前企业在本场招聘会下的岗位列表 |
| fairH5Url | 本场招聘会 H5 链接 |
| companyH5Url | 当前企业 H5 链接 |
| qrCodeContent | 二维码内容,前端用该值生成二维码 |
| deviceId | 绑定设备码 ID |
| deviceCode | 绑定设备码 |
## 2. 通过招聘会和企业获取 H5 数据
**请求方式:** `GET`
**请求路径:** `/{fairId}/companies/{companyId}/page`
用于未绑定设备码或后台预览企业 H5 页面。
## 3. 获取招聘会公共详情
**请求方式:** `GET`
**请求路径:** `/{fairId}`
返回 `fair``fairH5Url`
## 4. 后台参会企业二维码入口
后台接口 `GET /cms/outdoor-fair/{fairId}/companies` 的每行企业现在额外返回:
| 字段 | 说明 |
| --- | --- |
| deviceId | 已绑定设备码 ID未绑定为空 |
| deviceCode | 已绑定设备码,未绑定为空 |
| h5Url | 企业 H5 链接,格式为 `/h5/outdoor-fair/company.html?fairId={fairId}&companyId={companyId}` |
| qrCodeContent | 二维码内容,前端点击“查看二维码”时用该值生成二维码 |
也可以调用 `GET /cms/outdoor-fair/{fairId}/companies/{companyId}/qrcode` 单独获取某个参会企业的二维码内容。
## 5. 已提供的静态 H5
| 页面 | 说明 |
| --- | --- |
| `/h5/outdoor-fair/company.html?fairId={fairId}&companyId={companyId}` | 扫码后的参会企业公共页 |
| `/h5/outdoor-fair/detail.html?fairId={fairId}` | 招聘会公共详情页 |

View File

@@ -20,12 +20,16 @@ spring:
driverClassName: com.highgo.jdbc.Driver
druid:
# 主库数据源
# master:
# url: jdbc:highgo://124.243.245.42:5866/shz?useUnicode=true&characterEncoding=utf8&currentSchema=shz&stringtype=unspecified
# #username: syssso
# username: sysdba
# password: ZKR2024@comzkr
master:
url: jdbc:highgo://39.98.44.136:6023/highgo?useUnicode=true&characterEncoding=utf8&currentSchema=shz&stringtype=unspecified
#username: syssso
username: sysdba
password: Hello@2026
# 从库数据源
slave:
# 从数据源开关/默认关闭
@@ -86,7 +90,7 @@ spring:
# 密码
password: ZKR2024@@.com
# 连接超时时间
timeout: 10s
timeout: 100s
lettuce:
pool:
# 连接池中的最小空闲连接

View File

@@ -0,0 +1,299 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>参会企业</title>
<style>
:root {
color: #172033;
background: #f4f7fb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
body { margin: 0; background: #f4f7fb; }
.page { min-height: 100vh; padding: 18px 14px 28px; }
.hero {
padding: 22px 18px;
color: #fff;
background: linear-gradient(135deg, #1f6feb, #14866d);
border-radius: 8px;
box-shadow: 0 10px 28px rgba(31, 111, 235, .18);
}
.hero h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.25; letter-spacing: 0; }
.hero p { margin: 4px 0 0; font-size: 14px; line-height: 1.7; opacity: .92; }
.section { margin-top: 14px; padding: 16px; background: #fff; border-radius: 8px; box-shadow: 0 8px 22px rgba(23, 32, 51, .06); }
.section h2 { margin: 0 0 12px; font-size: 17px; line-height: 1.4; letter-spacing: 0; }
.meta { display: grid; gap: 8px; }
.meta div { display: flex; gap: 10px; font-size: 14px; line-height: 1.55; }
.meta span:first-child { flex: 0 0 72px; color: #667085; }
.meta span:last-child { min-width: 0; flex: 1; word-break: break-word; }
.desc { margin: 0; color: #344054; font-size: 14px; line-height: 1.75; white-space: pre-wrap; }
.jobs { display: grid; gap: 10px; }
.job { padding: 12px; border: 1px solid #e5eaf2; border-radius: 8px; background: #fbfcff; }
.job h3 { margin: 0 0 8px; font-size: 16px; line-height: 1.35; letter-spacing: 0; }
.job p { margin: 3px 0; color: #475467; font-size: 13px; line-height: 1.55; }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
.btn {
display: flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 10px 14px;
color: #fff;
background: #1f6feb;
border: 0;
border-radius: 8px;
font-size: 15px;
text-decoration: none;
}
.btn.secondary { color: #1f6feb; background: #eaf2ff; }
.btn.checkin { background: #14866d; }
.empty, .loading, .error { padding: 28px 16px; color: #667085; text-align: center; font-size: 14px; line-height: 1.7; }
.error { color: #b42318; }
/* 签到弹窗 */
.mask { position: fixed; inset: 0; background: rgba(23,32,51,.45); display: none; z-index: 50; }
.mask.show { display: block; }
.dialog {
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: calc(100vw - 56px); max-width: 380px; background: #fff;
border-radius: 10px; padding: 18px 16px; z-index: 51; display: none;
box-shadow: 0 18px 48px rgba(23,32,51,.22);
}
.dialog.show { display: block; }
.dialog h3 { margin: 0 0 4px; font-size: 17px; }
.dialog .sub { margin: 0 0 14px; font-size: 13px; color: #667085; }
.field { margin-bottom: 12px; }
.field label { display: block; margin-bottom: 6px; font-size: 14px; color: #344054; }
.field input {
width: 100%; min-height: 42px; padding: 8px 12px; border: 1px solid #d0d5dd;
border-radius: 8px; font-size: 15px; background: #fff; box-sizing: border-box;
}
.field input:focus { outline: none; border-color: #1f6feb; }
.dialog-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 4px; }
.btn.ghost { color: #475467; background: #f2f4f7; }
.toast {
position: fixed; left: 50%; top: 24px; transform: translateX(-50%);
padding: 10px 16px; border-radius: 8px; font-size: 14px; color: #fff;
background: #14866d; z-index: 60; display: none; max-width: 80vw; text-align: center;
}
.toast.error { background: #b42318; }
.toast.show { display: block; }
</style>
</head>
<body>
<main class="page" id="app">
<div class="loading">正在加载...</div>
</main>
<script>
(function () {
var app = document.getElementById('app');
var params = new URLSearchParams(window.location.search);
var deviceCode = params.get('deviceCode');
var fairId = params.get('fairId');
var companyId = params.get('companyId');
var pageFairId = null;
function text(value) {
return value === null || value === undefined || value === '' ? '暂无' : String(value);
}
function escapeHtml(value) {
return text(value).replace(/[&<>"']/g, function (ch) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch];
});
}
function apiUrl() {
if (deviceCode) {
return '/app/outdoor-fair/company-page?deviceCode=' + encodeURIComponent(deviceCode);
}
return '/app/outdoor-fair/' + encodeURIComponent(fairId) + '/companies/' + encodeURIComponent(companyId) + '/page';
}
function fetchJson(url) {
return fetch(url).then(function (res) {
return res.json();
});
}
function fetchDict(dictType) {
return fetchJson('/app/common/dict/' + encodeURIComponent(dictType)).then(function (body) {
if (body.code !== 200 || !Array.isArray(body.data)) {
return {};
}
return body.data.reduce(function (map, item) {
if (item && item.dictValue !== undefined && item.dictValue !== null) {
map[String(item.dictValue)] = item.dictLabel;
}
return map;
}, {});
}).catch(function () {
return {};
});
}
function dictLabel(dictMap, value) {
if (value === null || value === undefined || value === '') {
return '暂无';
}
return dictMap[String(value)] || String(value);
}
function render(data, dicts) {
var fair = data.fair || {};
var company = data.company || {};
var booth = data.booth || {};
var jobs = data.jobs || [];
var educationDict = dicts.education || {};
var experienceDict = dicts.experience || {};
pageFairId = (fair.id !== null && fair.id !== undefined) ? fair.id : fairId;
document.title = text(company.name);
app.innerHTML =
'<section class="hero">' +
'<h1>' + escapeHtml(company.name) + '</h1>' +
'<p>' + escapeHtml(fair.title) + '</p>' +
'<p>' + escapeHtml(fair.address) + '</p>' +
'</section>' +
'<div class="actions">' +
'<button type="button" class="btn checkin" id="checkinBtn">签到</button>' +
'<a class="btn" href="' + escapeHtml(data.fairH5Url) + '">查看招聘会</a>' +
'</div>' +
'<section class="section">' +
'<h2>企业信息</h2>' +
'<div class="meta">' +
'<div><span>行业</span><span>' + escapeHtml(company.industry) + '</span></div>' +
'<div><span>规模</span><span>' + escapeHtml(company.scale) + '</span></div>' +
'<div><span>性质</span><span>' + escapeHtml(company.nature) + '</span></div>' +
'<div><span>地址</span><span>' + escapeHtml(company.location || company.registeredAddress) + '</span></div>' +
'<div><span>联系人</span><span>' + escapeHtml(company.contactPerson) + '</span></div>' +
'<div><span>联系电话</span><span>' + escapeHtml(company.contactPersonPhone) + '</span></div>' +
'<div><span>展位</span><span>' + escapeHtml(booth.boothNumber) + '</span></div>' +
'</div>' +
'</section>' +
'<section class="section">' +
'<h2>企业介绍</h2>' +
'<p class="desc">' + escapeHtml(company.description) + '</p>' +
'</section>' +
'<section class="section">' +
'<h2>招聘岗位</h2>' +
(jobs.length ? '<div class="jobs">' + jobs.map(function (job) {
return renderJob(job, educationDict, experienceDict);
}).join('') + '</div>' : '<div class="empty">暂无岗位</div>') +
'</section>' +
'<div class="mask" id="checkinMask"></div>' +
'<div class="dialog" id="checkinDialog">' +
'<h3>签到</h3>' +
'<p class="sub">请填写您的姓名和手机号完成签到</p>' +
'<div class="field">' +
'<label for="checkinName">姓名</label>' +
'<input type="text" id="checkinName" placeholder="请输入姓名" autocomplete="name">' +
'</div>' +
'<div class="field">' +
'<label for="checkinPhone">手机号</label>' +
'<input type="tel" id="checkinPhone" placeholder="请输入手机号" autocomplete="tel" maxlength="11">' +
'</div>' +
'<div class="dialog-actions">' +
'<button type="button" class="btn ghost" id="checkinCancel">取消</button>' +
'<button type="button" class="btn checkin" id="checkinSubmit">提交签到</button>' +
'</div>' +
'</div>' +
'<div class="toast" id="checkinToast"></div>';
bindCheckIn();
}
function showToast(msg, isError) {
var toast = document.getElementById('checkinToast');
if (!toast) return;
toast.textContent = msg;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(function () { toast.className = 'toast' + (isError ? ' error' : ''); }, 2400);
}
function openCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask && dialog) {
mask.className = 'mask show';
dialog.className = 'dialog show';
}
}
function closeCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask) mask.className = 'mask';
if (dialog) dialog.className = 'dialog';
}
function bindCheckIn() {
var openBtn = document.getElementById('checkinBtn');
var cancelBtn = document.getElementById('checkinCancel');
var mask = document.getElementById('checkinMask');
var submitBtn = document.getElementById('checkinSubmit');
if (openBtn) openBtn.onclick = openCheckIn;
if (cancelBtn) cancelBtn.onclick = closeCheckIn;
if (mask) mask.onclick = closeCheckIn;
if (submitBtn) submitBtn.onclick = submitCheckIn;
}
function submitCheckIn() {
var name = (document.getElementById('checkinName') || {}).value;
var phone = (document.getElementById('checkinPhone') || {}).value;
if (!name || !name.trim()) { showToast('请输入姓名', true); return; }
if (!phone || !/^\d{11}$/.test(phone.trim())) { showToast('请输入正确的11位手机号', true); return; }
if (!pageFairId) { showToast('缺少招聘会信息', true); return; }
var submitBtn = document.getElementById('checkinSubmit');
if (submitBtn) submitBtn.disabled = true;
fetch('/app/outdoor-fair/check-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fairId: Number(pageFairId), name: name.trim(), phone: phone.trim() })
}).then(function (res) { return res.json(); }).then(function (body) {
if (submitBtn) submitBtn.disabled = false;
if (body && body.code === 200) {
closeCheckIn();
showToast('签到成功');
} else {
showToast((body && body.msg) || '签到失败', true);
}
}).catch(function () {
if (submitBtn) submitBtn.disabled = false;
showToast('网络异常,请重试', true);
});
}
function renderJob(job, educationDict, experienceDict) {
var salary = job.minSalary || job.maxSalary ? text(job.minSalary) + '-' + text(job.maxSalary) + ' 元' : '薪资面议';
return '<article class="job">' +
'<h3>' + escapeHtml(job.jobTitle) + '</h3>' +
'<p>' + escapeHtml(salary) + ' · ' + escapeHtml(dictLabel(educationDict, job.education)) + ' · ' + escapeHtml(dictLabel(experienceDict, job.experience)) + '</p>' +
'<p>招聘人数:' + escapeHtml(job.vacancies) + '</p>' +
'<p>工作地点:' + escapeHtml(job.jobAddress || job.jobLocation) + '</p>' +
'</article>';
}
if (!deviceCode && (!fairId || !companyId)) {
app.innerHTML = '<div class="error">缺少页面参数</div>';
return;
}
Promise.all([
fetchJson(apiUrl()),
fetchDict('education'),
fetchDict('experience')
]).then(function (results) {
var body = results[0];
if (body.code !== 200) {
throw new Error(body.msg || '加载失败');
}
render(body.data || {}, {
education: results[1],
experience: results[2]
});
}).catch(function (err) {
app.innerHTML = '<div class="error">' + escapeHtml(err.message) + '</div>';
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,208 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>户外招聘会</title>
<style>
:root {
color: #172033;
background: #f4f7fb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
body { margin: 0; background: #f4f7fb; }
.page { min-height: 100vh; padding: 18px 14px 28px; }
.hero {
padding: 22px 18px;
color: #fff;
background: linear-gradient(135deg, #1f6feb, #14866d);
border-radius: 8px;
box-shadow: 0 10px 28px rgba(31, 111, 235, .18);
}
.hero h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.25; letter-spacing: 0; }
.hero p { margin: 4px 0 0; font-size: 14px; line-height: 1.7; opacity: .92; }
.section { margin-top: 14px; padding: 16px; background: #fff; border-radius: 8px; box-shadow: 0 8px 22px rgba(23, 32, 51, .06); }
.section h2 { margin: 0 0 12px; font-size: 17px; line-height: 1.4; letter-spacing: 0; }
.meta { display: grid; gap: 8px; }
.meta div { display: flex; gap: 10px; font-size: 14px; line-height: 1.55; }
.meta span:first-child { flex: 0 0 86px; color: #667085; }
.meta span:last-child { min-width: 0; flex: 1; word-break: break-word; }
.photo { width: 100%; margin-top: 14px; border-radius: 8px; object-fit: cover; }
.loading, .error { padding: 28px 16px; color: #667085; text-align: center; font-size: 14px; line-height: 1.7; }
.error { color: #b42318; }
.actions { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 14px; }
.btn {
display: flex; align-items: center; justify-content: center;
min-height: 44px; padding: 10px 14px; color: #fff; background: #1f6feb;
border: 0; border-radius: 8px; font-size: 15px; text-decoration: none; width: 100%;
}
.btn.checkin { background: #14866d; }
/* 签到弹窗 */
.mask { position: fixed; inset: 0; background: rgba(23,32,51,.45); display: none; z-index: 50; }
.mask.show { display: block; }
.dialog {
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: calc(100vw - 56px); max-width: 380px; background: #fff;
border-radius: 10px; padding: 18px 16px; z-index: 51; display: none;
box-shadow: 0 18px 48px rgba(23,32,51,.22);
}
.dialog.show { display: block; }
.dialog h3 { margin: 0 0 4px; font-size: 17px; }
.dialog .sub { margin: 0 0 14px; font-size: 13px; color: #667085; }
.field { margin-bottom: 12px; }
.field label { display: block; margin-bottom: 6px; font-size: 14px; color: #344054; }
.field input {
width: 100%; min-height: 42px; padding: 8px 12px; border: 1px solid #d0d5dd;
border-radius: 8px; font-size: 15px; background: #fff; box-sizing: border-box;
}
.field input:focus { outline: none; border-color: #1f6feb; }
.dialog-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 4px; }
.btn.ghost { color: #475467; background: #f2f4f7; }
.toast {
position: fixed; left: 50%; top: 24px; transform: translateX(-50%);
padding: 10px 16px; border-radius: 8px; font-size: 14px; color: #fff;
background: #14866d; z-index: 60; display: none; max-width: 80vw; text-align: center;
}
.toast.error { background: #b42318; }
.toast.show { display: block; }
</style>
</head>
<body>
<main class="page" id="app">
<div class="loading">正在加载...</div>
</main>
<script>
(function () {
var app = document.getElementById('app');
var fairId = new URLSearchParams(window.location.search).get('fairId');
var pageFairId = null;
function text(value) {
return value === null || value === undefined || value === '' ? '暂无' : String(value);
}
function escapeHtml(value) {
return text(value).replace(/[&<>"']/g, function (ch) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch];
});
}
function render(data) {
var fair = data.fair || {};
pageFairId = (fair.id !== null && fair.id !== undefined) ? fair.id : fairId;
document.title = text(fair.title);
app.innerHTML =
'<section class="hero">' +
'<h1>' + escapeHtml(fair.title) + '</h1>' +
'<p>' + escapeHtml(fair.hostUnit) + '</p>' +
'<p>' + escapeHtml(fair.address) + '</p>' +
'</section>' +
'<div class="actions">' +
'<button type="button" class="btn checkin" id="checkinBtn">签到</button>' +
'</div>' +
'<section class="section">' +
'<h2>招聘会信息</h2>' +
'<div class="meta">' +
'<div><span>举办时间</span><span>' + escapeHtml(fair.holdTime) + '</span></div>' +
'<div><span>截止时间</span><span>' + escapeHtml(fair.endTime) + '</span></div>' +
'<div><span>招聘会类型</span><span>' + escapeHtml(fair.fairType) + '</span></div>' +
'<div><span>举办区域</span><span>' + escapeHtml(fair.region) + '</span></div>' +
'<div><span>举办场地</span><span>' + escapeHtml(fair.venueName) + '</span></div>' +
'<div><span>展位数量</span><span>' + escapeHtml(fair.boothCount) + '</span></div>' +
'</div>' +
(fair.photoUrl ? '<img class="photo" src="' + escapeHtml(fair.photoUrl) + '" alt="招聘会照片">' : '') +
'</section>' +
'<div class="mask" id="checkinMask"></div>' +
'<div class="dialog" id="checkinDialog">' +
'<h3>签到</h3>' +
'<p class="sub">请填写您的姓名和手机号完成签到</p>' +
'<div class="field">' +
'<label for="checkinName">姓名</label>' +
'<input type="text" id="checkinName" placeholder="请输入姓名" autocomplete="name">' +
'</div>' +
'<div class="field">' +
'<label for="checkinPhone">手机号</label>' +
'<input type="tel" id="checkinPhone" placeholder="请输入手机号" autocomplete="tel" maxlength="11">' +
'</div>' +
'<div class="dialog-actions">' +
'<button type="button" class="btn ghost" id="checkinCancel">取消</button>' +
'<button type="button" class="btn checkin" id="checkinSubmit">提交签到</button>' +
'</div>' +
'</div>' +
'<div class="toast" id="checkinToast"></div>';
bindCheckIn();
}
function showToast(msg, isError) {
var toast = document.getElementById('checkinToast');
if (!toast) return;
toast.textContent = msg;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(function () { toast.className = 'toast' + (isError ? ' error' : ''); }, 2400);
}
function openCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask && dialog) { mask.className = 'mask show'; dialog.className = 'dialog show'; }
}
function closeCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask) mask.className = 'mask';
if (dialog) dialog.className = 'dialog';
}
function bindCheckIn() {
var openBtn = document.getElementById('checkinBtn');
var cancelBtn = document.getElementById('checkinCancel');
var mask = document.getElementById('checkinMask');
var submitBtn = document.getElementById('checkinSubmit');
if (openBtn) openBtn.onclick = openCheckIn;
if (cancelBtn) cancelBtn.onclick = closeCheckIn;
if (mask) mask.onclick = closeCheckIn;
if (submitBtn) submitBtn.onclick = submitCheckIn;
}
function submitCheckIn() {
var name = (document.getElementById('checkinName') || {}).value;
var phone = (document.getElementById('checkinPhone') || {}).value;
if (!name || !name.trim()) { showToast('请输入姓名', true); return; }
if (!phone || !/^\d{11}$/.test(phone.trim())) { showToast('请输入正确的11位手机号', true); return; }
if (!pageFairId) { showToast('缺少招聘会信息', true); return; }
var submitBtn = document.getElementById('checkinSubmit');
if (submitBtn) submitBtn.disabled = true;
fetch('/app/outdoor-fair/check-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fairId: Number(pageFairId), name: name.trim(), phone: phone.trim() })
}).then(function (res) { return res.json(); }).then(function (body) {
if (submitBtn) submitBtn.disabled = false;
if (body && body.code === 200) { closeCheckIn(); showToast('签到成功'); }
else { showToast((body && body.msg) || '签到失败', true); }
}).catch(function () {
if (submitBtn) submitBtn.disabled = false;
showToast('网络异常,请重试', true);
});
}
if (!fairId) {
app.innerHTML = '<div class="error">缺少招聘会参数</div>';
return;
}
fetch('/app/outdoor-fair/' + encodeURIComponent(fairId)).then(function (res) {
return res.json();
}).then(function (body) {
if (body.code !== 200) {
throw new Error(body.msg || '加载失败');
}
render(body.data || {});
}).catch(function (err) {
app.innerHTML = '<div class="error">' + escapeHtml(err.message) + '</div>';
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,398 @@
package com.ruoyi.cms.controller.app;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.cms.domain.FairCompany;
import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.domain.rc.PublicJobFair;
import com.ruoyi.cms.domain.rc.PublicJobFairQuery;
import com.ruoyi.cms.domain.rc.PublicJobFairResponse;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.common.annotation.BussinessLog;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.Company;
import com.ruoyi.common.exception.ServiceException;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 移动端:户外招聘会公共 H5 接口。
*/
@RestController
@RequestMapping("/app/outdoor-fair")
@Api(tags = "移动端户外招聘会公共H5")
public class AppOutdoorFairController extends BaseController
{
private static final String DAY_PATTERN = "yyyy-MM-dd";
@Autowired
private IOutdoorFairService outdoorFairService;
@Autowired
private CompanyMapper companyMapper;
@Autowired
private FairCompanyMapper fairCompanyMapper;
@Autowired
private OutdoorFairDeviceMapper outdoorFairDeviceMapper;
@Autowired
private OutdoorFairBoothBookingMapper outdoorFairBoothBookingMapper;
@Autowired
private OutdoorFairJobMapper outdoorFairJobMapper;
@Autowired
private IOutdoorFairAttendeeService outdoorFairAttendeeService;
@BussinessLog(title = "户外招聘会列表")
@ApiOperation("分页查询户外招聘会列表")
@GetMapping("/page")
public PublicJobFairResponse page(PublicJobFairQuery query)
{
if (query == null) {
query = new PublicJobFairQuery();
}
int pageNum = query.getPageNum() == null || query.getPageNum() < 1 ? 1 : query.getPageNum();
int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
Page<OutdoorFair> page = outdoorFairService.page(new Page<>(pageNum, pageSize), buildOutdoorFairWrapper(query));
PublicJobFairResponse response = new PublicJobFairResponse();
response.setCode(200);
response.setMsg("查询成功");
response.setData(buildPageData(page, pageNum, pageSize));
return response;
}
@BussinessLog(title = "查看当季度户外招聘会信息")
@ApiOperation("查看当季度户外招聘会信息")
@GetMapping("/currentQuarter")
public AjaxResult currentQuarterFairs()
{
Calendar calendar = Calendar.getInstance();
int currentMonth = calendar.get(Calendar.MONTH);
int quarterStartMonth = currentMonth / 3 * 3;
calendar.set(Calendar.MONTH, quarterStartMonth);
calendar.set(Calendar.DAY_OF_MONTH, 1);
setStartOfDay(calendar);
Date startTime = calendar.getTime();
calendar.add(Calendar.MONTH, 3);
Date endTime = calendar.getTime();
return success(listOutdoorFairsByRange(startTime, endTime));
}
@BussinessLog(title = "查看当月户外招聘会信息")
@ApiOperation("查看当月户外招聘会信息")
@GetMapping("/currentMonth")
public AjaxResult currentMonthFairs()
{
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_MONTH, 1);
setStartOfDay(calendar);
Date startTime = calendar.getTime();
calendar.add(Calendar.MONTH, 1);
Date endTime = calendar.getTime();
return success(listOutdoorFairsByRange(startTime, endTime));
}
@BussinessLog(title = "查询有户外招聘会的日期列表")
@ApiOperation("查询有户外招聘会的日期列表")
@GetMapping("/dates")
public AjaxResult dates()
{
List<String> dates = outdoorFairService.list(new LambdaQueryWrapper<OutdoorFair>()
.isNotNull(OutdoorFair::getHoldTime)
.orderByAsc(OutdoorFair::getHoldTime))
.stream()
.map(OutdoorFair::getHoldTime)
.map(AppOutdoorFairController::formatDay)
.distinct()
.collect(Collectors.toList());
return success(dates);
}
@BussinessLog(title = "户外招聘会企业H5详情")
@ApiOperation("通过设备码获取户外招聘会企业H5详情")
@GetMapping("/company-page")
public AjaxResult companyPageByDeviceCode(@RequestParam String deviceCode, HttpServletRequest request)
{
if (StringUtils.isBlank(deviceCode)) {
return AjaxResult.error("设备码不能为空");
}
OutdoorFairDevice device = outdoorFairDeviceMapper.selectOne(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getDeviceCode, deviceCode)
.last("LIMIT 1"));
if (device == null || device.getCompanyId() == null) {
return AjaxResult.error("设备码未绑定参会企业");
}
return success(buildCompanyPage(device.getFairId(), device.getCompanyId(), device, request));
}
@BussinessLog(title = "户外招聘会企业H5详情")
@ApiOperation("通过招聘会ID和企业ID获取户外招聘会企业H5详情")
@GetMapping("/{fairId}/companies/{companyId}/page")
public AjaxResult companyPage(@PathVariable Long fairId, @PathVariable Long companyId,
HttpServletRequest request)
{
OutdoorFairDevice device = outdoorFairDeviceMapper.selectOne(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getFairId, fairId)
.eq(OutdoorFairDevice::getCompanyId, companyId)
.last("LIMIT 1"));
return success(buildCompanyPage(fairId, companyId, device, request));
}
@BussinessLog(title = "户外招聘会详情")
@ApiOperation("获取户外招聘会公共详情")
@GetMapping("/{fairId}")
public AjaxResult detail(@PathVariable Long fairId, HttpServletRequest request)
{
OutdoorFair fair = outdoorFairService.getById(fairId);
if (fair == null) {
return AjaxResult.error("招聘会不存在");
}
Map<String, Object> data = new HashMap<>();
data.put("fair", fair);
data.put("fairH5Url", buildFairH5Url(request, fairId));
return success(data);
}
/**
* 匿名签到(姓名+手机号。H5 企业页调用,无需登录。
*/
@BussinessLog(title = "户外招聘会企业H5签到")
@ApiOperation("匿名签到(姓名+手机号)")
@PostMapping("/check-in")
public AjaxResult checkIn(@RequestBody OutdoorFairAttendee attendee)
{
if (attendee.getFairId() == null) {
return AjaxResult.error("缺少招聘会参数");
}
if (StringUtils.isBlank(attendee.getName())) {
return AjaxResult.error("姓名不能为空");
}
if (StringUtils.isBlank(attendee.getPhone())) {
return AjaxResult.error("手机号不能为空");
}
// 同一招聘会下同手机号已签到则不重复记录
Long exists = outdoorFairAttendeeService.count(new LambdaQueryWrapper<OutdoorFairAttendee>()
.eq(OutdoorFairAttendee::getFairId, attendee.getFairId())
.eq(OutdoorFairAttendee::getPhone, attendee.getPhone()));
if (exists != null && exists > 0) {
return AjaxResult.error("该手机号已签到");
}
attendee.setCheckInTime(new Date());
return toAjax(outdoorFairAttendeeService.save(attendee));
}
private Map<String, Object> buildCompanyPage(Long fairId, Long companyId, OutdoorFairDevice device,
HttpServletRequest request)
{
OutdoorFair fair = outdoorFairService.getById(fairId);
if (fair == null) {
throw new ServiceException("招聘会不存在");
}
Company company = companyMapper.selectById(companyId);
if (company == null) {
throw new ServiceException("企业不存在");
}
Long relationCount = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, companyId));
if (relationCount == null || relationCount == 0) {
throw new ServiceException("该企业未参加当前招聘会");
}
OutdoorFairBoothBooking booking = outdoorFairBoothBookingMapper.selectOne(
new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId)
.eq(OutdoorFairBoothBooking::getCompanyId, companyId)
.eq(OutdoorFairBoothBooking::getStatus, "reserved")
.last("LIMIT 1"));
List<Job> jobs = outdoorFairJobMapper.selectFairJobPage(
fairId, companyId, null, null, null, null, 0, 200);
String companyH5Url = buildCompanyH5Url(request, fairId, companyId);
Map<String, Object> data = new HashMap<>();
data.put("fair", fair);
data.put("company", company);
data.put("booth", booking);
data.put("jobs", jobs);
data.put("fairH5Url", buildFairH5Url(request, fairId));
data.put("companyH5Url", companyH5Url);
data.put("qrCodeContent", companyH5Url);
data.put("deviceId", device == null ? null : device.getId());
data.put("deviceCode", device == null ? null : device.getDeviceCode());
return data;
}
private String buildCompanyH5Url(HttpServletRequest request, Long fairId, Long companyId)
{
String baseUrl = buildBaseUrl(request);
return baseUrl + "/h5/outdoor-fair/company.html?fairId=" + fairId + "&companyId=" + companyId;
}
private String buildFairH5Url(HttpServletRequest request, Long fairId)
{
return buildBaseUrl(request) + "/h5/outdoor-fair/detail.html?fairId=" + fairId;
}
private LambdaQueryWrapper<OutdoorFair> buildOutdoorFairWrapper(PublicJobFairQuery query)
{
LambdaQueryWrapper<OutdoorFair> wrapper = new LambdaQueryWrapper<>();
if (query != null && StringUtils.isNotBlank(query.getJobFairTitle())) {
wrapper.like(OutdoorFair::getTitle, query.getJobFairTitle());
}
if (query != null && StringUtils.isNotBlank(query.getZphjbsj())) {
Date[] dayRange = parseDayRange(query.getZphjbsj());
if (dayRange != null) {
wrapper.lt(OutdoorFair::getHoldTime, dayRange[1])
.and(w -> w.isNull(OutdoorFair::getEndTime)
.or()
.ge(OutdoorFair::getEndTime, dayRange[0]));
}
}
wrapper.orderByDesc(OutdoorFair::getHoldTime)
.orderByDesc(OutdoorFair::getCreateTime);
return wrapper;
}
private PublicJobFairResponse.PageData buildPageData(Page<OutdoorFair> page, int pageNum, int pageSize)
{
PublicJobFairResponse.PageData data = new PublicJobFairResponse.PageData();
List<PublicJobFair> list = page.getRecords().stream()
.map(this::buildPublicJobFair)
.collect(Collectors.toList());
int pages = (int) page.getPages();
data.setTotal(page.getTotal());
data.setList(list);
data.setPageNum(pageNum);
data.setPageSize(pageSize);
data.setSize(list.size());
data.setStartRow(list.isEmpty() ? 0 : (pageNum - 1) * pageSize + 1);
data.setEndRow(list.isEmpty() ? 0 : (pageNum - 1) * pageSize + list.size());
data.setPages(pages);
data.setPrePage(pageNum > 1 ? pageNum - 1 : 0);
data.setNextPage(pageNum < pages ? pageNum + 1 : 0);
data.setIsFirstPage(pageNum <= 1);
data.setIsLastPage(pageNum >= pages);
data.setHasPreviousPage(pageNum > 1);
data.setHasNextPage(pageNum < pages);
data.setNavigatePages(8);
data.setNavigatepageNums(buildNavigatePageNums(pages));
data.setNavigateFirstPage(pages > 0 ? 1 : 0);
data.setNavigateLastPage(pages);
return data;
}
private List<Integer> buildNavigatePageNums(int pages)
{
List<Integer> pageNums = new ArrayList<>();
for (int i = 1; i <= pages; i++) {
pageNums.add(i);
}
return pageNums;
}
private List<PublicJobFair> listOutdoorFairsByRange(Date startTime, Date endTime)
{
return outdoorFairService.list(new LambdaQueryWrapper<OutdoorFair>()
.ge(OutdoorFair::getHoldTime, startTime)
.lt(OutdoorFair::getHoldTime, endTime)
.orderByAsc(OutdoorFair::getHoldTime))
.stream()
.map(this::buildPublicJobFair)
.collect(Collectors.toList());
}
private PublicJobFair buildPublicJobFair(OutdoorFair fair)
{
PublicJobFair jobFair = new PublicJobFair();
jobFair.setJobFairId(String.valueOf(fair.getId()));
jobFair.setJobFairTitle(fair.getTitle());
jobFair.setJobFairAddress(StringUtils.isNotBlank(fair.getAddress()) ? fair.getAddress() : fair.getVenueName());
jobFair.setJobFairType("outdoor");
jobFair.setJobFairStartTime(fair.getHoldTime());
jobFair.setJobFairEndTime(fair.getEndTime());
jobFair.setJobFairHostUnit(fair.getHostUnit());
jobFair.setJobFairOrganizeUnit(fair.getHostUnit());
jobFair.setJobFairIntroduction(StringUtils.isNotBlank(fair.getVenueName()) ? fair.getVenueName() : fair.getAddress());
jobFair.setJobFairImage(fair.getPhotoUrl());
jobFair.setJobFairSignUpStartTime(fair.getApplyStartTime());
jobFair.setJobFairSignUpEndTime(fair.getApplyEndTime());
jobFair.setJobFairVenueId(fair.getVenueId() == null ? null : String.valueOf(fair.getVenueId()));
jobFair.setBoothNum(fair.getBoothCount() == null ? null : String.valueOf(fair.getBoothCount()));
return jobFair;
}
private Date[] parseDayRange(String day)
{
try {
Date startTime = new SimpleDateFormat(DAY_PATTERN).parse(day);
Calendar calendar = Calendar.getInstance();
calendar.setTime(startTime);
calendar.add(Calendar.DAY_OF_MONTH, 1);
return new Date[]{startTime, calendar.getTime()};
} catch (ParseException e) {
return null;
}
}
private static String formatDay(Date date)
{
return new SimpleDateFormat(DAY_PATTERN).format(date);
}
private void setStartOfDay(Calendar calendar)
{
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
}
private String buildBaseUrl(HttpServletRequest request)
{
String baseUrl = request.getScheme() + "://" + request.getServerName();
int port = request.getServerPort();
if (port != 80 && port != 443) {
baseUrl += ":" + port;
}
return baseUrl + (request.getContextPath() == null ? "" : request.getContextPath());
}
}

View File

@@ -39,6 +39,18 @@ public class CmsPublicJobFairController extends BaseController {
return getDataTable(list);
}
/**
* 查询跨域招聘会下的岗位可按联盟城市过滤cityNames 为逗号分隔的城市名称)
*/
@ApiOperation("跨域招聘会岗位列表")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:list')")
@GetMapping("/cross-domain-jobs")
public TableDataInfo crossDomainJobs(@RequestParam(required = false) String cityNames) {
startPage();
List<CrossDomainJobVO> list = publicJobFairService.selectCrossDomainJobs(cityNames);
return getDataTable(list);
}
/**
* 获取招聘会详情
*/

View File

@@ -0,0 +1,99 @@
package com.ruoyi.cms.controller.cms;
import java.util.Arrays;
import java.util.List;
import com.ruoyi.cms.domain.CrossCityAlliance;
import com.ruoyi.cms.service.ICrossCityAllianceService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 跨域联合招聘会-联盟城市 Controller
*
* @author ruoyi
* @date 2026-06-27
*/
@RestController
@RequestMapping("/cms/cross-city-alliance")
@Api(tags = "后台:跨域联合招聘会-联盟城市")
public class CrossCityAllianceController extends BaseController
{
@Autowired
private ICrossCityAllianceService crossCityAllianceService;
/**
* 查询联盟城市列表(支持按城市名称模糊查询)
*/
@ApiOperation("查询联盟城市列表")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:list')")
@GetMapping("/list")
public TableDataInfo list(CrossCityAlliance query)
{
startPage();
List<CrossCityAlliance> list = crossCityAllianceService.selectAllianceList(query);
return getDataTable(list);
}
/**
* 获取联盟城市详细信息
*/
@ApiOperation("获取联盟城市详细信息")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(crossCityAllianceService.getById(id));
}
/**
* 新增联盟城市
*/
@ApiOperation("新增联盟城市")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:add')")
@Log(title = "跨域联合招聘会-联盟城市", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody CrossCityAlliance alliance)
{
return toAjax(crossCityAllianceService.save(alliance));
}
/**
* 修改联盟城市
*/
@ApiOperation("修改联盟城市")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:edit')")
@Log(title = "跨域联合招聘会-联盟城市", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody CrossCityAlliance alliance)
{
return toAjax(crossCityAllianceService.updateById(alliance));
}
/**
* 删除联盟城市
*/
@ApiOperation("删除联盟城市")
@PreAuthorize("@ss.hasPermi('cms:crossCityFair:remove')")
@Log(title = "跨域联合招聘会-联盟城市", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(crossCityAllianceService.removeByIds(Arrays.asList(ids)));
}
}

View File

@@ -23,24 +23,31 @@ import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairBooth;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
import com.ruoyi.cms.domain.OutdoorFairJob;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.service.ICompanyService;
import com.ruoyi.cms.service.IJobService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.cms.util.RoleUtils;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.Company;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -58,6 +65,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 户外招聘会Controller
*
@@ -90,19 +99,27 @@ public class OutdoorFairController extends BaseController
@Autowired
private OutdoorFairJobMapper outdoorFairJobMapper;
@Autowired
private OutdoorFairDeviceMapper outdoorFairDeviceMapper;
@Autowired
private IJobService jobService;
@Autowired
private ICompanyService companyService;
/**
* 查询户外招聘会列表
*/
@ApiOperation("查询户外招聘会列表")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/list")
public TableDataInfo list(OutdoorFair outdoorFair)
public TableDataInfo list(OutdoorFair outdoorFair, HttpServletRequest request)
{
Company company = RoleUtils.isCompanyAdmin() ? getCurrentCompanyOrNull() : null;
startPage();
List<OutdoorFair> list = outdoorFairService.selectOutdoorFairList(outdoorFair);
fillCompanyApplyState(list, request, company);
return getDataTable(list);
}
@@ -114,6 +131,9 @@ public class OutdoorFairController extends BaseController
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权查看招聘会详情");
}
return success(outdoorFairService.getById(id));
}
@@ -346,6 +366,9 @@ public class OutdoorFairController extends BaseController
@PostMapping
public AjaxResult add(@RequestBody OutdoorFair outdoorFair)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权新增户外招聘会");
}
fillVenueFields(outdoorFair);
boolean saved = outdoorFairService.save(outdoorFair);
if (saved) {
@@ -364,6 +387,9 @@ public class OutdoorFairController extends BaseController
@PutMapping
public AjaxResult edit(@RequestBody OutdoorFair outdoorFair)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权编辑户外招聘会");
}
OutdoorFair oldFair = outdoorFair.getId() == null ? null : outdoorFairService.getById(outdoorFair.getId());
fillVenueFields(outdoorFair);
boolean updated = outdoorFairService.updateById(outdoorFair);
@@ -383,6 +409,9 @@ public class OutdoorFairController extends BaseController
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权删除户外招聘会");
}
return toAjax(outdoorFairService.removeByIds(Arrays.asList(ids)));
}
@@ -395,23 +424,60 @@ public class OutdoorFairController extends BaseController
@PostMapping("/dict-data")
public AjaxResult addDictData(@Validated @RequestBody OutdoorFairDictDataRequest request)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权维护户外招聘会字典");
}
return toAjax(outdoorFairService.insertOutdoorFairDictData(request.getDictType(), request.getDictLabel(), getUsername()));
}
@ApiOperation("企业报名户外招聘会")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@PostMapping("/{fairId}/company/signup")
@Transactional(rollbackFor = Exception.class)
public AjaxResult signupCompany(@PathVariable Long fairId, HttpServletRequest request)
{
Company company = getCurrentCompanyOrThrow();
ensureFairExists(fairId);
FairCompany relation = ensureFairCompany(fairId, company.getCompanyId(), "0");
Map<String, Object> data = buildCompanyQrCodeData(request, fairId, company.getCompanyId());
data.put("companyName", company.getName());
data.put("signedUp", true);
data.put("reviewStatus", relation.getReviewStatus());
data.put("reviewRemark", relation.getReviewRemark());
return success(data);
}
@ApiOperation("获取当前企业户外招聘会二维码内容")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/{fairId}/company/qrcode")
public AjaxResult getCurrentCompanyQrCode(@PathVariable Long fairId, HttpServletRequest request)
{
Company company = getCurrentCompanyOrThrow();
ensureFairCompanyExists(fairId, company.getCompanyId());
ensureFairCompanyApprovedIfCompany(fairId, company.getCompanyId());
return success(buildCompanyQrCodeData(request, fairId, company.getCompanyId()));
}
@ApiOperation("查询户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/companies")
public AjaxResult listCompanies(@PathVariable Long fairId, String companyName,
Integer current, Integer pageSize)
Integer current, Integer pageSize,
HttpServletRequest request)
{
List<FairCompany> relations = fairCompanyMapper.selectList(new LambdaQueryWrapper<FairCompany>()
LambdaQueryWrapper<FairCompany> relationWrapper = new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.orderByDesc(FairCompany::getId));
.orderByDesc(FairCompany::getId);
if (RoleUtils.isCompanyAdmin()) {
Company company = getCurrentCompanyOrNull();
if (company == null) {
return emptyAjaxPage();
}
relationWrapper.eq(FairCompany::getCompanyId, company.getCompanyId());
}
List<FairCompany> relations = fairCompanyMapper.selectList(relationWrapper);
if (relations.isEmpty()) {
AjaxResult result = AjaxResult.success();
result.put("rows", new ArrayList<>());
result.put("total", 0);
return result;
return emptyAjaxPage();
}
List<Long> companyIds = relations.stream().map(FairCompany::getCompanyId).collect(Collectors.toList());
@@ -428,6 +494,11 @@ public class OutdoorFairController extends BaseController
item -> Long.valueOf(String.valueOf(item.get("companyId"))),
item -> Long.valueOf(String.valueOf(item.get("jobCount"))),
(a, b) -> a));
Map<Long, OutdoorFairDevice> deviceMap = outdoorFairDeviceMapper.selectList(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getFairId, fairId)
.isNotNull(OutdoorFairDevice::getCompanyId))
.stream()
.collect(Collectors.toMap(OutdoorFairDevice::getCompanyId, item -> item, (a, b) -> a));
List<Map<String, Object>> rows = new ArrayList<>();
for (FairCompany relation : relations) {
@@ -451,6 +522,16 @@ public class OutdoorFairController extends BaseController
row.put("boothId", booking == null ? null : booking.getBoothId());
row.put("boothNumber", booking == null ? null : booking.getBoothNumber());
row.put("jobCount", jobCountMap.getOrDefault(company.getCompanyId(), 0L));
row.put("reviewStatus", relation.getReviewStatus());
row.put("reviewBy", relation.getReviewBy());
row.put("reviewTime", relation.getReviewTime());
row.put("reviewRemark", relation.getReviewRemark());
OutdoorFairDevice device = deviceMap.get(company.getCompanyId());
String h5Url = buildOutdoorFairCompanyH5Url(request, fairId, company.getCompanyId());
row.put("deviceId", device == null ? null : device.getId());
row.put("deviceCode", device == null ? null : device.getDeviceCode());
row.put("h5Url", h5Url);
row.put("qrCodeContent", h5Url);
rows.add(row);
}
@@ -465,13 +546,30 @@ public class OutdoorFairController extends BaseController
return result;
}
@ApiOperation("分页查询户外招聘会参会岗位")
@ApiOperation("获取户外招聘会参会企业二维码内容")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/companies/{companyId}/qrcode")
public AjaxResult getCompanyQrCode(@PathVariable Long fairId, @PathVariable Long companyId,
HttpServletRequest request)
{
ensureCurrentCompanyIfNeeded(companyId);
ensureFairCompanyExists(fairId, companyId);
ensureFairCompanyApprovedIfCompany(fairId, companyId);
return success(buildCompanyQrCodeData(request, fairId, companyId));
}
@ApiOperation("分页查询户外招聘会参会岗位")
@PreAuthorize("@ss.hasAnyPermi('cms:outdoorFair:list,cms:outdoorFair:query')")
@GetMapping("/{fairId}/jobs")
public AjaxResult listJobs(@PathVariable Long fairId, Long companyId, String companyName,
String jobTitle, String education, String experience,
Integer current, Integer pageSize)
{
if (RoleUtils.isCompanyAdmin()) {
Company company = getCurrentCompanyOrThrow();
companyId = company.getCompanyId();
companyName = null;
}
int page = current == null || current < 1 ? 1 : current;
int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
int offset = (page - 1) * size;
@@ -491,14 +589,29 @@ public class OutdoorFairController extends BaseController
public AjaxResult getCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId)
{
ensureFairJobExists(fairId, jobId);
ensureFairJobBelongsToCurrentCompanyIfNeeded(fairId, jobId);
return success(jobService.selectJobByJobId(jobId));
}
@ApiOperation("当前企业新增户外招聘会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/{fairId}/company/jobs")
public AjaxResult addCurrentCompanyJob(@PathVariable Long fairId, @RequestBody Job job)
{
Company company = getCurrentCompanyOrThrow();
ensureFairCompanyApprovedIfCompany(fairId, company.getCompanyId());
return addCompanyJobInternal(fairId, company.getCompanyId(), job);
}
@ApiOperation("新增户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PostMapping("/{fairId}/companies")
public AjaxResult addCompany(@PathVariable Long fairId, @RequestBody Map<String, Object> request)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户请使用报名入口");
}
Long companyId = Long.valueOf(String.valueOf(request.get("companyId")));
Company company = companyMapper.selectById(companyId);
if (company == null || company.getStatus() == null || company.getStatus() != 1) {
@@ -508,12 +621,36 @@ public class OutdoorFairController extends BaseController
return success();
}
@ApiOperation("审核户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PutMapping("/{fairId}/companies/{id}/review")
public AjaxResult reviewCompany(@PathVariable Long fairId, @PathVariable Long id,
@RequestBody Map<String, Object> request)
{
String reviewStatus = String.valueOf(request.get("reviewStatus"));
validateReviewStatus(reviewStatus);
FairCompany relation = fairCompanyMapper.selectById(id);
if (relation == null || !fairId.equals(relation.getJobFairId())) {
return AjaxResult.error("参会企业不存在");
}
String reviewRemark = request.get("reviewRemark") == null ? null : String.valueOf(request.get("reviewRemark"));
validateRejectRemark(reviewStatus, reviewRemark);
relation.setReviewStatus(reviewStatus);
relation.setReviewBy(getUsername());
relation.setReviewTime(new Date());
relation.setReviewRemark(reviewRemark);
return toAjax(fairCompanyMapper.updateById(relation));
}
@ApiOperation("移除户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@DeleteMapping("/{fairId}/companies/{id}")
@Transactional(rollbackFor = Exception.class)
public AjaxResult removeCompany(@PathVariable Long fairId, @PathVariable Long id)
{
if (RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("企业用户无权移除参会企业");
}
FairCompany relation = fairCompanyMapper.selectById(id);
if (relation == null || !fairId.equals(relation.getJobFairId())) {
return AjaxResult.error("参会企业不存在");
@@ -530,9 +667,19 @@ public class OutdoorFairController extends BaseController
@PostMapping("/{fairId}/companies/{companyId}/jobs")
public AjaxResult addCompanyJob(@PathVariable Long fairId, @PathVariable Long companyId,
@RequestBody Job job)
{
ensureCurrentCompanyIfNeeded(companyId);
ensureFairCompanyApprovedIfCompany(fairId, companyId);
return addCompanyJobInternal(fairId, companyId, job);
}
private AjaxResult addCompanyJobInternal(Long fairId, Long companyId, Job job)
{
ensureFairCompanyExists(fairId, companyId);
Company company = companyMapper.selectById(companyId);
if (company == null) {
return AjaxResult.error("企业不存在");
}
job.setCompanyId(companyId);
job.setCompanyName(company.getName());
if (job.getIsPublish() == null) {
@@ -546,6 +693,7 @@ public class OutdoorFairController extends BaseController
relation.setFairId(fairId);
relation.setCompanyId(companyId);
relation.setJobId(job.getJobId());
relation.setReviewStatus(RoleUtils.isCompanyAdmin() ? "0" : "1");
relation.setCreateBy(getUsername());
outdoorFairJobMapper.insert(relation);
return success(job);
@@ -558,6 +706,7 @@ public class OutdoorFairController extends BaseController
@RequestBody Job job)
{
ensureFairJobExists(fairId, jobId);
ensureFairJobBelongsToCurrentCompanyIfNeeded(fairId, jobId);
job.setJobId(jobId);
// 当前弹窗不修改发布状态,避免重复触发岗位发布流程。
job.setIsPublish(null);
@@ -571,12 +720,37 @@ public class OutdoorFairController extends BaseController
public AjaxResult removeCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId)
{
ensureFairJobExists(fairId, jobId);
ensureFairJobBelongsToCurrentCompanyIfNeeded(fairId, jobId);
outdoorFairJobMapper.delete(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getJobId, jobId));
return toAjax(jobService.deleteJobByJobIds(new Long[]{jobId}));
}
@ApiOperation("审核户外招聘会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PutMapping("/{fairId}/jobs/{jobId}/review")
public AjaxResult reviewCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId,
@RequestBody Map<String, Object> request)
{
String reviewStatus = String.valueOf(request.get("reviewStatus"));
validateReviewStatus(reviewStatus);
OutdoorFairJob relation = outdoorFairJobMapper.selectOne(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getJobId, jobId)
.last("LIMIT 1"));
if (relation == null) {
return AjaxResult.error("当前招聘会不存在该岗位");
}
String reviewRemark = request.get("reviewRemark") == null ? null : String.valueOf(request.get("reviewRemark"));
validateRejectRemark(reviewStatus, reviewRemark);
relation.setReviewStatus(reviewStatus);
relation.setReviewBy(getUsername());
relation.setReviewTime(new Date());
relation.setReviewRemark(reviewRemark);
return toAjax(outdoorFairJobMapper.updateById(relation));
}
private void ensureFairCompanyExists(Long fairId, Long companyId)
{
Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
@@ -587,6 +761,30 @@ public class OutdoorFairController extends BaseController
}
}
private void ensureFairCompanyApprovedIfCompany(Long fairId, Long companyId)
{
if (!RoleUtils.isCompanyAdmin()) {
return;
}
FairCompany relation = fairCompanyMapper.selectOne(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, companyId)
.last("LIMIT 1"));
if (relation == null) {
throw new ServiceException("该企业未参加当前招聘会");
}
if (!"1".equals(relation.getReviewStatus())) {
throw new ServiceException("报名审核通过后才能操作");
}
}
private void ensureFairExists(Long fairId)
{
if (fairId == null || outdoorFairService.getById(fairId) == null) {
throw new ServiceException("户外招聘会不存在");
}
}
private void ensureFairJobExists(Long fairId, Long jobId)
{
Long count = outdoorFairJobMapper.selectCount(new LambdaQueryWrapper<OutdoorFairJob>()
@@ -597,18 +795,171 @@ public class OutdoorFairController extends BaseController
}
}
private void ensureFairJobBelongsToCurrentCompanyIfNeeded(Long fairId, Long jobId)
{
if (!RoleUtils.isCompanyAdmin()) {
return;
}
Company company = getCurrentCompanyOrThrow();
Long count = outdoorFairJobMapper.selectCount(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getJobId, jobId)
.eq(OutdoorFairJob::getCompanyId, company.getCompanyId()));
if (count == null || count == 0) {
throw new ServiceException("只能维护本企业报名岗位");
}
}
private void ensureCurrentCompanyIfNeeded(Long companyId)
{
if (!RoleUtils.isCompanyAdmin()) {
return;
}
Company company = getCurrentCompanyOrThrow();
if (!company.getCompanyId().equals(companyId)) {
throw new ServiceException("只能维护本企业报名信息");
}
}
private Company getCurrentCompanyOrThrow()
{
Company company = getCurrentCompanyOrNull();
if (company == null) {
throw new ServiceException("未查询到当前登录企业信息");
}
return company;
}
private Company getCurrentCompanyOrNull()
{
if (!RoleUtils.isCompanyAdmin()) {
return null;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || loginUser.getUser() == null) {
return null;
}
SysUser sysUser = loginUser.getUser();
Company company = null;
if (StringUtils.isNotBlank(sysUser.getIdCard())) {
company = companyService.queryCodeCompany(sysUser.getIdCard());
if (company != null) {
return company;
}
}
Long userId = loginUser.getUserId() == null ? sysUser.getUserId() : loginUser.getUserId();
if (userId != null) {
company = companyMapper.selectOne(new LambdaQueryWrapper<Company>()
.eq(Company::getUserId, userId)
.eq(Company::getDelFlag, "0")
.orderByDesc(Company::getUpdateTime)
.last("LIMIT 1"));
if (company != null) {
return company;
}
}
List<String> companyNames = new ArrayList<>();
if (StringUtils.isNotBlank(sysUser.getNickName())) {
companyNames.add(sysUser.getNickName());
}
if (StringUtils.isNotBlank(sysUser.getUserName()) && !companyNames.contains(sysUser.getUserName())) {
companyNames.add(sysUser.getUserName());
}
if (!companyNames.isEmpty()) {
List<Company> companies = companyMapper.selectList(new LambdaQueryWrapper<Company>()
.in(Company::getName, companyNames)
.eq(Company::getStatus, 1)
.eq(Company::getDelFlag, "0")
.orderByDesc(Company::getUpdateTime));
if (companies.size() == 1) {
return companies.get(0);
}
}
return null;
}
private void fillCompanyApplyState(List<OutdoorFair> list, HttpServletRequest request, Company company)
{
if (!RoleUtils.isCompanyAdmin() || list == null || list.isEmpty()) {
return;
}
if (company == null) {
for (OutdoorFair fair : list) {
fair.setSignedUp(false);
}
return;
}
List<Long> fairIds = list.stream().map(OutdoorFair::getId).collect(Collectors.toList());
Map<Long, FairCompany> relationMap = fairCompanyMapper.selectList(new LambdaQueryWrapper<FairCompany>()
.in(FairCompany::getJobFairId, fairIds)
.eq(FairCompany::getCompanyId, company.getCompanyId()))
.stream()
.collect(Collectors.toMap(FairCompany::getJobFairId, item -> item, (a, b) -> a));
for (OutdoorFair fair : list) {
FairCompany relation = relationMap.get(fair.getId());
boolean signedUp = relation != null;
fair.setCompanyId(company.getCompanyId());
fair.setCompanyName(company.getName());
fair.setSignedUp(signedUp);
fair.setReviewStatus(relation == null ? null : relation.getReviewStatus());
fair.setReviewRemark(relation == null ? null : relation.getReviewRemark());
if (signedUp) {
String h5Url = buildOutdoorFairCompanyH5Url(request, fair.getId(), company.getCompanyId());
fair.setH5Url(h5Url);
fair.setQrCodeContent(h5Url);
}
}
}
private AjaxResult emptyAjaxPage()
{
AjaxResult result = AjaxResult.success();
result.put("rows", new ArrayList<>());
result.put("total", 0);
return result;
}
private void ensureFairCompany(Long fairId, Long companyId)
{
Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
ensureFairCompany(fairId, companyId, "1");
}
private FairCompany ensureFairCompany(Long fairId, Long companyId, String reviewStatus)
{
FairCompany existing = fairCompanyMapper.selectOne(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, companyId));
if (count != null && count > 0) {
return;
.eq(FairCompany::getCompanyId, companyId)
.last("LIMIT 1"));
if (existing != null) {
if (StringUtils.isBlank(existing.getReviewStatus())) {
existing.setReviewStatus(reviewStatus);
fairCompanyMapper.updateById(existing);
}
return existing;
}
FairCompany fairCompany = new FairCompany();
fairCompany.setJobFairId(fairId);
fairCompany.setCompanyId(companyId);
fairCompany.setReviewStatus(reviewStatus);
fairCompanyMapper.insert(fairCompany);
return fairCompany;
}
private void validateReviewStatus(String reviewStatus)
{
if (!"0".equals(reviewStatus) && !"1".equals(reviewStatus) && !"2".equals(reviewStatus)) {
throw new ServiceException("审核状态不正确");
}
}
private void validateRejectRemark(String reviewStatus, String reviewRemark)
{
if ("2".equals(reviewStatus) && StringUtils.isBlank(reviewRemark)) {
throw new ServiceException("驳回时请填写原因");
}
}
private void resetBoothSnapshot(OutdoorFair outdoorFair, boolean forceRecreate)
@@ -640,4 +991,32 @@ public class OutdoorFairController extends BaseController
outdoorFair.setBoothCount(0);
}
}
private String buildOutdoorFairCompanyH5Url(HttpServletRequest request, Long fairId, Long companyId)
{
String baseUrl = request.getScheme() + "://" + request.getServerName();
int port = request.getServerPort();
if (port != 80 && port != 443) {
baseUrl += ":" + port;
}
String contextPath = request.getContextPath() == null ? "" : request.getContextPath();
return baseUrl + contextPath + "/h5/outdoor-fair/company.html?fairId=" + fairId + "&companyId=" + companyId;
}
private Map<String, Object> buildCompanyQrCodeData(HttpServletRequest request, Long fairId, Long companyId)
{
OutdoorFairDevice device = outdoorFairDeviceMapper.selectOne(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getFairId, fairId)
.eq(OutdoorFairDevice::getCompanyId, companyId)
.last("LIMIT 1"));
String h5Url = buildOutdoorFairCompanyH5Url(request, fairId, companyId);
Map<String, Object> data = new HashMap<>();
data.put("fairId", fairId);
data.put("companyId", companyId);
data.put("deviceId", device == null ? null : device.getId());
data.put("deviceCode", device == null ? null : device.getDeviceCode());
data.put("h5Url", h5Url);
data.put("qrCodeContent", h5Url);
return data;
}
}

View File

@@ -63,20 +63,21 @@ public class OutdoorFairDeviceController extends BaseController
}
/**
* 批量生成设备码UUID
* 请求体:{ fairId, quantity }
* 新增设备码(由用户手动填写设备码,可选同时绑定参会企业
* 请求体:{ fairId, deviceCode, companyId? }
*/
@ApiOperation("批量生成设备码")
@ApiOperation("新增设备码")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
@Log(title = "户外招聘会设备码", businessType = BusinessType.INSERT)
@PostMapping("/batch")
public AjaxResult batch(@RequestBody Map<String, Object> request)
@PostMapping("/add")
public AjaxResult add(@RequestBody Map<String, Object> request)
{
Long fairId = request.get("fairId") == null ? null : Long.valueOf(String.valueOf(request.get("fairId")));
Integer quantity = request.get("quantity") == null ? null : Integer.valueOf(String.valueOf(request.get("quantity")));
List<OutdoorFairDevice> devices = outdoorFairDeviceService.batchGenerate(fairId, quantity);
AjaxResult result = AjaxResult.success("生成成功");
result.put("data", devices);
String deviceCode = request.get("deviceCode") == null ? null : String.valueOf(request.get("deviceCode")).trim();
Long companyId = request.get("companyId") == null ? null : Long.valueOf(String.valueOf(request.get("companyId")));
OutdoorFairDevice device = outdoorFairDeviceService.addDevice(fairId, deviceCode, companyId);
AjaxResult result = AjaxResult.success("新增成功");
result.put("data", device);
return result;
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.cms.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
/**
* 跨域联合招聘会-联盟城市。
*/
@Data
@TableName("shz.cms_cross_city_alliance")
public class CrossCityAlliance extends BaseEntity
{
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 城市名称 */
private String name;
}

View File

@@ -9,6 +9,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 公司招聘会关联对象 fair_company
* @author lishundong
@@ -34,4 +36,16 @@ public class FairCompany extends BaseEntity
@ApiModelProperty("招聘会id")
private Long jobFairId;
@ApiModelProperty("审核状态 0待审核 1通过 2驳回")
private String reviewStatus;
@ApiModelProperty("审核人")
private String reviewBy;
@ApiModelProperty("审核时间")
private Date reviewTime;
@ApiModelProperty("审核备注")
private String reviewRemark;
}

View File

@@ -205,6 +205,19 @@ public class Job extends BaseEntity
@ApiModelProperty("审核状态")
private String reviewStatus;
@TableField(exist = false)
@ApiModelProperty("户外招聘会岗位审核状态")
private String fairReviewStatus;
@TableField(exist = false)
@ApiModelProperty("户外招聘会岗位审核备注")
private String fairReviewRemark;
@TableField(exist = false)
@ApiModelProperty("户外招聘会岗位审核时间")
private String fairReviewTime;
//简历是否被查看
@TableField(exist = false)
private String isView;

View File

@@ -3,6 +3,7 @@ package com.ruoyi.cms.domain;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -88,4 +89,32 @@ public class OutdoorFair extends BaseEntity
@Excel(name = "招聘会照片")
@ApiModelProperty("招聘会照片")
private String photoUrl;
@TableField(exist = false)
@ApiModelProperty("当前企业ID")
private Long companyId;
@TableField(exist = false)
@ApiModelProperty("当前企业名称")
private String companyName;
@TableField(exist = false)
@ApiModelProperty("当前企业是否已报名")
private Boolean signedUp;
@TableField(exist = false)
@ApiModelProperty("当前企业公共H5页面链接")
private String h5Url;
@TableField(exist = false)
@ApiModelProperty("当前企业二维码内容")
private String qrCodeContent;
@TableField(exist = false)
@ApiModelProperty("当前企业报名审核状态")
private String reviewStatus;
@TableField(exist = false)
@ApiModelProperty("当前企业报名审核备注")
private String reviewRemark;
}

View File

@@ -1,11 +1,15 @@
package com.ruoyi.cms.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 户外招聘会岗位关联。
*/
@@ -21,4 +25,20 @@ public class OutdoorFairJob extends BaseEntity
private Long companyId;
private Long jobId;
/** 审核状态 0待审核 1通过 2驳回。 */
private String reviewStatus;
private String reviewBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reviewTime;
private String reviewRemark;
@TableField(exist = false)
private String companyName;
@TableField(exist = false)
private String jobTitle;
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.cms.domain.rc;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 跨域招聘会岗位视图(聚合 public_job_fair -> public_job_fair_job -> job -> company
*/
@Data
@ApiModel("跨域招聘会岗位")
public class CrossDomainJobVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("岗位ID")
private Long jobId;
@ApiModelProperty("岗位名称")
private String jobTitle;
@ApiModelProperty("最低薪资")
private Integer minSalary;
@ApiModelProperty("最高薪资")
private Integer maxSalary;
@ApiModelProperty("学历要求")
private String education;
@ApiModelProperty("经验要求")
private String experience;
@ApiModelProperty("招聘人数")
private Integer vacancies;
@JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty("发布日期")
private Date postingDate;
@ApiModelProperty("企业ID")
private Long companyId;
@ApiModelProperty("企业名称")
private String companyName;
@ApiModelProperty("行业")
private String industry;
@ApiModelProperty("企业规模")
private String scale;
@ApiModelProperty("招聘会ID")
private String jobFairId;
@ApiModelProperty("招聘会标题")
private String jobFairTitle;
@ApiModelProperty("跨域联盟城市(逗号分隔)")
private String crossDomainCities;
}

View File

@@ -151,6 +151,12 @@ public class PublicJobFair implements Serializable {
@ApiModelProperty("招聘会类别")
private String jobFairCategory;
@ApiModelProperty("是否跨域Y-是 N-否)")
private String isCrossDomain;
@ApiModelProperty("跨域联盟城市(逗号分隔的城市名称)")
private String crossDomainCities;
@ApiModelProperty("纬度")
private java.math.BigDecimal latitude;

View File

@@ -0,0 +1,11 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.CrossCityAlliance;
/**
* 跨域联合招聘会-联盟城市 Mapper。
*/
public interface CrossCityAllianceMapper extends BaseMapper<CrossCityAlliance>
{
}

View File

@@ -32,6 +32,11 @@ public interface PublicJobFairMapper extends BaseMapper<PublicJobFair> {
*/
List<Job> selectJobListByJobFairIdAndCompanyId(@Param("jobFairId") String jobFairId, @Param("companyId") Long companyId);
/**
* 查询跨域招聘会下的岗位可按联盟城市过滤cityNames 为逗号分隔的城市名称)
*/
List<CrossDomainJobVO> selectCrossDomainJobs(@Param("cityNames") String cityNames);
/**
* 检查用户是否已报名
*/

View File

@@ -0,0 +1,17 @@
package com.ruoyi.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.CrossCityAlliance;
import java.util.List;
/**
* 跨域联合招聘会-联盟城市 Service。
*/
public interface ICrossCityAllianceService extends IService<CrossCityAlliance>
{
/**
* 查询联盟城市列表(支持按城市名称模糊查询)。
*/
List<CrossCityAlliance> selectAllianceList(CrossCityAlliance query);
}

View File

@@ -16,13 +16,14 @@ public interface IOutdoorFairDeviceService extends IService<OutdoorFairDevice>
List<OutdoorFairDevice> selectDeviceList(OutdoorFairDevice query);
/**
* 批量生成设备码UUID)。
* 新增设备码(由用户手动填写设备码,可选同时绑定参会企业)。
*
* @param fairId 招聘会ID
* @param quantity 生成数量
* @return 已生成的设备码列表
* @param deviceCode 设备码(全局唯一)
* @param companyId 参会企业ID为空表示暂不绑定
* @return 新增的设备码
*/
List<OutdoorFairDevice> batchGenerate(Long fairId, Integer quantity);
OutdoorFairDevice addDevice(Long fairId, String deviceCode, Long companyId);
/**
* 绑定/解绑参会企业companyId 为空表示解绑)。

View File

@@ -0,0 +1,29 @@
package com.ruoyi.cms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.CrossCityAlliance;
import com.ruoyi.cms.mapper.CrossCityAllianceMapper;
import com.ruoyi.cms.service.ICrossCityAllianceService;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 跨域联合招聘会-联盟城市 Service 实现。
*/
@Service
public class CrossCityAllianceServiceImpl
extends ServiceImpl<CrossCityAllianceMapper, CrossCityAlliance>
implements ICrossCityAllianceService
{
@Override
public List<CrossCityAlliance> selectAllianceList(CrossCityAlliance query)
{
LambdaQueryWrapper<CrossCityAlliance> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getName()), CrossCityAlliance::getName, query.getName());
wrapper.orderByAsc(CrossCityAlliance::getId);
return list(wrapper);
}
}

View File

@@ -15,10 +15,8 @@ import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 户外招聘会设备码 Service 实现。
@@ -47,28 +45,45 @@ public class OutdoorFairDeviceServiceImpl
}
@Override
public List<OutdoorFairDevice> batchGenerate(Long fairId, Integer quantity)
public OutdoorFairDevice addDevice(Long fairId, String deviceCode, Long companyId)
{
if (fairId == null) {
throw new ServiceException("招聘会ID不能为空");
}
int count = quantity == null ? 0 : quantity;
if (count <= 0) {
throw new ServiceException("生成数量必须大于0");
if (StringUtils.isBlank(deviceCode)) {
throw new ServiceException("设备码不能为空");
}
if (count > 1000) {
throw new ServiceException("单次生成数量不能超过1000");
// 设备码全局唯一H5 端按设备码查询)
Long exists = baseMapper.selectCount(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getDeviceCode, deviceCode));
if (exists != null && exists > 0) {
throw new ServiceException("设备码已存在");
}
List<OutdoorFairDevice> devices = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
OutdoorFairDevice device = new OutdoorFairDevice();
device.setFairId(fairId);
// 去掉中划线,得到 32 位十六进制设备码
device.setDeviceCode(UUID.randomUUID().toString().replace("-", ""));
devices.add(device);
device.setDeviceCode(deviceCode);
if (companyId != null) {
// 仅允许绑定已参加当前招聘会的企业
Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, companyId));
if (count == null || count == 0) {
throw new ServiceException("该企业未参加当前招聘会");
}
saveBatch(devices);
return devices;
// 同一招聘会下,一个企业只能绑定一个设备码
Long bound = baseMapper.selectCount(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getFairId, fairId)
.eq(OutdoorFairDevice::getCompanyId, companyId));
if (bound != null && bound > 0) {
throw new ServiceException("该企业已绑定其他设备码");
}
Company company = companyMapper.selectById(companyId);
device.setCompanyId(companyId);
device.setCompanyName(company == null ? null : company.getName());
device.setBindTime(new Date());
}
save(device);
return device;
}
@Override

View File

@@ -100,4 +100,9 @@ public interface IPublicJobFairService {
* 查询招聘会报名列表
*/
List<JobFairSignUpVO> selectSignUpList(String jobFairId);
/**
* 查询跨域招聘会下的岗位可按联盟城市过滤cityNames 为逗号分隔的城市名称)
*/
List<CrossDomainJobVO> selectCrossDomainJobs(String cityNames);
}

View File

@@ -219,4 +219,9 @@ public class PublicJobFairServiceImpl implements IPublicJobFairService {
public List<JobFairSignUpVO> selectSignUpList(String jobFairId) {
return publicJobFairMapper.selectSignUpListByJobFairId(jobFairId);
}
@Override
public List<CrossDomainJobVO> selectCrossDomainJobs(String cityNames) {
return publicJobFairMapper.selectCrossDomainJobs(cityNames);
}
}

View File

@@ -34,7 +34,10 @@
</sql>
<select id="selectFairJobPage" resultType="com.ruoyi.cms.domain.Job">
SELECT job.*
SELECT job.*,
relation.review_status AS "fairReviewStatus",
relation.review_remark AS "fairReviewRemark",
to_char(relation.review_time, 'YYYY-MM-DD HH24:MI:SS') AS "fairReviewTime"
FROM shz.cms_outdoor_fair_job relation
INNER JOIN shz.job job ON job.job_id = relation.job_id
<include refid="fairJobWhere"/>

View File

@@ -31,6 +31,8 @@
<result property="deptCodeLast" column="dept_code_last"/>
<result property="deptCodeAll" column="dept_code_all"/>
<result property="jobFairCategory" column="job_fair_category"/>
<result property="isCrossDomain" column="is_cross_domain"/>
<result property="crossDomainCities" column="cross_domain_cities"/>
</resultMap>
<resultMap type="com.ruoyi.cms.domain.rc.PublicJobFairDetail" id="DetailResult">
@@ -59,7 +61,7 @@
job_fair_host_unit, job_fair_help_unit, job_fair_organize_unit, job_fair_introduction, job_fair_remark,
job_fair_phone, job_fair_image, job_fair_area_image, create_by, create_time, update_by, update_time,
job_fair_venue_id, job_fair_sign_up_start_time, job_fair_sign_up_end_time, enterprise_num, booth_num,
division_name, dept_code_last, dept_code_all, job_fair_category
division_name, dept_code_last, dept_code_all, job_fair_category, is_cross_domain, cross_domain_cities
from public_job_fair
</sql>
@@ -106,6 +108,23 @@
order by j.create_time desc
</select>
<select id="selectCrossDomainJobs" resultType="com.ruoyi.cms.domain.rc.CrossDomainJobVO">
select j.job_id as jobId, j.job_title as jobTitle, j.min_salary as minSalary, j.max_salary as maxSalary,
j.education, j.experience, j.vacancies, j.posting_date as postingDate,
j.company_id as companyId, j.company_name as companyName,
c.industry, c.scale,
pf.job_fair_id as jobFairId, pf.job_fair_title as jobFairTitle, pf.cross_domain_cities as crossDomainCities
from public_job_fair pf
inner join public_job_fair_job pfj on pfj.job_fair_id = pf.job_fair_id and pfj.del_flag = '0'
inner join job j on j.job_id = pfj.job_id and j.del_flag = '0'
left join company c on c.company_id = pfj.company_id
where pf.is_cross_domain = 'y' and pf.del_flag = '0'
<if test="cityNames != null and cityNames != ''">
and string_to_array(pf.cross_domain_cities, ',') &amp;&amp; string_to_array(#{cityNames}, ',')
</if>
order by j.posting_date desc nulls last, j.job_id desc
</select>
<select id="checkUserSignUp" resultType="java.lang.Integer">
select count(1) from rc_job_fair_sign_up
where job_fair_id = #{jobFairId}::bigint and user_id = #{personId} and del_flag = '0' and status = '0'
@@ -126,7 +145,7 @@
job_fair_image, job_fair_area_image, job_fair_venue_id,
job_fair_sign_up_start_time, job_fair_sign_up_end_time,
enterprise_num, booth_num, division_name, dept_code_last, dept_code_all,
job_fair_category, latitude, longitude,
job_fair_category, is_cross_domain, cross_domain_cities, latitude, longitude,
create_by, create_time, del_flag
) values (
#{jobFairId}, #{jobFairTitle}, #{jobFairAddress}, #{jobFairType},
@@ -135,7 +154,7 @@
#{jobFairImage}, #{jobFairAreaImage}, #{jobFairVenueId},
#{jobFairSignUpStartTime}, #{jobFairSignUpEndTime},
#{enterpriseNum}, #{boothNum}, #{divisionName}, #{deptCodeLast}, #{deptCodeAll},
#{jobFairCategory}, #{latitude}, #{longitude},
#{jobFairCategory}, #{isCrossDomain}, #{crossDomainCities}, #{latitude}, #{longitude},
#{createBy}, #{createTime}, '0'
)
</insert>
@@ -165,6 +184,8 @@
<if test="deptCodeLast != null">dept_code_last = #{deptCodeLast},</if>
<if test="deptCodeAll != null">dept_code_all = #{deptCodeAll},</if>
<if test="jobFairCategory != null">job_fair_category = #{jobFairCategory},</if>
<if test="isCrossDomain != null">is_cross_domain = #{isCrossDomain},</if>
<if test="crossDomainCities != null">cross_domain_cities = #{crossDomainCities},</if>
<if test="latitude != null">latitude = #{latitude},</if>
<if test="longitude != null">longitude = #{longitude},</if>
update_by = #{updateBy},

View File

@@ -282,6 +282,8 @@ public class SsoService {
company.setLegalPhone(object.getString("entLegalPersonPhone"));
company.setIndustry(object.getString("entIndustry"));
company.setRegisteredAddress(object.getString("entRegisteredAddress"));
company.setNature(object.getString("entType"));
// company.setScale(object.getString("entSize"));
companyService.insertCompany(company);
}
/**

100
scripts/migrate-highgo-shz.sh Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Back up the current SHZ schema and restore it into a new `shz` database on
# the destination HighGo instance. Run this manually from the repository root.
set -euo pipefail
SOURCE_SSH_HOST="${SOURCE_SSH_HOST:-root@124.243.245.42}"
SOURCE_DB_HOST="${SOURCE_DB_HOST:-39.98.44.136}"
SOURCE_DB_PORT="${SOURCE_DB_PORT:-6023}"
SOURCE_DB_NAME="${SOURCE_DB_NAME:-highgo}"
SOURCE_DB_USER="${SOURCE_DB_USER:-sysdba}"
SOURCE_SCHEMA="${SOURCE_SCHEMA:-shz}"
SOURCE_PASSWORD_FILE="${SOURCE_PASSWORD_FILE:-.codex/skills/query-shz-highgo/.password}"
TARGET_SSH_HOST="${TARGET_SSH_HOST:-root@124.243.245.42}"
TARGET_DB_HOST="${TARGET_DB_HOST:-127.0.0.1}"
TARGET_DB_PORT="${TARGET_DB_PORT:-5866}"
TARGET_DB_NAME="${TARGET_DB_NAME:-shz}"
TARGET_DB_USER="${TARGET_DB_USER:-sysdba}"
HIGHGO_BIN="${HIGHGO_BIN:-/opt/HighGo4.5.7-see/bin}"
BACKUP_DIR="${BACKUP_DIR:-backups/highgo}"
for command in ssh shasum; do
command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 2; }
done
[[ -r "$SOURCE_PASSWORD_FILE" ]] || {
echo "Source password file is missing or unreadable: $SOURCE_PASSWORD_FILE" >&2
exit 2
}
read -r -s -p "Target HighGo password for ${TARGET_DB_USER}: " TARGET_PASSWORD
printf '\n'
[[ -n "$TARGET_PASSWORD" ]] || { echo "Target password cannot be empty." >&2; exit 2; }
timestamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
backup_file="$BACKUP_DIR/${SOURCE_SCHEMA}-${timestamp}.dump"
checksum_file="${backup_file}.sha256"
sequence_backup_file="$BACKUP_DIR/public-sequences-${timestamp}.dump"
sequence_checksum_file="${sequence_backup_file}.sha256"
remote_psql() {
local database="$1"
local sql="$2"
printf '%s\n' "$TARGET_PASSWORD" | ssh -o BatchMode=yes -o ConnectTimeout=10 "$TARGET_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/psql' -X --no-psqlrc -h '$TARGET_DB_HOST' -p '$TARGET_DB_PORT' -U '$TARGET_DB_USER' -d '$database' -v ON_ERROR_STOP=1 -Atc \"$sql\""
}
echo "Checking target instance and database name..."
remote_psql highgo 'SELECT 1' >/dev/null
if [[ "$(remote_psql highgo "SELECT 1 FROM pg_database WHERE datname = '$TARGET_DB_NAME'")" == "1" ]]; then
echo "Refusing to continue: target database '$TARGET_DB_NAME' already exists." >&2
echo "No backup or restore was performed." >&2
exit 3
fi
echo "Creating source backup: $backup_file"
source_password="$(<"$SOURCE_PASSWORD_FILE")"
printf '%s\n' "$source_password" | ssh -o BatchMode=yes -o ConnectTimeout=10 "$SOURCE_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/pg_dump' -Fc --no-owner --no-privileges -h '$SOURCE_DB_HOST' -p '$SOURCE_DB_PORT' -U '$SOURCE_DB_USER' -d '$SOURCE_DB_NAME' -n '$SOURCE_SCHEMA'" \
>"$backup_file"
[[ -s "$backup_file" ]] || { echo "Backup is empty; refusing to create target database." >&2; rm -f "$backup_file"; exit 4; }
shasum -a 256 "$backup_file" >"$checksum_file"
echo "Creating public-sequence dependency backup: $sequence_backup_file"
printf '%s\n' "$source_password" | ssh -o BatchMode=yes -o ConnectTimeout=10 "$SOURCE_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/pg_dump' -Fc --no-owner --no-privileges -h '$SOURCE_DB_HOST' -p '$SOURCE_DB_PORT' -U '$SOURCE_DB_USER' -d '$SOURCE_DB_NAME' -t 'public.*_seq'" \
>"$sequence_backup_file"
[[ -s "$sequence_backup_file" ]] || { echo "Sequence backup is empty; refusing to create target database." >&2; rm -f "$backup_file" "$sequence_backup_file"; exit 4; }
shasum -a 256 "$sequence_backup_file" >"$sequence_checksum_file"
echo "Creating target database '$TARGET_DB_NAME'..."
remote_psql highgo "CREATE DATABASE \"$TARGET_DB_NAME\"" >/dev/null
echo "Restoring schema '$SOURCE_SCHEMA' into '$TARGET_DB_NAME'..."
{ printf '%s\n' "$TARGET_PASSWORD"; cat "$sequence_backup_file"; } | ssh -o BatchMode=yes -o ConnectTimeout=10 "$TARGET_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/pg_restore' --single-transaction --exit-on-error --no-owner --no-privileges -h '$TARGET_DB_HOST' -p '$TARGET_DB_PORT' -U '$TARGET_DB_USER' -d '$TARGET_DB_NAME'" \
>/dev/null
{ printf '%s\n' "$TARGET_PASSWORD"; cat "$backup_file"; } | ssh -o BatchMode=yes -o ConnectTimeout=10 "$TARGET_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/pg_restore' --single-transaction --exit-on-error --no-owner --no-privileges -h '$TARGET_DB_HOST' -p '$TARGET_DB_PORT' -U '$TARGET_DB_USER' -d '$TARGET_DB_NAME'" \
>/dev/null
echo "Verifying restored schema..."
source_count="$(printf '%s\n' "$source_password" | ssh -o BatchMode=yes -o ConnectTimeout=10 "$SOURCE_SSH_HOST" \
"IFS= read -r PGPASSWORD; export PGPASSWORD; exec '$HIGHGO_BIN/psql' -X --no-psqlrc -h '$SOURCE_DB_HOST' -p '$SOURCE_DB_PORT' -U '$SOURCE_DB_USER' -d '$SOURCE_DB_NAME' -Atc \"SELECT count(*) FROM information_schema.tables WHERE table_schema = '$SOURCE_SCHEMA' AND table_type = 'BASE TABLE'\"")"
target_count="$(remote_psql "$TARGET_DB_NAME" "SELECT count(*) FROM information_schema.tables WHERE table_schema = '$SOURCE_SCHEMA' AND table_type = 'BASE TABLE'")"
if [[ "$source_count" != "$target_count" ]]; then
echo "Verification failed: source has $source_count tables, target has $target_count." >&2
exit 5
fi
unset TARGET_PASSWORD source_password
echo "Migration complete. Backup: $backup_file"
echo "Checksum: $checksum_file"
echo "Sequence backup: $sequence_backup_file"
echo "Sequence checksum: $sequence_checksum_file"
echo "Verified base tables in schema '$SOURCE_SCHEMA': $target_count"

View File

@@ -0,0 +1,67 @@
-- 跨域联合招聘会 - 联盟城市表
-- 用于「各地市就业信息」Tab 的联盟城市筛选与管理(替代前端硬编码 mock
-- 依赖sys_menu 已存在 menu_id=2114「跨域联合招聘会管理」parent_id=2055
CREATE SEQUENCE IF NOT EXISTS shz.cms_cross_city_alliance_id_seq
START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS shz.cms_cross_city_alliance (
id BIGINT NOT NULL DEFAULT nextval('shz.cms_cross_city_alliance_id_seq'::regclass) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE shz.cms_cross_city_alliance IS '跨域联合招聘会-联盟城市表';
COMMENT ON COLUMN shz.cms_cross_city_alliance.id IS '主键';
COMMENT ON COLUMN shz.cms_cross_city_alliance.name IS '城市名称';
COMMENT ON COLUMN shz.cms_cross_city_alliance.del_flag IS '删除标志0存在 2删除';
-- 种子8 个联盟城市(与原前端 mock 一致),仅在表为空时插入
INSERT INTO shz.cms_cross_city_alliance (name, create_by, create_time, del_flag)
SELECT * FROM (VALUES
('乌鲁木齐市', 'admin', now(), '0'),
('克拉玛依市', 'admin', now(), '0'),
('昌吉回族自治州', 'admin', now(), '0'),
('哈密市', 'admin', now(), '0'),
('吐鲁番市', 'admin', now(), '0'),
('喀什地区', 'admin', now(), '0'),
('阿克苏地区', 'admin', now(), '0'),
('伊犁哈萨克自治州','admin', now(), '0')
) AS seed(name, create_by, create_time, del_flag)
WHERE NOT EXISTS (SELECT 1 FROM shz.cms_cross_city_alliance);
-- 给已存在的页面菜单 2114 补上 perms仅当为空时
UPDATE sys_menu
SET perms = 'cms:crossCityFair:list', update_by = 'system', update_time = now()
WHERE menu_id = 2114 AND (perms IS NULL OR perms = '');
-- 新增 4 个按钮权限(仅在不存在时插入)
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT btn.menu_name, 2114, btn.order_num, '', NULL, NULL, '', 1, 0, 'F', '0', '0', btn.perms, '#', 'system', now(), '跨域联合招聘会-按钮'
FROM (VALUES
('联盟城市查询', 1, 'cms:crossCityFair:query'),
('联盟城市新增', 2, 'cms:crossCityFair:add'),
('联盟城市修改', 3, 'cms:crossCityFair:edit'),
('联盟城市删除', 4, 'cms:crossCityFair:remove')
) AS btn(menu_name, order_num, perms)
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = btn.perms);
-- 授权:把上述按钮权限授予拥有 menu_id=2114 的角色
WITH cross_roles AS (
SELECT DISTINCT role_id FROM sys_role_menu WHERE menu_id = 2114
),
new_menus AS (
SELECT menu_id FROM sys_menu
WHERE perms IN ('cms:crossCityFair:query','cms:crossCityFair:add','cms:crossCityFair:edit','cms:crossCityFair:remove')
),
rows AS (
SELECT cross_roles.role_id, new_menus.menu_id FROM cross_roles CROSS JOIN new_menus
)
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT rows.role_id, rows.menu_id FROM rows
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu ex WHERE ex.role_id = rows.role_id AND ex.menu_id = rows.menu_id);

View File

@@ -0,0 +1,7 @@
-- 公共招聘会:新增「跨域联盟城市」列,存逗号分隔的城市名称。
-- 依赖shz.public_job_fair 已存在shz.cms_cross_city_alliance 提供城市数据。
ALTER TABLE public_job_fair
ADD COLUMN IF NOT EXISTS cross_domain_cities VARCHAR(500);
COMMENT ON COLUMN public_job_fair.cross_domain_cities IS '跨域联盟城市(逗号分隔的城市名称)';