feat: Add Outdoor Fair Attendee and Device management

- Implemented Outdoor Fair Attendee and Device entities, including their respective controllers, services, and mappers.
- Added statistics endpoint for Outdoor Fair data, allowing aggregation by day, week, month, or year.
- Introduced SQL scripts for creating tables and mock data for attendees and devices.
- Created a write-capable script for executing DML/DDL statements against the SHZ HighGo database.
- Enhanced the existing query capabilities to support both read and write operations.
This commit is contained in:
2026-06-26 11:26:04 +08:00
parent 7ca2d878cc
commit 0ccfb6e6dc
15 changed files with 884 additions and 5 deletions

View File

@@ -1,13 +1,18 @@
---
name: query-shz-highgo
description: Query the SHZ HighGo Security Enterprise Edition database through its SSH gateway using a guarded read-only psql workflow. Use when Claude Code is working in the shz-backend project and needs to inspect database schemas, tables, columns, row counts, or application data; diagnose data-related behavior; or execute SELECT, WITH, SHOW, or EXPLAIN statements against the project's HighGo database.
description: Run SQL against the SHZ HighGo Security Enterprise Edition database through its SSH gateway. Use when Claude Code is working in the shz-backend project and needs to inspect schemas, tables, columns, row counts, or application data; diagnose data-related behavior; or execute SELECT/WITH/SHOW/EXPLAIN. Also supports write operations — INSERT, UPDATE, DELETE, MERGE, CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE — when the user explicitly asks to add, modify, delete, or otherwise change data or schema.
---
# Query SHZ HighGo
# SHZ HighGo SQL
Use the bundled script to run read-only SQL through the configured SSH gateway.
Two bundled scripts run SQL through the configured SSH gateway:
## Query workflow
- `scripts/query.sh`**read-only**. Default for inspection and diagnostics.
- `scripts/modify.sh`**write-capable**. Use only when an actual change is intended and the user has asked for it.
Both set `search_path` to `shz,public`, apply a 30-second `statement_timeout`, reject multiple statements in one call, and base64-encode the SQL over the SSH channel. `query.sh` additionally enforces database-level `default_transaction_read_only=on` and accepts only `SELECT/WITH/SHOW/EXPLAIN`.
## Query workflow (read-only)
1. Form the narrowest useful query. Add a small `LIMIT` when inspecting rows.
2. For unfamiliar tables, inspect `information_schema.columns` before selecting business data.
@@ -19,7 +24,26 @@ Use the bundled script to run read-only SQL through the configured SSH gateway.
4. Summarize the result and distinguish returned facts from inferences.
The script accepts only statements beginning with `SELECT`, `WITH`, `SHOW`, or `EXPLAIN`, rejects multiple statements, sets `search_path` to `shz,public`, enforces database-level read-only mode, and applies a 30-second timeout. Never bypass these controls or perform writes unless the user explicitly requests a separate write-capable workflow.
## Modify workflow (writes)
⚠️ These statements change data or schema on a production database. Confirm the exact statement with the user before running it, and prefer a `WHERE` clause or scoped target. Read unfamiliar tables with `query.sh` first.
1. Inspect the target with `query.sh` (columns, existing rows) so the statement is well-formed.
2. Run the write:
```bash
.claude/skills/query-shz-highgo/scripts/modify.sh "INSERT INTO shz.some_table (col) VALUES ('val')"
.claude/skills/query-shz-highgo/scripts/modify.sh "UPDATE shz.some_table SET col = 'val' WHERE id = 1"
.claude/skills/query-shz-highgo/scripts/modify.sh "DELETE FROM shz.some_table WHERE id = 1"
.claude/skills/query-shz-highgo/scripts/modify.sh "CREATE TABLE shz.example (id bigint PRIMARY KEY)"
```
3. Verify the effect with a follow-up `query.sh` `SELECT` and report rows affected.
Notes:
- Only one statement per call. To run several, invoke the script multiple times.
- Transactions across calls are NOT supported (each call is its own session). psql autocommits each statement.
- `DROP`/`TRUNCATE`/`DELETE` without a `WHERE` are destructive — re-state the impact to the user and wait for explicit go-ahead.
## Schema discovery

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
# Write-capable counterpart to query.sh. Runs a single DML/DDL statement
# against the SHZ HighGo database through the SSH gateway. Read-only mode
# is NOT applied — only use when an actual change is intended.
skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
password_file="${HIGHGO_PASSWORD_FILE:-$skill_dir/.password}"
ssh_host="root@124.243.245.42"
psql_path="/opt/HighGo4.5.7-see/bin/psql"
db_host="39.98.44.136"
db_port="6023"
db_name="highgo"
db_user="sysdba"
if [[ ! -r "$password_file" ]]; then
echo "HighGo password file is missing or unreadable: $password_file" >&2
exit 2
fi
if [[ $# -eq 0 ]]; then
echo "Usage: $0 'INSERT ...' | 'UPDATE ...' | 'DELETE ...' | 'CREATE ...' | 'ALTER ...' | 'DROP ...'" >&2
exit 2
fi
sql="$*"
# Reject multiple statements — prevents a trailing destructive command from
# being smuggled in after the intended one. A single optional trailing
# semicolon is tolerated.
sql_without_final_semicolon="${sql%;}"
if [[ "$sql_without_final_semicolon" == *';'* ]]; then
echo "Rejected: multiple SQL statements are not allowed. Run them one at a time." >&2
exit 3
fi
password="$(<"$password_file")"
sql_base64="$(printf '%s' "$sql" | base64 | tr -d '\n')"
{
printf '%s\n' "$password"
printf '%s\n' "$sql_base64"
} | ssh -o BatchMode=yes -o ConnectTimeout=10 "$ssh_host" \
"read -r PGPASSWORD; read -r SQL_BASE64; export PGPASSWORD; export PGOPTIONS='-c search_path=shz,public -c statement_timeout=30000'; SQL=\$(printf '%s' \"\$SQL_BASE64\" | base64 -d); exec '$psql_path' -X --no-psqlrc -h '$db_host' -p '$db_port' -U '$db_user' -d '$db_name' -v ON_ERROR_STOP=1 -P pager=off -c \"\$SQL\""

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.OutdoorFairAttendee;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
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-25
*/
@RestController
@RequestMapping("/cms/outdoor-fair-attendee")
@Api(tags = "后台:户外招聘会签到人员")
public class OutdoorFairAttendeeController extends BaseController
{
@Autowired
private IOutdoorFairAttendeeService outdoorFairAttendeeService;
/**
* 查询签到人员列表(支持按人员姓名、手机号模糊查询)
*/
@ApiOperation("查询签到人员列表")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/list")
public TableDataInfo list(OutdoorFairAttendee query)
{
startPage();
List<OutdoorFairAttendee> list = outdoorFairAttendeeService.selectAttendeeList(query);
return getDataTable(list);
}
/**
* 获取签到人员详细信息
*/
@ApiOperation("获取签到人员详细信息")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(outdoorFairAttendeeService.getById(id));
}
/**
* 新增签到人员
*/
@ApiOperation("新增签到人员")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
@Log(title = "户外招聘会签到人员", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody OutdoorFairAttendee attendee)
{
return toAjax(outdoorFairAttendeeService.save(attendee));
}
/**
* 修改签到人员
*/
@ApiOperation("修改签到人员")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Log(title = "户外招聘会签到人员", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody OutdoorFairAttendee attendee)
{
return toAjax(outdoorFairAttendeeService.updateById(attendee));
}
/**
* 删除签到人员
*/
@ApiOperation("删除签到人员")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:remove')")
@Log(title = "户外招聘会签到人员", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(outdoorFairAttendeeService.removeByIds(Arrays.asList(ids)));
}
}

View File

@@ -2,10 +2,20 @@ package com.ruoyi.cms.controller.cms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.cms.domain.FairCompany;
@@ -35,6 +45,7 @@ import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -106,6 +117,225 @@ public class OutdoorFairController extends BaseController
return success(outdoorFairService.getById(id));
}
/**
* 招聘会数据统计dashboard
* 按年/周/月/日维度,统计举办时间落在区间内的招聘会:
* - 累计发布招聘会数、参会单位数、职位数;
* - 招聘会数同比(去年同期等长区间)、环比(紧邻的上一等长区间);
* - 各时间桶的招聘会数/参会单位数/职位数序列(用于曲线图)。
* granularity 取值day/week/month/year默认 month。未传时间时统计全部招聘会
* (“累计”口径),曲线图区间按数据实际举办时间的最小/最大值生成。
*/
@ApiOperation("招聘会数据统计")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/statistics")
public AjaxResult statistics(
String granularity,
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime)
{
String gran = (StringUtils.isEmpty(granularity) ? "month" : granularity.toLowerCase());
boolean hasRange = startTime != null || endTime != null;
// 区间右端含当天start 为当天 00:00end 为次日 00:00开区间
LocalDate startLd = null;
LocalDate endLd = null;
Date start = null;
Date end = null;
if (hasRange) {
endLd = endTime != null ? toLocalDate(endTime) : LocalDate.now();
startLd = startTime != null ? toLocalDate(startTime)
: endLd.minusMonths(11).withDayOfMonth(1);
start = toDate(startLd.atStartOfDay(ZoneId.systemDefault()).toInstant());
end = toDate(endLd.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
}
// 区间内招聘会(未传区间时查全部,用于“累计”口径)
LambdaQueryWrapper<OutdoorFair> fairWrapper = new LambdaQueryWrapper<OutdoorFair>()
.orderByAsc(OutdoorFair::getHoldTime);
if (start != null) {
fairWrapper.ge(OutdoorFair::getHoldTime, start);
}
if (end != null) {
fairWrapper.lt(OutdoorFair::getHoldTime, end);
}
List<OutdoorFair> fairs = outdoorFairService.list(fairWrapper);
// 未传区间时,按数据实际举办时间的最小/最大值生成曲线图区间
if (!hasRange) {
LocalDate minD = null;
LocalDate maxD = null;
for (OutdoorFair f : fairs) {
if (f.getHoldTime() == null) {
continue;
}
LocalDate d = toLocalDate(f.getHoldTime());
if (minD == null || d.isBefore(minD)) {
minD = d;
}
if (maxD == null || d.isAfter(maxD)) {
maxD = d;
}
}
if (minD == null) {
startLd = LocalDate.now().withDayOfMonth(1);
endLd = LocalDate.now();
} else {
startLd = minD;
endLd = maxD;
}
}
// 每场参会单位/职位数(一次查回,内存分组,避免逐场 N 次查询)
Map<Long, Long> companyByFair = new HashMap<>();
Map<Long, Long> jobByFair = new HashMap<>();
if (!fairs.isEmpty()) {
List<Long> fairIds = fairs.stream().map(OutdoorFair::getId).collect(Collectors.toList());
for (FairCompany fc : fairCompanyMapper.selectList(
new LambdaQueryWrapper<FairCompany>().in(FairCompany::getJobFairId, fairIds))) {
companyByFair.merge(fc.getJobFairId(), 1L, Long::sum);
}
for (OutdoorFairJob ojf : outdoorFairJobMapper.selectList(
new LambdaQueryWrapper<OutdoorFairJob>().in(OutdoorFairJob::getFairId, fairIds))) {
jobByFair.merge(ojf.getFairId(), 1L, Long::sum);
}
}
// 生成区间内全部时间桶(含空桶),按桶累加
List<LocalDate> buckets = buildBuckets(startLd, endLd, gran);
Map<LocalDate, long[]> agg = new TreeMap<>();
for (LocalDate b : buckets) {
agg.put(b, new long[]{0, 0, 0});
}
long fairTotal = 0L, companyTotal = 0L, jobTotal = 0L;
for (OutdoorFair f : fairs) {
if (f.getHoldTime() == null) {
continue;
}
LocalDate b = bucketOf(toLocalDate(f.getHoldTime()), gran);
long[] arr = agg.get(b);
if (arr == null) {
continue;
}
long cc = companyByFair.getOrDefault(f.getId(), 0L);
long jc = jobByFair.getOrDefault(f.getId(), 0L);
arr[0] += 1;
arr[1] += cc;
arr[2] += jc;
fairTotal += 1;
companyTotal += cc;
jobTotal += jc;
}
DateTimeFormatter fmt = formatterFor(gran);
List<Map<String, Object>> series = new ArrayList<>();
for (LocalDate b : buckets) {
long[] arr = agg.get(b);
Map<String, Object> row = new LinkedHashMap<>();
row.put("time", b.format(fmt));
row.put("fairCount", arr[0]);
row.put("companyCount", arr[1]);
row.put("jobCount", arr[2]);
series.add(row);
}
// 同比:去年同期等长区间;环比:紧邻的上一等长区间
long yoyPrev = countFairs(startLd.minusYears(1), endLd.minusYears(1));
long rangeDays = ChronoUnit.DAYS.between(startLd, endLd) + 1;
long momPrev = countFairs(startLd.minusDays(rangeDays), endLd.minusDays(rangeDays));
Map<String, Object> result = new LinkedHashMap<>();
result.put("granularity", gran);
result.put("totals", buildTotals(fairTotal, companyTotal, jobTotal));
result.put("yoy", buildRate(fairTotal, yoyPrev));
result.put("mom", buildRate(fairTotal, momPrev));
result.put("series", series);
return AjaxResult.success(result);
}
/** 统计 [startLd, endLd](含当天)区间内的招聘会数 */
private long countFairs(LocalDate startLd, LocalDate endLd) {
Date ds = toDate(startLd.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date de = toDate(endLd.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
return outdoorFairService.count(new LambdaQueryWrapper<OutdoorFair>()
.ge(OutdoorFair::getHoldTime, ds).lt(OutdoorFair::getHoldTime, de));
}
private LocalDate toLocalDate(Date d) {
return d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
private Date toDate(Instant instant) {
return Date.from(instant);
}
/** 按粒度取所属时间桶的起始日期 */
private LocalDate bucketOf(LocalDate ld, String gran) {
switch (gran) {
case "day":
return ld;
case "week":
return ld.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
case "year":
return ld.withDayOfYear(1);
default:
return ld.withDayOfMonth(1);
}
}
/** 生成区间内按粒度的全部时间桶(含空桶) */
private List<LocalDate> buildBuckets(LocalDate startLd, LocalDate endLd, String gran) {
List<LocalDate> out = new ArrayList<>();
LocalDate cur = bucketOf(startLd, gran);
LocalDate endBucket = bucketOf(endLd, gran);
while (!cur.isAfter(endBucket)) {
out.add(cur);
switch (gran) {
case "day":
cur = cur.plusDays(1);
break;
case "week":
cur = cur.plusWeeks(1);
break;
case "year":
cur = cur.plusYears(1);
break;
default:
cur = cur.plusMonths(1);
break;
}
}
return out;
}
private DateTimeFormatter formatterFor(String gran) {
switch (gran) {
case "day":
return DateTimeFormatter.ofPattern("yyyy-MM-dd");
case "week":
return DateTimeFormatter.ofPattern("yyyy-'W'ww");
case "year":
return DateTimeFormatter.ofPattern("yyyy");
default:
return DateTimeFormatter.ofPattern("yyyy-MM");
}
}
private Map<String, Object> buildTotals(long fair, long company, long job) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("fairCount", fair);
m.put("companyCount", company);
m.put("jobCount", job);
return m;
}
private Map<String, Object> buildRate(long current, long previous) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("fairCount", current);
m.put("previousFairCount", previous);
m.put("rate", previous == 0 ? null : (double) (current - previous) / (double) previous);
return m;
}
/**
* 新增户外招聘会
*/

View File

@@ -0,0 +1,108 @@
package com.ruoyi.cms.controller.cms;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.service.IOutdoorFairDeviceService;
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-26
*/
@RestController
@RequestMapping("/cms/outdoor-fair-device")
@Api(tags = "后台:户外招聘会设备码")
public class OutdoorFairDeviceController extends BaseController
{
@Autowired
private IOutdoorFairDeviceService outdoorFairDeviceService;
/**
* 查询设备码列表(支持按设备码、企业名称模糊查询)
*/
@ApiOperation("查询设备码列表")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/list")
public TableDataInfo list(OutdoorFairDevice query)
{
startPage();
List<OutdoorFairDevice> list = outdoorFairDeviceService.selectDeviceList(query);
return getDataTable(list);
}
/**
* 获取设备码详细信息
*/
@ApiOperation("获取设备码详细信息")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(outdoorFairDeviceService.getById(id));
}
/**
* 批量生成设备码UUID
* 请求体:{ fairId, quantity }
*/
@ApiOperation("批量生成设备码")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
@Log(title = "户外招聘会设备码", businessType = BusinessType.INSERT)
@PostMapping("/batch")
public AjaxResult batch(@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);
return result;
}
/**
* 绑定/解绑参会企业
* 请求体:{ companyId }companyId 为空表示解绑
*/
@ApiOperation("绑定/解绑参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Log(title = "户外招聘会设备码", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/bind")
public AjaxResult bind(@PathVariable("id") Long id, @RequestBody Map<String, Object> request)
{
Long companyId = request.get("companyId") == null ? null : Long.valueOf(String.valueOf(request.get("companyId")));
return toAjax(outdoorFairDeviceService.bindCompany(id, companyId));
}
/**
* 删除设备码
*/
@ApiOperation("删除设备码")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:remove')")
@Log(title = "户外招聘会设备码", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(outdoorFairDeviceService.removeByIds(Arrays.asList(ids)));
}
}

