feat: Add Cross-Domain Job Fair and Alliance City Management

- Implemented AppOutdoorFairController for outdoor job fair public H5 interfaces.
- Created CrossCityAllianceController for managing cross-domain alliance cities.
- Added CrossCityAlliance domain model and corresponding service and mapper.
- Developed CrossDomainJobVO for aggregating job data across job fairs.
- Created SQL scripts for initializing the cross-city alliance table and modifying the public job fair schema.
- Added migration script for backing up and restoring the HighGo database schema.
This commit is contained in:
2026-06-27 22:44:56 +08:00
parent 0ccfb6e6dc
commit b35ed5989e
29 changed files with 1969 additions and 50 deletions

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>