diff --git a/.claude/skills/query-shz-highgo/SKILL.md b/.claude/skills/query-shz-highgo/SKILL.md index a59645d..99aa2b1 100644 --- a/.claude/skills/query-shz-highgo/SKILL.md +++ b/.claude/skills/query-shz-highgo/SKILL.md @@ -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 diff --git a/.claude/skills/query-shz-highgo/scripts/modify.sh b/.claude/skills/query-shz-highgo/scripts/modify.sh new file mode 100755 index 0000000..c10056d --- /dev/null +++ b/.claude/skills/query-shz-highgo/scripts/modify.sh @@ -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\"" diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java new file mode 100644 index 0000000..eb34f61 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java @@ -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 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))); + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java index 097dbaa..3f85249 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java @@ -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:00,end 为次日 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 fairWrapper = new LambdaQueryWrapper() + .orderByAsc(OutdoorFair::getHoldTime); + if (start != null) { + fairWrapper.ge(OutdoorFair::getHoldTime, start); + } + if (end != null) { + fairWrapper.lt(OutdoorFair::getHoldTime, end); + } + List 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 companyByFair = new HashMap<>(); + Map jobByFair = new HashMap<>(); + if (!fairs.isEmpty()) { + List fairIds = fairs.stream().map(OutdoorFair::getId).collect(Collectors.toList()); + for (FairCompany fc : fairCompanyMapper.selectList( + new LambdaQueryWrapper().in(FairCompany::getJobFairId, fairIds))) { + companyByFair.merge(fc.getJobFairId(), 1L, Long::sum); + } + for (OutdoorFairJob ojf : outdoorFairJobMapper.selectList( + new LambdaQueryWrapper().in(OutdoorFairJob::getFairId, fairIds))) { + jobByFair.merge(ojf.getFairId(), 1L, Long::sum); + } + } + + // 生成区间内全部时间桶(含空桶),按桶累加 + List buckets = buildBuckets(startLd, endLd, gran); + Map 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> series = new ArrayList<>(); + for (LocalDate b : buckets) { + long[] arr = agg.get(b); + Map 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 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() + .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 buildBuckets(LocalDate startLd, LocalDate endLd, String gran) { + List 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 buildTotals(long fair, long company, long job) { + Map m = new LinkedHashMap<>(); + m.put("fairCount", fair); + m.put("companyCount", company); + m.put("jobCount", job); + return m; + } + + private Map buildRate(long current, long previous) { + Map m = new LinkedHashMap<>(); + m.put("fairCount", current); + m.put("previousFairCount", previous); + m.put("rate", previous == 0 ? null : (double) (current - previous) / (double) previous); + return m; + } + /** * 新增户外招聘会 */ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairDeviceController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairDeviceController.java new file mode 100644 index 0000000..a84c5c5 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairDeviceController.java @@ -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 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 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 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 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))); + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java new file mode 100644 index 0000000..0c37448 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairDevice.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairDevice.java new file mode 100644 index 0000000..b4de7ce --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairDevice.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java new file mode 100644 index 0000000..98e7c1c --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java @@ -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 +{ +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairDeviceMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairDeviceMapper.java new file mode 100644 index 0000000..db78ff2 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairDeviceMapper.java @@ -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 +{ +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java new file mode 100644 index 0000000..3281453 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java @@ -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 +{ + /** + * 查询签到人员列表(支持按人员姓名、手机号模糊查询)。 + */ + List selectAttendeeList(OutdoorFairAttendee query); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairDeviceService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairDeviceService.java new file mode 100644 index 0000000..f63bb39 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairDeviceService.java @@ -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 +{ + /** + * 查询设备码列表(支持按设备码、企业名称模糊查询)。 + */ + List selectDeviceList(OutdoorFairDevice query); + + /** + * 批量生成设备码(UUID)。 + * + * @param fairId 招聘会ID + * @param quantity 生成数量 + * @return 已生成的设备码列表 + */ + List batchGenerate(Long fairId, Integer quantity); + + /** + * 绑定/解绑参会企业(companyId 为空表示解绑)。 + */ + boolean bindCompany(Long id, Long companyId); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java new file mode 100644 index 0000000..3231aaa --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java @@ -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 + implements IOutdoorFairAttendeeService +{ + @Override + public List selectAttendeeList(OutdoorFairAttendee query) + { + LambdaQueryWrapper 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); + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairDeviceServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairDeviceServiceImpl.java new file mode 100644 index 0000000..149ba4c --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairDeviceServiceImpl.java @@ -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 + implements IOutdoorFairDeviceService +{ + @Autowired + private FairCompanyMapper fairCompanyMapper; + + @Autowired + private CompanyMapper companyMapper; + + @Override + public List selectDeviceList(OutdoorFairDevice query) + { + LambdaQueryWrapper 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 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 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 wrapper = new UpdateWrapper<>(); + wrapper.eq("id", id); + if (companyId != null) { + // 仅允许绑定已参加当前招聘会的企业 + Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper() + .eq(FairCompany::getJobFairId, device.getFairId()) + .eq(FairCompany::getCompanyId, companyId)); + if (count == null || count == 0) { + throw new ServiceException("该企业未参加当前招聘会"); + } + // 同一招聘会下,一个企业只能绑定一个设备码 + Long bound = baseMapper.selectCount(new LambdaQueryWrapper() + .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; + } +} diff --git a/sql/cms_outdoor_fair_attendee.sql b/sql/cms_outdoor_fair_attendee.sql new file mode 100644 index 0000000..2a21bbd --- /dev/null +++ b/sql/cms_outdoor_fair_attendee.sql @@ -0,0 +1,42 @@ +-- 户外招聘会签到人员表 +-- 依赖:shz.cms_outdoor_fair(fair_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'); diff --git a/sql/cms_outdoor_fair_device.sql b/sql/cms_outdoor_fair_device.sql new file mode 100644 index 0000000..c98bede --- /dev/null +++ b/sql/cms_outdoor_fair_device.sql @@ -0,0 +1,42 @@ +-- 户外招聘会设备码表 +-- 依赖:shz.cms_outdoor_fair(fair_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');