View File

@@ -0,0 +1,37 @@
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.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 户外招聘会签到人员。
*/
@Data
@TableName("shz.cms_outdoor_fair_attendee")
public class OutdoorFairAttendee extends BaseEntity
{
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 招聘会ID */
private Long fairId;
/** 人员姓名 */
private String name;
/** 手机号 */
private String phone;
/** 住址 */
private String address;
/** 签到时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date checkInTime;
}

View File

@@ -0,0 +1,37 @@
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.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 户外招聘会设备码。
*/
@Data
@TableName("shz.cms_outdoor_fair_device")
public class OutdoorFairDevice extends BaseEntity
{
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 招聘会ID */
private Long fairId;
/** 设备码UUID */
private String deviceCode;
/** 绑定的参会企业ID */
private Long companyId;
/** 绑定的参会企业名称(快照) */
private String companyName;
/** 绑定时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date bindTime;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
/**
* 户外招聘会签到人员 Mapper。
*/
public interface OutdoorFairAttendeeMapper extends BaseMapper<OutdoorFairAttendee>
{
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairDevice;
/**
* 户外招聘会设备码 Mapper。
*/
public interface OutdoorFairDeviceMapper extends BaseMapper<OutdoorFairDevice>
{
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import java.util.List;
/**
* 户外招聘会签到人员 Service。
*/
public interface IOutdoorFairAttendeeService extends IService<OutdoorFairAttendee>
{
/**
* 查询签到人员列表(支持按人员姓名、手机号模糊查询)。
*/
List<OutdoorFairAttendee> selectAttendeeList(OutdoorFairAttendee query);
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import java.util.List;
/**
* 户外招聘会设备码 Service。
*/
public interface IOutdoorFairDeviceService extends IService<OutdoorFairDevice>
{
/**
* 查询设备码列表(支持按设备码、企业名称模糊查询)。
*/
List<OutdoorFairDevice> selectDeviceList(OutdoorFairDevice query);
/**
* 批量生成设备码UUID
*
* @param fairId 招聘会ID
* @param quantity 生成数量
* @return 已生成的设备码列表
*/
List<OutdoorFairDevice> batchGenerate(Long fairId, Integer quantity);
/**
* 绑定/解绑参会企业companyId 为空表示解绑)。
*/
boolean bindCompany(Long id, Long companyId);
}

View File

@@ -0,0 +1,32 @@
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.OutdoorFairAttendee;
import com.ruoyi.cms.mapper.OutdoorFairAttendeeMapper;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 户外招聘会签到人员 Service 实现。
*/
@Service
public class OutdoorFairAttendeeServiceImpl
extends ServiceImpl<OutdoorFairAttendeeMapper, OutdoorFairAttendee>
implements IOutdoorFairAttendeeService
{
@Override
public List<OutdoorFairAttendee> selectAttendeeList(OutdoorFairAttendee query)
{
LambdaQueryWrapper<OutdoorFairAttendee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(query.getFairId() != null, OutdoorFairAttendee::getFairId, query.getFairId());
wrapper.like(StringUtils.isNotBlank(query.getName()), OutdoorFairAttendee::getName, query.getName());
wrapper.like(StringUtils.isNotBlank(query.getPhone()), OutdoorFairAttendee::getPhone, query.getPhone());
wrapper.orderByDesc(OutdoorFairAttendee::getCheckInTime);
wrapper.orderByDesc(OutdoorFairAttendee::getId);
return list(wrapper);
}
}

View File

@@ -0,0 +1,112 @@
package com.ruoyi.cms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.FairCompany;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.service.IOutdoorFairDeviceService;
import com.ruoyi.common.core.domain.entity.Company;
import com.ruoyi.common.exception.ServiceException;
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 实现。
*/
@Service
public class OutdoorFairDeviceServiceImpl
extends ServiceImpl<OutdoorFairDeviceMapper, OutdoorFairDevice>
implements IOutdoorFairDeviceService
{
@Autowired
private FairCompanyMapper fairCompanyMapper;
@Autowired
private CompanyMapper companyMapper;
@Override
public List<OutdoorFairDevice> selectDeviceList(OutdoorFairDevice query)
{
LambdaQueryWrapper<OutdoorFairDevice> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(query.getFairId() != null, OutdoorFairDevice::getFairId, query.getFairId());
wrapper.eq(query.getCompanyId() != null, OutdoorFairDevice::getCompanyId, query.getCompanyId());
wrapper.like(StringUtils.isNotBlank(query.getDeviceCode()), OutdoorFairDevice::getDeviceCode, query.getDeviceCode());
wrapper.like(StringUtils.isNotBlank(query.getCompanyName()), OutdoorFairDevice::getCompanyName, query.getCompanyName());
wrapper.orderByDesc(OutdoorFairDevice::getId);
return list(wrapper);
}
@Override
public List<OutdoorFairDevice> batchGenerate(Long fairId, Integer quantity)
{
if (fairId == null) {
throw new ServiceException("招聘会ID不能为空");
}
int count = quantity == null ? 0 : quantity;
if (count <= 0) {
throw new ServiceException("生成数量必须大于0");
}
if (count > 1000) {
throw new ServiceException("单次生成数量不能超过1000");
}
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);
}
saveBatch(devices);
return devices;
}
@Override
public boolean bindCompany(Long id, Long companyId)
{
OutdoorFairDevice device = getById(id);
if (device == null) {
throw new ServiceException("设备码不存在");
}
// 使用 UpdateWrapper 显式 set避免 updateById 默认跳过 null 字段导致解绑失效
UpdateWrapper<OutdoorFairDevice> wrapper = new UpdateWrapper<>();
wrapper.eq("id", id);
if (companyId != null) {
// 仅允许绑定已参加当前招聘会的企业
Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, device.getFairId())
.eq(FairCompany::getCompanyId, companyId));
if (count == null || count == 0) {
throw new ServiceException("该企业未参加当前招聘会");
}
// 同一招聘会下,一个企业只能绑定一个设备码
Long bound = baseMapper.selectCount(new LambdaQueryWrapper<OutdoorFairDevice>()
.eq(OutdoorFairDevice::getFairId, device.getFairId())
.eq(OutdoorFairDevice::getCompanyId, companyId));
if (bound != null && bound > 0) {
throw new ServiceException("该企业已绑定其他设备码");
}
Company company = companyMapper.selectById(companyId);
wrapper.set("company_id", companyId)
.set("company_name", company == null ? null : company.getName())
.set("bind_time", new Date());
}
else {
// 解绑:显式置空
wrapper.set("company_id", null)
.set("company_name", null)
.set("bind_time", null);
}
return baseMapper.update(null, wrapper) > 0;
}
}

View File

@@ -0,0 +1,42 @@
-- 户外招聘会签到人员表
-- 依赖shz.cms_outdoor_fairfair_id 外键逻辑关联,不建物理外键)
CREATE SEQUENCE IF NOT EXISTS shz.cms_outdoor_fair_attendee_id_seq
START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS shz.cms_outdoor_fair_attendee (
id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_attendee_id_seq'::regclass) PRIMARY KEY,
fair_id BIGINT NOT NULL,
name VARCHAR(64) NOT NULL,
phone VARCHAR(20),
address VARCHAR(255),
check_in_time TIMESTAMP,
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0'
);
CREATE INDEX IF NOT EXISTS idx_cms_outdoor_fair_attendee_fair
ON shz.cms_outdoor_fair_attendee (fair_id);
COMMENT ON TABLE shz.cms_outdoor_fair_attendee IS '户外招聘会签到人员表';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.id IS '主键';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.fair_id IS '招聘会ID';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.name IS '人员姓名';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.phone IS '手机号';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.address IS '住址';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.check_in_time IS '签到时间';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.del_flag IS '删除标志0存在 2删除';
-- mock 5 条签到人员数据,归属 id=8 的招聘会
INSERT INTO shz.cms_outdoor_fair_attendee
(fair_id, name, phone, address, check_in_time, create_by, create_time, del_flag)
VALUES
(8, '张伟', '13800000001', '石河子市东城街道北一路上段', '2026-06-25 09:05:00', 'admin', now(), '0'),
(8, '李娜', '13900000002', '石河子市新城街道西一路社区', '2026-06-25 09:12:00', 'admin', now(), '0'),
(8, '王强', '13700000003', '石河子市红山街道三小区', '2026-06-25 09:20:00', 'admin', now(), '0'),
(8, '陈静', '13600000004', '石河子市向阳街道一社区', '2026-06-25 09:31:00', 'admin', now(), '0'),
(8, '刘洋', '13500000005', '石河子市老街街道五小区', '2026-06-25 09:45:00', 'admin', now(), '0');

