diff --git a/.claude/skills/inspect-shz-logs/SKILL.md b/.claude/skills/inspect-shz-logs/SKILL.md new file mode 100644 index 0000000..adf4bfc --- /dev/null +++ b/.claude/skills/inspect-shz-logs/SKILL.md @@ -0,0 +1,56 @@ +--- +name: inspect-shz-logs +description: Inspect and follow the SHZ backend's local and production logs through bounded, read-only commands. Use when Claude Code is working in the shz-backend project and needs to diagnose startup failures, exceptions, request behavior, scheduled jobs, or runtime incidents from local backend.log or the production backend, error, info, and user logs on the shz server. +--- + +# Inspect SHZ Logs + +Use the bundled script instead of assembling SSH commands manually. Keep every initial read bounded; follow logs only when the user explicitly asks to monitor live activity. + +## Inspect local logs + +Read the last 200 lines: + +```bash +.claude/skills/inspect-shz-logs/scripts/view.sh local 200 +``` + +Follow the local log interactively: + +```bash +.claude/skills/inspect-shz-logs/scripts/view.sh local 200 --follow +``` + +## Inspect production logs + +Read the main application log: + +```bash +.claude/skills/inspect-shz-logs/scripts/view.sh online backend 200 +``` + +Read the current error log: + +```bash +.claude/skills/inspect-shz-logs/scripts/view.sh online error 200 +``` + +Choose only one production log type: `backend`, `error`, `info`, or `user`. Add `--follow` as the final argument only for an explicit live-monitoring request. + +For keyword analysis, fetch a bounded window and filter locally, for example: + +```bash +.claude/skills/inspect-shz-logs/scripts/view.sh online error 1000 | rg -i 'exception|error|failed' +``` + +## Diagnose from logs + +1. Establish whether the request concerns local or production behavior. +2. Read 100–300 recent lines from the relevant log. +3. Expand up to the script's 5,000-line cap only when timestamps or stack traces require more context. +4. Correlate timestamps, thread names, exception causes, and nearby requests. +5. Report the evidence and likely cause without exposing tokens, passwords, personal data, or unrelated log content. + +Treat production access as read-only. Never restart services, edit files, deploy code, truncate logs, or run arbitrary remote commands through this skill. + +Read [references/log-locations.md](references/log-locations.md) only when checking server identity, paths, or supported log types. diff --git a/.claude/skills/inspect-shz-logs/references/log-locations.md b/.claude/skills/inspect-shz-logs/references/log-locations.md new file mode 100644 index 0000000..03f4b0a --- /dev/null +++ b/.claude/skills/inspect-shz-logs/references/log-locations.md @@ -0,0 +1,19 @@ +# Log locations + +## Local + +- Main log: `/.local/logs/backend.log` +- Producer: `/local.sh` + +## Production: shz + +- SSH: `root@39.98.44.136:6022` +- Application: `/opt/service/project/shz-backend/ruoyi-admin/target/ruoyi-admin.jar` +- Profile: `dev` +- HTTP port: `9091` +- Main process log: `/opt/service/project/shz-backend/logs/backend.log` +- Current error log: `/opt/service/project/shz-backend/logs/sys-error.log` +- Current info log: `/opt/service/project/shz-backend/logs/sys-info.log` +- Current user log: `/opt/service/project/shz-backend/logs/sys-user.log` + +Use only the bundled read-only viewer. Rotated files use date suffixes but are intentionally not exposed by the default script. diff --git a/.claude/skills/inspect-shz-logs/scripts/view.sh b/.claude/skills/inspect-shz-logs/scripts/view.sh new file mode 100755 index 0000000..bc7fd45 --- /dev/null +++ b/.claude/skills/inspect-shz-logs/scripts/view.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +project_dir="$(cd "${skill_dir}/../../.." && pwd)" +local_log="${project_dir}/.local/logs/backend.log" + +ssh_host="root@39.98.44.136" +ssh_port="6022" +remote_log_dir="/opt/service/project/shz-backend/logs" + +usage() { + cat <<'EOF' +Usage: + view.sh local [lines] [--follow] + view.sh online [backend|error|info|user] [lines] [--follow] + +Examples: + view.sh local 200 + view.sh local 100 --follow + view.sh online backend 200 + view.sh online error 500 --follow +EOF +} + +validate_lines() { + local lines="$1" + if [[ ! "${lines}" =~ ^[0-9]+$ ]] || (( lines < 1 || lines > 5000 )); then + echo "Lines must be an integer between 1 and 5000: ${lines}" >&2 + exit 2 + fi +} + +validate_follow() { + local follow="${1:-}" + if [[ -n "${follow}" && "${follow}" != "--follow" ]]; then + echo "Unknown option: ${follow}" >&2 + usage >&2 + exit 2 + fi +} + +view_local() { + local lines="${1:-200}" + local follow="${2:-}" + validate_lines "${lines}" + validate_follow "${follow}" + + if [[ ! -f "${local_log}" ]]; then + echo "Local log does not exist: ${local_log}" >&2 + echo "Start the project first with: ./local.sh start" >&2 + exit 1 + fi + + if [[ "${follow}" == "--follow" ]]; then + tail -n "${lines}" -F -- "${local_log}" + else + tail -n "${lines}" -- "${local_log}" + fi +} + +view_online() { + local kind="${1:-backend}" + local lines="${2:-200}" + local follow="${3:-}" + local remote_file + + case "${kind}" in + backend) remote_file="${remote_log_dir}/backend.log" ;; + error) remote_file="${remote_log_dir}/sys-error.log" ;; + info) remote_file="${remote_log_dir}/sys-info.log" ;; + user) remote_file="${remote_log_dir}/sys-user.log" ;; + *) + echo "Unknown online log type: ${kind}" >&2 + usage >&2 + exit 2 + ;; + esac + + validate_lines "${lines}" + validate_follow "${follow}" + + if [[ "${follow}" == "--follow" ]]; then + ssh -p "${ssh_port}" -o BatchMode=yes -o ConnectTimeout=10 "${ssh_host}" \ + tail -n "${lines}" -F -- "${remote_file}" + else + ssh -p "${ssh_port}" -o BatchMode=yes -o ConnectTimeout=10 "${ssh_host}" \ + tail -n "${lines}" -- "${remote_file}" + fi +} + +target="${1:-}" +case "${target}" in + local) + [[ $# -le 3 ]] || { usage >&2; exit 2; } + view_local "${2:-200}" "${3:-}" + ;; + online) + [[ $# -le 4 ]] || { usage >&2; exit 2; } + view_online "${2:-backend}" "${3:-200}" "${4:-}" + ;; + help|-h|--help) + usage + ;; + *) + usage >&2 + exit 2 + ;; +esac diff --git a/.claude/skills/query-shz-highgo/.password b/.claude/skills/query-shz-highgo/.password new file mode 100644 index 0000000..81bf242 --- /dev/null +++ b/.claude/skills/query-shz-highgo/.password @@ -0,0 +1 @@ +Hello@2026 diff --git a/.claude/skills/query-shz-highgo/SKILL.md b/.claude/skills/query-shz-highgo/SKILL.md new file mode 100644 index 0000000..99aa2b1 --- /dev/null +++ b/.claude/skills/query-shz-highgo/SKILL.md @@ -0,0 +1,66 @@ +--- +name: query-shz-highgo +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. +--- + +# SHZ HighGo SQL + +Two bundled scripts run SQL through the configured SSH gateway: + +- `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. +3. Run: + + ```bash + .claude/skills/query-shz-highgo/scripts/query.sh 'SELECT current_database(), current_user, current_schema()' + ``` + +4. Summarize the result and distinguish returned facts from inferences. + +## 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 + +List user tables: + +```bash +.claude/skills/query-shz-highgo/scripts/query.sh "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY 1, 2" +``` + +Inspect columns: + +```bash +.claude/skills/query-shz-highgo/scripts/query.sh "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'shz' AND table_name = 'lowercase_table_name' ORDER BY ordinal_position" +``` + +HighGo may expose unquoted identifiers in lowercase and quoted identifiers with exact case. Query `information_schema` first when the spelling is uncertain. + +## Connection details + +Read [references/connection.md](references/connection.md) only when troubleshooting connectivity or updating the endpoint. Do not print, quote, or expose the password file. diff --git a/.claude/skills/query-shz-highgo/references/connection.md b/.claude/skills/query-shz-highgo/references/connection.md new file mode 100644 index 0000000..a96f752 --- /dev/null +++ b/.claude/skills/query-shz-highgo/references/connection.md @@ -0,0 +1,12 @@ +# Connection reference + +- SSH gateway: `root@124.243.245.42` +- HighGo client: `/opt/HighGo4.5.7-see/bin/psql` +- Database endpoint: `39.98.44.136:6023` +- Database: `highgo` +- User: `sysdba` +- Default schemas: `shz,public` +- Detected server: HighGo Security Enterprise Edition Database System 4.5.10 +- Password source: `../.password`, mode `600`; override with `HIGHGO_PASSWORD_FILE` + +The SSH gateway must be reachable with the workstation's existing SSH key. Keep credentials out of commands, logs, responses, and `SKILL.md`. 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/.claude/skills/query-shz-highgo/scripts/query.sh b/.claude/skills/query-shz-highgo/scripts/query.sh new file mode 100755 index 0000000..2c88bde --- /dev/null +++ b/.claude/skills/query-shz-highgo/scripts/query.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +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 'SELECT ...'" >&2 + exit 2 +fi + +sql="$*" +if [[ ! "$sql" =~ ^[[:space:]]*(SELECT|select|WITH|with|SHOW|show|EXPLAIN|explain)([[:space:]]|$) ]]; then + echo "Rejected: only SELECT, WITH, SHOW, and EXPLAIN queries are allowed." >&2 + exit 3 +fi + +sql_without_final_semicolon="${sql%;}" +if [[ "$sql_without_final_semicolon" == *';'* ]]; then + echo "Rejected: multiple SQL statements are not allowed." >&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 default_transaction_read_only=on -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/.codex/skills.zip b/.codex/skills.zip deleted file mode 100644 index a2dcd36..0000000 Binary files a/.codex/skills.zip and /dev/null differ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/OutdoorFairJobMigration.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/OutdoorFairJobMigration.java new file mode 100644 index 0000000..7f75b1a --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/OutdoorFairJobMigration.java @@ -0,0 +1,65 @@ +package com.ruoyi.cms.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +/** + * 户外招聘会岗位关联表自动迁移。 + */ +@Slf4j +@Component +public class OutdoorFairJobMigration +{ + @Autowired + private DataSource dataSource; + + @EventListener(ApplicationReadyEvent.class) + public void migrate() + { + try (Connection connection = dataSource.getConnection(); + ResultSet tables = connection.getMetaData().getTables( + connection.getCatalog(), "shz", "cms_outdoor_fair_job", new String[]{"TABLE"})) + { + if (tables.next()) + { + log.info("户外招聘会岗位关联表已存在"); + return; + } + } + catch (Exception e) + { + log.error("检查户外招聘会岗位关联表失败: {}", e.getMessage(), e); + return; + } + + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) + { + statement.execute("CREATE SEQUENCE shz.cms_outdoor_fair_job_id_seq " + + "START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1"); + statement.execute("CREATE TABLE shz.cms_outdoor_fair_job (" + + "id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_job_id_seq') PRIMARY KEY, " + + "fair_id BIGINT NOT NULL, company_id BIGINT NOT NULL, " + + "job_id BIGINT 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')"); + statement.execute("CREATE UNIQUE INDEX uk_cms_outdoor_fair_job " + + "ON shz.cms_outdoor_fair_job (fair_id, job_id)"); + statement.execute("CREATE INDEX idx_cms_outdoor_fair_job_company " + + "ON shz.cms_outdoor_fair_job (fair_id, company_id)"); + log.info("户外招聘会岗位关联表检查完成"); + } + catch (Exception e) + { + log.error("户外招聘会岗位关联表迁移失败: {}", e.getMessage(), e); + } + } +} 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 f5b849d..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,22 +2,36 @@ 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; +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.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.OutdoorFairJobMapper; import com.ruoyi.cms.mapper.CompanyMapper; +import com.ruoyi.cms.service.IJobService; import com.ruoyi.cms.service.IOutdoorFairService; import com.ruoyi.cms.service.IVenueInfoService; import com.ruoyi.common.annotation.Log; @@ -31,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; @@ -72,6 +87,12 @@ public class OutdoorFairController extends BaseController @Autowired private OutdoorFairBoothMapper outdoorFairBoothMapper; + @Autowired + private OutdoorFairJobMapper outdoorFairJobMapper; + + @Autowired + private IJobService jobService; + /** * 查询户外招聘会列表 */ @@ -96,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; + } + /** * 新增户外招聘会 */ @@ -161,7 +401,8 @@ public class OutdoorFairController extends BaseController @ApiOperation("查询户外招聘会参会企业") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')") @GetMapping("/{fairId}/companies") - public AjaxResult listCompanies(@PathVariable Long fairId, String companyName, Integer current, Integer pageSize) + public AjaxResult listCompanies(@PathVariable Long fairId, String companyName, + Integer current, Integer pageSize) { List relations = fairCompanyMapper.selectList(new LambdaQueryWrapper() .eq(FairCompany::getJobFairId, fairId) @@ -182,6 +423,11 @@ public class OutdoorFairController extends BaseController .stream() .filter(item -> item.getCompanyId() != null) .collect(Collectors.toMap(OutdoorFairBoothBooking::getCompanyId, item -> item, (a, b) -> a)); + Map jobCountMap = outdoorFairJobMapper.countJobsByCompany(fairId).stream() + .collect(Collectors.toMap( + item -> Long.valueOf(String.valueOf(item.get("companyId"))), + item -> Long.valueOf(String.valueOf(item.get("jobCount"))), + (a, b) -> a)); List> rows = new ArrayList<>(); for (FairCompany relation : relations) { @@ -204,7 +450,7 @@ public class OutdoorFairController extends BaseController row.put("contactPhone", company.getContactPersonPhone()); row.put("boothId", booking == null ? null : booking.getBoothId()); row.put("boothNumber", booking == null ? null : booking.getBoothNumber()); - row.put("jobList", new ArrayList<>()); + row.put("jobCount", jobCountMap.getOrDefault(company.getCompanyId(), 0L)); rows.add(row); } @@ -219,6 +465,35 @@ public class OutdoorFairController extends BaseController return result; } + @ApiOperation("分页查询户外招聘会参会岗位") + @PreAuthorize("@ss.hasPermi('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) + { + int page = current == null || current < 1 ? 1 : current; + int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100); + int offset = (page - 1) * size; + List rows = outdoorFairJobMapper.selectFairJobPage( + fairId, companyId, companyName, jobTitle, education, experience, offset, size); + Long total = outdoorFairJobMapper.countFairJobs( + fairId, companyId, companyName, jobTitle, education, experience); + AjaxResult result = AjaxResult.success(); + result.put("rows", rows); + result.put("total", total == null ? 0 : total); + return result; + } + + @ApiOperation("获取户外招聘会岗位详情") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')") + @GetMapping("/{fairId}/jobs/{jobId}") + public AjaxResult getCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId) + { + ensureFairJobExists(fairId, jobId); + return success(jobService.selectJobByJobId(jobId)); + } + @ApiOperation("新增户外招聘会参会企业") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") @PostMapping("/{fairId}/companies") @@ -236,11 +511,90 @@ public class OutdoorFairController extends BaseController @ApiOperation("移除户外招聘会参会企业") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") @DeleteMapping("/{fairId}/companies/{id}") + @Transactional(rollbackFor = Exception.class) public AjaxResult removeCompany(@PathVariable Long fairId, @PathVariable Long id) { - return toAjax(fairCompanyMapper.delete(new LambdaQueryWrapper() + FairCompany relation = fairCompanyMapper.selectById(id); + if (relation == null || !fairId.equals(relation.getJobFairId())) { + return AjaxResult.error("参会企业不存在"); + } + outdoorFairJobMapper.delete(new LambdaQueryWrapper() + .eq(OutdoorFairJob::getFairId, fairId) + .eq(OutdoorFairJob::getCompanyId, relation.getCompanyId())); + return toAjax(fairCompanyMapper.deleteById(id)); + } + + @ApiOperation("新增户外招聘会岗位") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @Transactional(rollbackFor = Exception.class) + @PostMapping("/{fairId}/companies/{companyId}/jobs") + public AjaxResult addCompanyJob(@PathVariable Long fairId, @PathVariable Long companyId, + @RequestBody Job job) + { + ensureFairCompanyExists(fairId, companyId); + Company company = companyMapper.selectById(companyId); + job.setCompanyId(companyId); + job.setCompanyName(company.getName()); + if (job.getIsPublish() == null) { + job.setIsPublish(1); + } + int inserted = jobService.insertJob(job); + if (inserted <= 0) { + return AjaxResult.error("岗位新增失败"); + } + OutdoorFairJob relation = new OutdoorFairJob(); + relation.setFairId(fairId); + relation.setCompanyId(companyId); + relation.setJobId(job.getJobId()); + relation.setCreateBy(getUsername()); + outdoorFairJobMapper.insert(relation); + return success(job); + } + + @ApiOperation("修改户外招聘会岗位") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @PutMapping("/{fairId}/jobs/{jobId}") + public AjaxResult updateCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId, + @RequestBody Job job) + { + ensureFairJobExists(fairId, jobId); + job.setJobId(jobId); + // 当前弹窗不修改发布状态,避免重复触发岗位发布流程。 + job.setIsPublish(null); + return toAjax(jobService.updateJob(job)); + } + + @ApiOperation("删除户外招聘会岗位") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @Transactional(rollbackFor = Exception.class) + @DeleteMapping("/{fairId}/jobs/{jobId}") + public AjaxResult removeCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId) + { + ensureFairJobExists(fairId, jobId); + outdoorFairJobMapper.delete(new LambdaQueryWrapper() + .eq(OutdoorFairJob::getFairId, fairId) + .eq(OutdoorFairJob::getJobId, jobId)); + return toAjax(jobService.deleteJobByJobIds(new Long[]{jobId})); + } + + private void ensureFairCompanyExists(Long fairId, Long companyId) + { + Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper() .eq(FairCompany::getJobFairId, fairId) - .eq(FairCompany::getId, id))); + .eq(FairCompany::getCompanyId, companyId)); + if (count == null || count == 0) { + throw new ServiceException("该企业未参加当前招聘会"); + } + } + + private void ensureFairJobExists(Long fairId, Long jobId) + { + Long count = outdoorFairJobMapper.selectCount(new LambdaQueryWrapper() + .eq(OutdoorFairJob::getFairId, fairId) + .eq(OutdoorFairJob::getJobId, jobId)); + if (count == null || count == 0) { + throw new ServiceException("当前招聘会不存在该岗位"); + } } private void ensureFairCompany(Long fairId, Long companyId) 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/domain/OutdoorFairJob.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairJob.java new file mode 100644 index 0000000..040a3af --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairJob.java @@ -0,0 +1,24 @@ +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_outdoor_fair_job") +public class OutdoorFairJob extends BaseEntity +{ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long fairId; + + private Long companyId; + + private Long jobId; +} 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/mapper/OutdoorFairJobMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairJobMapper.java new file mode 100644 index 0000000..f9133e0 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairJobMapper.java @@ -0,0 +1,33 @@ +package com.ruoyi.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.cms.domain.Job; +import com.ruoyi.cms.domain.OutdoorFairJob; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 户外招聘会岗位关联 Mapper。 + */ +public interface OutdoorFairJobMapper extends BaseMapper +{ + List selectFairJobPage(@Param("fairId") Long fairId, + @Param("companyId") Long companyId, + @Param("companyName") String companyName, + @Param("jobTitle") String jobTitle, + @Param("education") String education, + @Param("experience") String experience, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + Long countFairJobs(@Param("fairId") Long fairId, + @Param("companyId") Long companyId, + @Param("companyName") String companyName, + @Param("jobTitle") String jobTitle, + @Param("education") String education, + @Param("experience") String experience); + + List> countJobsByCompany(@Param("fairId") Long fairId); +} 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/ESJobSearchImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java index c99a72c..8684aff 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java @@ -806,6 +806,7 @@ public class ESJobSearchImpl implements IESJobSearchService @Override public void deleteJob(Long jobId) { + ensureJobDocumentIndexExists(); LambdaEsQueryWrapper lambdaEsQueryWrapper = new LambdaEsQueryWrapper(); lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,jobId); esJobDocumentMapper.delete(lambdaEsQueryWrapper); @@ -847,12 +848,31 @@ public class ESJobSearchImpl implements IESJobSearchService if(StringUtil.isEmptyOrNull(job.getCompanyNature())){ esJobDocument.setCompanyNature("6"); } + ensureJobDocumentIndexExists(); + LambdaEsQueryWrapper lambdaEsQueryWrapper = new LambdaEsQueryWrapper(); lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,job.getJobId()); esJobDocumentMapper.delete(lambdaEsQueryWrapper); esJobDocumentMapper.insert(esJobDocument); } + /** + * 确保job_document索引存在;缺失时懒创建(仅建空索引与映射,不重建全量数据)。 + * process-index-mode: manual 下easy-es不会自动建索引,缺失时delete/update等写操作 + * 会抛 index_not_found_exception 导致业务接口500并回滚事务,故在写操作前调用以自愈。 + */ + private void ensureJobDocumentIndexExists() { + try { + if (!esJobDocumentMapper.existsIndex("job_document")) { + esJobDocumentMapper.createIndex(); + logger.info("job_document索引不存在,已自动创建"); + } + } catch (Exception e) { + // 并发场景下其他请求可能已创建索引,忽略“索引已存在”类异常 + logger.warn("自动创建job_document索引异常(可能已被其他请求创建,忽略)", e); + } + } + public List selectByIds(Long[] jobIds) { LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); wrapper.in(ESJobDocument::getJobId,jobIds); 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/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairServiceImpl.java index 97362ff..4ab28ec 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairServiceImpl.java @@ -40,6 +40,7 @@ public class OutdoorFairServiceImpl extends ServiceImpl + + + + + WHERE relation.fair_id = #{fairId} + AND (relation.del_flag IS NULL OR relation.del_flag = '0') + AND job.del_flag = '0' + + AND relation.company_id = #{companyId} + + + AND job.company_name LIKE concat( + '%', + cast(#{companyName, jdbcType=VARCHAR} as varchar), + '%' + ) + + + AND job.job_title LIKE concat( + '%', + cast(#{jobTitle, jdbcType=VARCHAR} as varchar), + '%' + ) + + + AND job.education = #{education} + + + AND job.experience = #{experience} + + + + + + + + + diff --git a/sql/cms_outdoor_fair_8_mock_jobs.sql b/sql/cms_outdoor_fair_8_mock_jobs.sql new file mode 100644 index 0000000..2898bb2 --- /dev/null +++ b/sql/cms_outdoor_fair_8_mock_jobs.sql @@ -0,0 +1,136 @@ +BEGIN; + +WITH companies(company_id, company_name, target_count) AS ( + VALUES + (4412::BIGINT, '瑜兴辉煌科技有限公司5', 6), + (3013::BIGINT, '瑜兴辉煌科技有限公司', 15), + (93001::BIGINT, '未知企业', 12), + (93000::BIGINT, '钵施然智能', 11), + (4414::BIGINT, '阿里巴巴', 8), + (4410::BIGINT, 'test616', 15), + (93002::BIGINT, '新疆石达赛特科技有限公司', 13), + (93005::BIGINT, '新疆智图信息科技有限公司', 5), + (93004::BIGINT, '新疆中汇新能环保科技有限公司', 15), + (93003::BIGINT, '新疆微诊医学检验有限公司', 14), + (93007::BIGINT, '街景梦工厂', 7), + (93009::BIGINT, '石河子市稼乐农业生产资料有限责任公司', 9) +), +templates( + seq_no, job_title, min_salary, max_salary, education, experience, + area_code, job_type, vacancies, job_location, description +) AS ( + VALUES + (1, '行政专员', 4000, 6000, '3', '0', '17', '0', 2, '石河子市东城街道', '负责日常行政事务、资料整理及部门协作。'), + (2, '人事专员', 4500, 6500, '4', '4', '16', '0', 2, '石河子市新城街道', '负责招聘、员工关系及人事档案管理。'), + (3, '财务会计', 5000, 7500, '4', '4', '15', '0', 2, '石河子市红山街道', '负责账务处理、报表编制和税务资料整理。'), + (4, '销售顾问', 4500, 9000, '3', '0', '19', '0', 6, '石河子市向阳街道', '负责客户开发、产品介绍、合同跟进和客户维护。'), + (5, '客户服务专员', 4000, 6000, '3', '0', '18', '0', 4, '石河子市老街街道', '负责客户咨询、售后问题处理及服务记录维护。'), + (6, '市场运营专员', 5000, 8000, '4', '4', '17', '0', 3, '石河子市东城街道', '负责市场活动策划、渠道运营和数据分析。'), + (7, '新媒体运营', 5000, 7500, '4', '2', '16', '0', 2, '石河子市新城街道', '负责公众号、短视频平台内容策划与运营。'), + (8, '软件开发工程师', 7000, 12000, '4', '4', '17', '0', 3, '石河子市开发区', '参与业务系统开发、接口联调、测试和技术文档编写。'), + (9, '实施运维工程师', 6000, 9000, '3', '4', '17', '0', 3, '石河子市开发区', '负责系统部署、用户培训、故障处理和日常运维。'), + (10, '设备维修技术员', 5500, 8500, '3', '4', '20', '0', 4, '石河子镇', '负责生产设备巡检、维护保养和故障维修。'), + (11, '质量检验员', 4500, 6500, '3', '0', '20', '0', 4, '石河子镇', '负责原料及产品质量检测、记录和异常反馈。'), + (12, '生产操作员', 4500, 7000, '2', '0', '20', '0', 10, '石河子镇', '按照生产规范完成操作、设备点检及现场整理。'), + (13, '农业技术员', 5000, 8000, '3', '4', '9', '0', 5, '石总场', '提供农业生产技术指导,开展田间巡查和技术服务。'), + (14, '仓储物流专员', 4500, 6500, '3', '0', '21', '0', 4, '北泉镇', '负责货物入库、盘点、出库及物流信息维护。'), + (15, '项目经理', 8000, 13000, '4', '5', '17', '0', 2, '石河子市开发区', '负责项目计划、资源协调、进度控制和交付验收。') +) +INSERT INTO shz.job ( + job_title, min_salary, max_salary, education, experience, + company_name, job_location, posting_date, vacancies, + del_flag, create_by, create_time, update_by, update_time, + company_id, is_hot, apply_num, job_location_area_code, + description, is_publish, data_source, job_type, job_address, + review_status +) +SELECT + templates.job_title, + templates.min_salary, + templates.max_salary, + templates.education, + templates.experience, + companies.company_name, + templates.job_location, + CURRENT_TIMESTAMP, + templates.vacancies, + '0', + 'admin', + CURRENT_TIMESTAMP, + 'admin', + CURRENT_TIMESTAMP, + companies.company_id, + '0', + '0', + templates.area_code, + templates.description, + 1, + '户外招聘会8模拟数据', + templates.job_type, + templates.job_location, + NULL +FROM companies +JOIN templates ON templates.seq_no <= companies.target_count +WHERE NOT EXISTS ( + SELECT 1 + FROM shz.job existing + WHERE existing.company_id = companies.company_id + AND existing.job_title = templates.job_title + AND existing.data_source = '户外招聘会8模拟数据' + AND existing.del_flag = '0' +); + +INSERT INTO shz.cms_outdoor_fair_job ( + fair_id, company_id, job_id, create_by, create_time, + update_by, update_time, del_flag +) +SELECT + 8, + job.company_id, + job.job_id, + 'admin', + CURRENT_TIMESTAMP, + 'admin', + CURRENT_TIMESTAMP, + '0' +FROM shz.job job +WHERE job.data_source = '户外招聘会8模拟数据' + AND job.del_flag = '0' + AND EXISTS ( + SELECT 1 + FROM shz.fair_company fair_company + WHERE fair_company.job_fair_id = 8 + AND fair_company.company_id = job.company_id + ) + AND NOT EXISTS ( + SELECT 1 + FROM shz.cms_outdoor_fair_job relation + WHERE relation.fair_id = 8 + AND relation.job_id = job.job_id + ); + +INSERT INTO shz.job_contact ( + job_id, contact_person, contact_person_phone, position, + del_flag, create_by, create_time, update_by, update_time +) +SELECT + job.job_id, + '招聘负责人', + '13909930008', + '人力资源', + '0', + 'admin', + CURRENT_TIMESTAMP, + 'admin', + CURRENT_TIMESTAMP +FROM shz.job job +WHERE job.data_source = '户外招聘会8模拟数据' + AND job.del_flag = '0' + AND NOT EXISTS ( + SELECT 1 + FROM shz.job_contact contact + WHERE contact.job_id = job.job_id + AND contact.del_flag = '0' + ); + +COMMIT; 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'); diff --git a/sql/cms_outdoor_fair_job.sql b/sql/cms_outdoor_fair_job.sql new file mode 100644 index 0000000..6f0b0be --- /dev/null +++ b/sql/cms_outdoor_fair_job.sql @@ -0,0 +1,23 @@ +CREATE SEQUENCE IF NOT EXISTS shz.cms_outdoor_fair_job_id_seq + START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; + +CREATE TABLE IF NOT EXISTS shz.cms_outdoor_fair_job ( + id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_job_id_seq'::regclass) PRIMARY KEY, + fair_id BIGINT NOT NULL, + company_id BIGINT NOT NULL, + job_id BIGINT 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' +); + +CREATE UNIQUE INDEX IF NOT EXISTS uk_cms_outdoor_fair_job + ON shz.cms_outdoor_fair_job (fair_id, job_id); + +CREATE INDEX IF NOT EXISTS idx_cms_outdoor_fair_job_company + ON shz.cms_outdoor_fair_job (fair_id, company_id); + +COMMENT ON TABLE shz.cms_outdoor_fair_job IS '户外招聘会岗位关联表'; diff --git a/sql/hide_indoor_fixed_booth_menu.sql b/sql/hide_indoor_fixed_booth_menu.sql new file mode 100644 index 0000000..ab57b38 --- /dev/null +++ b/sql/hide_indoor_fixed_booth_menu.sql @@ -0,0 +1,10 @@ +-- 隐藏并停用左侧菜单“室内固定摊位”。 +-- 保留菜单及角色关联数据,便于需要时恢复。 +UPDATE sys_menu +SET visible = '1', + status = '1', + update_by = 'admin', + update_time = CURRENT_TIMESTAMP, + remark = '已移除左侧菜单入口' +WHERE menu_id = 2112 + AND menu_name = '室内固定摊位';