View File

@@ -0,0 +1,42 @@
-- 户外招聘会设备码表
-- 依赖shz.cms_outdoor_fairfair_id 外键逻辑关联,不建物理外键)
CREATE SEQUENCE IF NOT EXISTS shz.cms_outdoor_fair_device_id_seq
START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS shz.cms_outdoor_fair_device (
id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_device_id_seq'::regclass) PRIMARY KEY,
fair_id BIGINT NOT NULL,
device_code VARCHAR(64) NOT NULL,
company_id BIGINT,
company_name VARCHAR(255),
bind_time TIMESTAMP,
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
remark VARCHAR(500),
del_flag CHAR(1) DEFAULT '0'
);
CREATE INDEX IF NOT EXISTS idx_cms_outdoor_fair_device_fair
ON shz.cms_outdoor_fair_device (fair_id);
COMMENT ON TABLE shz.cms_outdoor_fair_device IS '户外招聘会设备码表';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.id IS '主键';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.fair_id IS '招聘会ID';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.device_code IS '设备码UUID';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.company_id IS '绑定的参会企业ID';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.company_name IS '绑定的参会企业名称';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.bind_time IS '绑定时间';
COMMENT ON COLUMN shz.cms_outdoor_fair_device.del_flag IS '删除标志0存在 2删除';
-- mock 数据:为 id=8 的招聘会生成 5 个设备码,其中 2 个绑定参会企业
INSERT INTO shz.cms_outdoor_fair_device
(fair_id, device_code, company_id, company_name, bind_time, create_by, create_time, del_flag)
VALUES
(8, 'a1b2c3d4e5f6471a9b8c1d2e3f4a5b6c', null, null, null, 'admin', now(), '0'),
(8, 'b2c3d4e5f6a7482bac9d0e1f2a3b4c5d', null, null, null, 'admin', now(), '0'),
(8, 'c3d4e5f6a7b8493cbd0e1f2a3b4c5d6e', 101, '示例科技有限公司', '2026-06-25 10:00:00', 'admin', now(), '0'),
(8, 'd4e5f6a7b8c9404dce1f2a3b4c5d6e7f', 102, '示范商贸有限公司', '2026-06-25 10:05:00', 'admin', now(), '0'),
(8, 'e5f6a7b8c9d0415edf2a3b4c5d6e7f8a', null, null, null, 'admin', now(), '0');