From c3de38ee825dd3c59018eebe45116512f69db59c Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 25 Jun 2026 15:53:01 +0800 Subject: [PATCH] feat: add inspect-shz-logs skill for log inspection and monitoring - Implemented a new skill to inspect and follow SHZ backend logs with bounded read-only commands. - Added scripts for viewing local and production logs, including options for following logs. - Created references for log locations and usage guidelines. feat: add query-shz-highgo skill for database querying - Introduced a skill to query the SHZ HighGo database through an SSH gateway using a read-only psql workflow. - Included scripts for executing SQL queries and schema discovery. - Added connection details and usage instructions. feat: add OutdoorFairBooth domain and mapper - Created OutdoorFairBooth domain model with relevant fields and annotations. - Implemented a mapper interface for physical deletion of records based on fair ID. feat: create SQL scripts for cms_outdoor_fair_booth - Added SQL script to create the cms_outdoor_fair_booth table and its associated sequence. - Included comments for clarity and compatibility with historical data. - Added a script to drop the cms_venue_booth table after migration confirmation. --- .codex/skills.zip | Bin 0 -> 16898 bytes .codex/skills/inspect-shz-logs/SKILL.md | 56 +++++ .../inspect-shz-logs/agents/openai.yaml | 4 + .../references/log-locations.md | 19 ++ .../skills/inspect-shz-logs/scripts/view.sh | 109 +++++++++ .codex/skills/query-shz-highgo/SKILL.md | 42 ++++ .../query-shz-highgo/agents/openai.yaml | 4 + .../query-shz-highgo/references/connection.md | 12 + .../skills/query-shz-highgo/scripts/query.sh | 43 ++++ .gitignore | 4 +- .../controller/cms/OutdoorFairController.java | 161 +++++++++++++- .../controller/cms/VenueInfoController.java | 210 ++++++++++-------- ...{VenueBooth.java => OutdoorFairBooth.java} | 16 +- .../mapper/OutdoorFairBoothBookingMapper.java | 10 + .../cms/mapper/OutdoorFairBoothMapper.java | 24 ++ .../ruoyi/cms/mapper/VenueBoothMapper.java | 24 -- sql/cms_outdoor_fair_booth_snapshot.sql | 46 ++++ sql/cms_venue_booth_map.sql | 86 ------- sql/drop_cms_venue_booth.sql | 5 + 19 files changed, 665 insertions(+), 210 deletions(-) create mode 100644 .codex/skills.zip create mode 100644 .codex/skills/inspect-shz-logs/SKILL.md create mode 100644 .codex/skills/inspect-shz-logs/agents/openai.yaml create mode 100644 .codex/skills/inspect-shz-logs/references/log-locations.md create mode 100755 .codex/skills/inspect-shz-logs/scripts/view.sh create mode 100644 .codex/skills/query-shz-highgo/SKILL.md create mode 100644 .codex/skills/query-shz-highgo/agents/openai.yaml create mode 100644 .codex/skills/query-shz-highgo/references/connection.md create mode 100755 .codex/skills/query-shz-highgo/scripts/query.sh rename ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/{VenueBooth.java => OutdoorFairBooth.java} (66%) create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothMapper.java delete mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java create mode 100644 sql/cms_outdoor_fair_booth_snapshot.sql delete mode 100644 sql/cms_venue_booth_map.sql create mode 100644 sql/drop_cms_venue_booth.sql diff --git a/.codex/skills.zip b/.codex/skills.zip new file mode 100644 index 0000000000000000000000000000000000000000..a2dcd36f60460095823f07c97fe0ec65b11c300e GIT binary patch literal 16898 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/.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/.codex/skills/inspect-shz-logs/scripts/view.sh b/.codex/skills/inspect-shz-logs/scripts/view.sh new file mode 100755 index 0000000..bc7fd45 --- /dev/null +++ b/.codex/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/.codex/skills/query-shz-highgo/SKILL.md b/.codex/skills/query-shz-highgo/SKILL.md new file mode 100644 index 0000000..1d1b059 --- /dev/null +++ b/.codex/skills/query-shz-highgo/SKILL.md @@ -0,0 +1,42 @@ +--- +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 Codex 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. +--- + +# Query SHZ HighGo + +Use the bundled script to run read-only SQL through the configured SSH gateway. + +## Query workflow + +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 + .codex/skills/query-shz-highgo/scripts/query.sh 'SELECT current_database(), current_user, current_schema()' + ``` + +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. + +## Schema discovery + +List user tables: + +```bash +.codex/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 +.codex/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/.codex/skills/query-shz-highgo/agents/openai.yaml b/.codex/skills/query-shz-highgo/agents/openai.yaml new file mode 100644 index 0000000..330b230 --- /dev/null +++ b/.codex/skills/query-shz-highgo/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "查询 SHZ 翰高数据库" + short_description: "通过 SSH 网关安全执行本项目翰高数据库只读 SQL 查询" + default_prompt: "Use $query-shz-highgo to run a read-only query against this project's SHZ HighGo database." diff --git a/.codex/skills/query-shz-highgo/references/connection.md b/.codex/skills/query-shz-highgo/references/connection.md new file mode 100644 index 0000000..a96f752 --- /dev/null +++ b/.codex/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/.codex/skills/query-shz-highgo/scripts/query.sh b/.codex/skills/query-shz-highgo/scripts/query.sh new file mode 100755 index 0000000..2c88bde --- /dev/null +++ b/.codex/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/.gitignore b/.gitignore index a86bdd0..fb41a99 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,5 @@ nbdist/ # Local application runtime files .local/ -.codex/ -local.sh \ No newline at end of file + +local.sh 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 e289dbe..f5b849d 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 @@ -1,23 +1,38 @@ package com.ruoyi.cms.controller.cms; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.cms.domain.FairCompany; 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.VenueInfo; +import com.ruoyi.cms.mapper.FairCompanyMapper; +import com.ruoyi.cms.mapper.OutdoorFairBoothMapper; +import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper; +import com.ruoyi.cms.mapper.CompanyMapper; import com.ruoyi.cms.service.IOutdoorFairService; import com.ruoyi.cms.service.IVenueInfoService; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.Company; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -45,6 +60,18 @@ public class OutdoorFairController extends BaseController @Autowired private IVenueInfoService venueInfoService; + @Autowired + private FairCompanyMapper fairCompanyMapper; + + @Autowired + private CompanyMapper companyMapper; + + @Autowired + private OutdoorFairBoothBookingMapper outdoorFairBoothBookingMapper; + + @Autowired + private OutdoorFairBoothMapper outdoorFairBoothMapper; + /** * 查询户外招聘会列表 */ @@ -75,11 +102,16 @@ public class OutdoorFairController extends BaseController @ApiOperation("新增户外招聘会") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')") @Log(title = "户外招聘会", businessType = BusinessType.INSERT) + @Transactional(rollbackFor = Exception.class) @PostMapping public AjaxResult add(@RequestBody OutdoorFair outdoorFair) { fillVenueFields(outdoorFair); - return toAjax(outdoorFairService.save(outdoorFair)); + boolean saved = outdoorFairService.save(outdoorFair); + if (saved) { + resetBoothSnapshot(outdoorFair, true); + } + return toAjax(saved); } /** @@ -88,11 +120,18 @@ public class OutdoorFairController extends BaseController @ApiOperation("修改户外招聘会") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") @Log(title = "户外招聘会", businessType = BusinessType.UPDATE) + @Transactional(rollbackFor = Exception.class) @PutMapping public AjaxResult edit(@RequestBody OutdoorFair outdoorFair) { + OutdoorFair oldFair = outdoorFair.getId() == null ? null : outdoorFairService.getById(outdoorFair.getId()); fillVenueFields(outdoorFair); - return toAjax(outdoorFairService.updateById(outdoorFair)); + boolean updated = outdoorFairService.updateById(outdoorFair); + if (updated) { + boolean venueChanged = oldFair == null || !java.util.Objects.equals(oldFair.getVenueId(), outdoorFair.getVenueId()); + resetBoothSnapshot(outdoorFair, venueChanged); + } + return toAjax(updated); } /** @@ -119,6 +158,120 @@ public class OutdoorFairController extends BaseController return toAjax(outdoorFairService.insertOutdoorFairDictData(request.getDictType(), request.getDictLabel(), getUsername())); } + @ApiOperation("查询户外招聘会参会企业") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')") + @GetMapping("/{fairId}/companies") + public AjaxResult listCompanies(@PathVariable Long fairId, String companyName, Integer current, Integer pageSize) + { + List relations = fairCompanyMapper.selectList(new LambdaQueryWrapper() + .eq(FairCompany::getJobFairId, fairId) + .orderByDesc(FairCompany::getId)); + if (relations.isEmpty()) { + AjaxResult result = AjaxResult.success(); + result.put("rows", new ArrayList<>()); + result.put("total", 0); + return result; + } + + List companyIds = relations.stream().map(FairCompany::getCompanyId).collect(Collectors.toList()); + Map companyMap = companyMapper.selectBatchIds(companyIds).stream() + .collect(Collectors.toMap(Company::getCompanyId, item -> item, (a, b) -> a)); + Map bookingMap = outdoorFairBoothBookingMapper.selectList(new LambdaQueryWrapper() + .eq(OutdoorFairBoothBooking::getFairId, fairId) + .eq(OutdoorFairBoothBooking::getStatus, "reserved")) + .stream() + .filter(item -> item.getCompanyId() != null) + .collect(Collectors.toMap(OutdoorFairBoothBooking::getCompanyId, item -> item, (a, b) -> a)); + + List> rows = new ArrayList<>(); + for (FairCompany relation : relations) { + Company company = companyMap.get(relation.getCompanyId()); + if (company == null) { + continue; + } + if (StringUtils.isNotBlank(companyName) && (company.getName() == null || !company.getName().contains(companyName))) { + continue; + } + OutdoorFairBoothBooking booking = bookingMap.get(company.getCompanyId()); + Map row = new HashMap<>(); + row.put("id", relation.getId()); + row.put("fairId", fairId); + row.put("companyId", company.getCompanyId()); + row.put("companyName", company.getName()); + row.put("industry", company.getIndustry()); + row.put("scale", company.getScale()); + row.put("contactPerson", company.getContactPerson()); + 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<>()); + rows.add(row); + } + + int total = rows.size(); + int page = current == null || current < 1 ? 1 : current; + int size = pageSize == null || pageSize < 1 ? 10 : pageSize; + int fromIndex = Math.min((page - 1) * size, total); + int toIndex = Math.min(fromIndex + size, total); + AjaxResult result = AjaxResult.success(); + result.put("rows", rows.subList(fromIndex, toIndex)); + result.put("total", total); + return result; + } + + @ApiOperation("新增户外招聘会参会企业") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @PostMapping("/{fairId}/companies") + public AjaxResult addCompany(@PathVariable Long fairId, @RequestBody Map request) + { + Long companyId = Long.valueOf(String.valueOf(request.get("companyId"))); + Company company = companyMapper.selectById(companyId); + if (company == null || company.getStatus() == null || company.getStatus() != 1) { + return AjaxResult.error("只能添加审核通过的企业"); + } + ensureFairCompany(fairId, companyId); + return success(); + } + + @ApiOperation("移除户外招聘会参会企业") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @DeleteMapping("/{fairId}/companies/{id}") + public AjaxResult removeCompany(@PathVariable Long fairId, @PathVariable Long id) + { + return toAjax(fairCompanyMapper.delete(new LambdaQueryWrapper() + .eq(FairCompany::getJobFairId, fairId) + .eq(FairCompany::getId, id))); + } + + private void ensureFairCompany(Long fairId, Long companyId) + { + Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper() + .eq(FairCompany::getJobFairId, fairId) + .eq(FairCompany::getCompanyId, companyId)); + if (count != null && count > 0) { + return; + } + FairCompany fairCompany = new FairCompany(); + fairCompany.setJobFairId(fairId); + fairCompany.setCompanyId(companyId); + fairCompanyMapper.insert(fairCompany); + } + + private void resetBoothSnapshot(OutdoorFair outdoorFair, boolean forceRecreate) + { + if (outdoorFair.getId() == null) { + return; + } + if (forceRecreate) { + outdoorFairBoothBookingMapper.physicalDeleteByFairId(outdoorFair.getId()); + outdoorFairBoothMapper.physicalDeleteByFairId(outdoorFair.getId()); + } + Long boothCount = outdoorFairBoothMapper.selectCount(new LambdaQueryWrapper() + .eq(OutdoorFairBooth::getFairId, outdoorFair.getId())); + outdoorFair.setBoothCount(boothCount == null ? 0 : boothCount.intValue()); + outdoorFairService.updateById(outdoorFair); + } + private void fillVenueFields(OutdoorFair outdoorFair) { if (outdoorFair.getVenueId() == null) { @@ -129,6 +282,8 @@ public class OutdoorFairController extends BaseController throw new ServiceException("绑定场地不存在"); } outdoorFair.setVenueName(venueInfo.getVenueName()); - outdoorFair.setBoothCount(venueInfo.getFloorCount() == null ? 0 : venueInfo.getFloorCount()); + if (outdoorFair.getBoothCount() == null) { + outdoorFair.setBoothCount(0); + } } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java index c8e4414..d3ba639 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/VenueInfoController.java @@ -12,14 +12,16 @@ import java.util.stream.Collectors; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.cms.domain.VenueInfo; import com.ruoyi.cms.domain.OutdoorFair; +import com.ruoyi.cms.domain.OutdoorFairBooth; import com.ruoyi.cms.domain.OutdoorFairBoothBooking; -import com.ruoyi.cms.domain.VenueBooth; +import com.ruoyi.cms.domain.FairCompany; import com.ruoyi.cms.domain.VenueInfoDictDataRequest; import com.ruoyi.cms.domain.VenueBoothSwapRequest; import com.ruoyi.cms.mapper.CompanyMapper; +import com.ruoyi.cms.mapper.FairCompanyMapper; +import com.ruoyi.cms.mapper.OutdoorFairBoothMapper; import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper; import com.ruoyi.cms.mapper.OutdoorFairMapper; -import com.ruoyi.cms.mapper.VenueBoothMapper; import com.ruoyi.cms.service.IVenueInfoService; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; @@ -57,18 +59,21 @@ public class VenueInfoController extends BaseController @Autowired private IVenueInfoService venueInfoService; - @Autowired - private VenueBoothMapper venueBoothMapper; - @Autowired private OutdoorFairMapper outdoorFairMapper; @Autowired private OutdoorFairBoothBookingMapper outdoorFairBoothBookingMapper; + @Autowired + private OutdoorFairBoothMapper outdoorFairBoothMapper; + @Autowired private CompanyMapper companyMapper; + @Autowired + private FairCompanyMapper fairCompanyMapper; + /** * 查询场地信息列表 */ @@ -143,78 +148,6 @@ public class VenueInfoController extends BaseController return toAjax(venueInfoService.insertVenueInfoDictData(request.getDictType(), request.getDictLabel(), getUsername())); } - @ApiOperation("查询场地展位图") - @PreAuthorize("@ss.hasAnyPermi('cms:venueInfo:query,cms:outdoorFair:query')") - @GetMapping("/{venueId}/booths") - public AjaxResult listBooths(@PathVariable Long venueId) - { - List list = venueBoothMapper.selectList(new LambdaQueryWrapper() - .eq(VenueBooth::getVenueId, venueId) - .orderByAsc(VenueBooth::getPositionY) - .orderByAsc(VenueBooth::getPositionX) - .orderByAsc(VenueBooth::getBoothNumber)); - return success(list); - } - - @ApiOperation("保存场地展位图") - @PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')") - @Transactional(rollbackFor = Exception.class) - @PostMapping("/{venueId}/booths") - public AjaxResult saveBooths(@PathVariable Long venueId, @RequestBody List booths) - { - venueBoothMapper.physicalDeleteDeletedByVenueId(venueId); - List existingBooths = venueBoothMapper.selectList(new LambdaQueryWrapper() - .eq(VenueBooth::getVenueId, venueId)); - Map existingById = existingBooths.stream() - .filter(item -> item.getId() != null) - .collect(Collectors.toMap(VenueBooth::getId, item -> item, (a, b) -> a)); - Map existingByNumber = existingBooths.stream() - .filter(item -> StringUtils.isNotBlank(item.getBoothNumber())) - .collect(Collectors.toMap(VenueBooth::getBoothNumber, item -> item, (a, b) -> a)); - Set boothNumbers = new HashSet<>(); - Set savedIds = new HashSet<>(); - if (booths != null) { - for (VenueBooth booth : booths) { - if (StringUtils.isBlank(booth.getBoothNumber())) { - return AjaxResult.error("展位号不能为空"); - } - if (!boothNumbers.add(booth.getBoothNumber())) { - return AjaxResult.error("展位号不能重复:" + booth.getBoothNumber()); - } - } - for (VenueBooth booth : booths) { - VenueBooth existing = booth.getId() == null ? null : existingById.get(booth.getId()); - if (existing == null) { - existing = existingByNumber.get(booth.getBoothNumber()); - } - booth.setVenueId(venueId); - if (existing == null) { - booth.setId(null); - venueBoothMapper.insert(booth); - } else { - booth.setId(existing.getId()); - venueBoothMapper.updateById(booth); - } - if (booth.getId() != null) { - savedIds.add(booth.getId()); - } - } - VenueInfo venueInfo = venueInfoService.getById(venueId); - if (venueInfo != null) { - venueInfo.setFloorCount(booths.size()); - venueInfoService.updateById(venueInfo); - } - } - List removedIds = existingBooths.stream() - .map(VenueBooth::getId) - .filter(id -> id != null && !savedIds.contains(id)) - .collect(Collectors.toList()); - if (!removedIds.isEmpty()) { - venueBoothMapper.physicalDeleteByVenueIdAndIds(venueId, removedIds); - } - return success(); - } - @ApiOperation("查询户外招聘会展位图") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')") @GetMapping("/fair/{fairId}/booth-map") @@ -224,16 +157,16 @@ public class VenueInfoController extends BaseController if (fair == null || fair.getVenueId() == null) { return success(new ArrayList<>()); } - List booths = venueBoothMapper.selectList(new LambdaQueryWrapper() - .eq(VenueBooth::getVenueId, fair.getVenueId()) - .orderByAsc(VenueBooth::getPositionY) - .orderByAsc(VenueBooth::getPositionX) - .orderByAsc(VenueBooth::getBoothNumber)); + List booths = outdoorFairBoothMapper.selectList(new LambdaQueryWrapper() + .eq(OutdoorFairBooth::getFairId, fairId) + .orderByAsc(OutdoorFairBooth::getPositionY) + .orderByAsc(OutdoorFairBooth::getPositionX) + .orderByAsc(OutdoorFairBooth::getBoothNumber)); List bookings = outdoorFairBoothBookingMapper.selectList(new LambdaQueryWrapper() .eq(OutdoorFairBoothBooking::getFairId, fairId)); Map bookingMap = bookings.stream().collect(Collectors.toMap(OutdoorFairBoothBooking::getBoothId, item -> item, (a, b) -> a)); List> result = new ArrayList<>(); - for (VenueBooth booth : booths) { + for (OutdoorFairBooth booth : booths) { OutdoorFairBoothBooking booking = bookingMap.get(booth.getId()); Map row = new java.util.HashMap<>(); row.put("id", booth.getId()); @@ -250,8 +183,85 @@ public class VenueInfoController extends BaseController return success(result); } + @ApiOperation("保存户外招聘会展位图副本") + @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @Transactional(rollbackFor = Exception.class) + @PostMapping("/fair/{fairId}/booths") + public AjaxResult saveFairBooths(@PathVariable Long fairId, @RequestBody List booths) + { + OutdoorFair fair = outdoorFairMapper.selectById(fairId); + if (fair == null || fair.getVenueId() == null) { + return AjaxResult.error("当前招聘会未绑定场地"); + } + List existingBooths = outdoorFairBoothMapper.selectList(new LambdaQueryWrapper() + .eq(OutdoorFairBooth::getFairId, fairId)); + Map existingById = existingBooths.stream() + .filter(item -> item.getId() != null) + .collect(Collectors.toMap(OutdoorFairBooth::getId, item -> item, (a, b) -> a)); + Map existingByNumber = existingBooths.stream() + .filter(item -> StringUtils.isNotBlank(item.getBoothNumber())) + .collect(Collectors.toMap(OutdoorFairBooth::getBoothNumber, item -> item, (a, b) -> a)); + Set boothNumbers = new HashSet<>(); + Set savedIds = new HashSet<>(); + if (booths != null) { + for (OutdoorFairBooth booth : booths) { + if (StringUtils.isBlank(booth.getBoothNumber())) { + return AjaxResult.error("展位号不能为空"); + } + if (!boothNumbers.add(booth.getBoothNumber())) { + return AjaxResult.error("展位号不能重复:" + booth.getBoothNumber()); + } + } + for (OutdoorFairBooth booth : booths) { + OutdoorFairBooth existing = booth.getId() == null ? null : existingById.get(booth.getId()); + if (existing == null) { + existing = existingByNumber.get(booth.getBoothNumber()); + } + booth.setFairId(fairId); + booth.setSourceVenueId(fair.getVenueId()); + if (existing == null) { + booth.setId(null); + outdoorFairBoothMapper.insert(booth); + } else { + booth.setId(existing.getId()); + if (booth.getSourceBoothId() == null) { + booth.setSourceBoothId(existing.getSourceBoothId()); + } + outdoorFairBoothMapper.updateById(booth); + } + if (booth.getId() != null) { + savedIds.add(booth.getId()); + List bookings = outdoorFairBoothBookingMapper.selectList(new LambdaQueryWrapper() + .eq(OutdoorFairBoothBooking::getFairId, fairId) + .eq(OutdoorFairBoothBooking::getBoothId, booth.getId())); + for (OutdoorFairBoothBooking booking : bookings) { + booking.setBoothNumber(booth.getBoothNumber()); + outdoorFairBoothBookingMapper.updateById(booking); + } + } + } + } + List removedIds = existingBooths.stream() + .map(OutdoorFairBooth::getId) + .filter(id -> id != null && !savedIds.contains(id)) + .collect(Collectors.toList()); + if (!removedIds.isEmpty()) { + Long bookedRemovedCount = outdoorFairBoothBookingMapper.selectCount(new LambdaQueryWrapper() + .eq(OutdoorFairBoothBooking::getFairId, fairId) + .in(OutdoorFairBoothBooking::getBoothId, removedIds)); + if (bookedRemovedCount != null && bookedRemovedCount > 0) { + return AjaxResult.error("不能删除已预定展位,请先取消预定"); + } + outdoorFairBoothMapper.physicalDeleteByFairIdAndIds(fairId, removedIds); + } + fair.setBoothCount(booths == null ? 0 : booths.size()); + outdoorFairMapper.updateById(fair); + return success(); + } + @ApiOperation("预定户外招聘会展位") @PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')") + @Transactional(rollbackFor = Exception.class) @PostMapping("/fair/{fairId}/booth-booking") public AjaxResult bookBooth(@PathVariable Long fairId, @RequestBody OutdoorFairBoothBooking booking) { @@ -262,20 +272,28 @@ public class VenueInfoController extends BaseController if (company == null || company.getStatus() == null || company.getStatus() != 1) { return AjaxResult.error("只能预定审核通过的企业"); } - VenueBooth booth = venueBoothMapper.selectById(booking.getBoothId()); + OutdoorFairBooth booth = outdoorFairBoothMapper.selectById(booking.getBoothId()); + if (booth == null || !fairId.equals(booth.getFairId())) { + return AjaxResult.error("展位不存在或不属于当前招聘会"); + } booking.setFairId(fairId); - booking.setBoothNumber(booth == null ? booking.getBoothNumber() : booth.getBoothNumber()); + booking.setBoothNumber(booth.getBoothNumber()); booking.setCompanyName(company.getName()); booking.setStatus(StringUtils.isBlank(booking.getStatus()) ? "reserved" : booking.getStatus()); + outdoorFairBoothBookingMapper.physicalDeleteDeletedByFairIdAndBoothId(fairId, booking.getBoothId()); OutdoorFairBoothBooking existing = outdoorFairBoothBookingMapper.selectOne(new LambdaQueryWrapper() .eq(OutdoorFairBoothBooking::getFairId, fairId) .eq(OutdoorFairBoothBooking::getBoothId, booking.getBoothId()) .last("limit 1")); if (existing == null) { - return toAjax(outdoorFairBoothBookingMapper.insert(booking)); + int rows = outdoorFairBoothBookingMapper.insert(booking); + ensureFairCompany(fairId, booking.getCompanyId()); + return toAjax(rows); } booking.setId(existing.getId()); - return toAjax(outdoorFairBoothBookingMapper.updateById(booking)); + int rows = outdoorFairBoothBookingMapper.updateById(booking); + ensureFairCompany(fairId, booking.getCompanyId()); + return toAjax(rows); } @ApiOperation("取消户外招聘会展位预定") @@ -283,9 +301,7 @@ public class VenueInfoController extends BaseController @DeleteMapping("/fair/{fairId}/booth-booking/{boothId}") public AjaxResult cancelBooth(@PathVariable Long fairId, @PathVariable Long boothId) { - return toAjax(outdoorFairBoothBookingMapper.delete(new LambdaQueryWrapper() - .eq(OutdoorFairBoothBooking::getFairId, fairId) - .eq(OutdoorFairBoothBooking::getBoothId, boothId))); + return toAjax(outdoorFairBoothBookingMapper.physicalDeleteByFairIdAndBoothId(fairId, boothId)); } @ApiOperation("调换户外招聘会展位") @@ -313,6 +329,20 @@ public class VenueInfoController extends BaseController return success(); } + private void ensureFairCompany(Long fairId, Long companyId) + { + Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper() + .eq(FairCompany::getJobFairId, fairId) + .eq(FairCompany::getCompanyId, companyId)); + if (count != null && count > 0) { + return; + } + FairCompany fairCompany = new FairCompany(); + fairCompany.setJobFairId(fairId); + fairCompany.setCompanyId(companyId); + fairCompanyMapper.insert(fairCompany); + } + private void fillHandleFields(VenueInfo venueInfo) { if (venueInfo.getHandleDate() == null) { diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairBooth.java similarity index 66% rename from ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java rename to ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairBooth.java index c948bd7..64ed82b 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/VenueBooth.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairBooth.java @@ -9,17 +9,23 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data -@ApiModel("场地展位") -@TableName("cms_venue_booth") -public class VenueBooth extends BaseEntity +@ApiModel("户外招聘会展位副本") +@TableName("cms_outdoor_fair_booth") +public class OutdoorFairBooth extends BaseEntity { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; - @ApiModelProperty("场地ID") - private Long venueId; + @ApiModelProperty("招聘会ID") + private Long fairId; + + @ApiModelProperty("来源场地ID") + private Long sourceVenueId; + + @ApiModelProperty("来源场地展位ID") + private Long sourceBoothId; @ApiModelProperty("展位号") private String boothNumber; diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java index 4736774..765fa1b 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothBookingMapper.java @@ -2,7 +2,17 @@ package com.ruoyi.cms.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.cms.domain.OutdoorFairBoothBooking; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; public interface OutdoorFairBoothBookingMapper extends BaseMapper { + @Delete("DELETE FROM cms_outdoor_fair_booth_booking WHERE fair_id = #{fairId} AND booth_id = #{boothId} AND del_flag <> '0'") + int physicalDeleteDeletedByFairIdAndBoothId(@Param("fairId") Long fairId, @Param("boothId") Long boothId); + + @Delete("DELETE FROM cms_outdoor_fair_booth_booking WHERE fair_id = #{fairId} AND booth_id = #{boothId}") + int physicalDeleteByFairIdAndBoothId(@Param("fairId") Long fairId, @Param("boothId") Long boothId); + + @Delete("DELETE FROM cms_outdoor_fair_booth_booking WHERE fair_id = #{fairId}") + int physicalDeleteByFairId(@Param("fairId") Long fairId); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothMapper.java new file mode 100644 index 0000000..1f7e6d9 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairBoothMapper.java @@ -0,0 +1,24 @@ +package com.ruoyi.cms.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.cms.domain.OutdoorFairBooth; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface OutdoorFairBoothMapper extends BaseMapper +{ + @Delete("DELETE FROM cms_outdoor_fair_booth WHERE fair_id = #{fairId}") + int physicalDeleteByFairId(@Param("fairId") Long fairId); + + @Delete({ + "" + }) + int physicalDeleteByFairIdAndIds(@Param("fairId") Long fairId, @Param("ids") List ids); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java deleted file mode 100644 index f2fe589..0000000 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/VenueBoothMapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ruoyi.cms.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.cms.domain.VenueBooth; -import org.apache.ibatis.annotations.Delete; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -public interface VenueBoothMapper extends BaseMapper -{ - @Delete("DELETE FROM cms_venue_booth WHERE venue_id = #{venueId} AND del_flag <> '0'") - int physicalDeleteDeletedByVenueId(@Param("venueId") Long venueId); - - @Delete({ - "" - }) - int physicalDeleteByVenueIdAndIds(@Param("venueId") Long venueId, @Param("ids") List ids); -} diff --git a/sql/cms_outdoor_fair_booth_snapshot.sql b/sql/cms_outdoor_fair_booth_snapshot.sql new file mode 100644 index 0000000..e6c8df7 --- /dev/null +++ b/sql/cms_outdoor_fair_booth_snapshot.sql @@ -0,0 +1,46 @@ +-- 户外招聘会展位副本:招聘会绑定场地后,在招聘会详情内独立生成和维护展位图 +-- 瀚高数据库 / PostgreSQL 兼容 + +CREATE SEQUENCE IF NOT EXISTS "cms_outdoor_fair_booth_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; + +CREATE TABLE IF NOT EXISTS "cms_outdoor_fair_booth" ( + "id" BIGINT NOT NULL DEFAULT nextval('"cms_outdoor_fair_booth_id_seq"') PRIMARY KEY, + "fair_id" BIGINT NOT NULL, + "source_venue_id" BIGINT, + "source_booth_id" BIGINT, + "booth_number" VARCHAR(50) NOT NULL, + "position_x" INTEGER NOT NULL DEFAULT 0, + "position_y" INTEGER NOT NULL DEFAULT 0, + "create_by" VARCHAR(64) DEFAULT '', + "create_time" TIMESTAMP(0), + "update_by" VARCHAR(64) DEFAULT '', + "update_time" TIMESTAMP(0), + "remark" VARCHAR(500), + "del_flag" CHAR(1) DEFAULT '0' +); + +COMMENT ON TABLE "cms_outdoor_fair_booth" IS '户外招聘会展位图副本'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."fair_id" IS '招聘会ID'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."source_venue_id" IS '来源场地ID'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."source_booth_id" IS '来源场地展位ID'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."booth_number" IS '展位号'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."position_x" IS 'X坐标'; +COMMENT ON COLUMN "cms_outdoor_fair_booth"."position_y" IS 'Y坐标'; + +CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_outdoor_fair_booth_number" ON "cms_outdoor_fair_booth" ("fair_id", "booth_number"); +CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_booth_fair" ON "cms_outdoor_fair_booth" ("fair_id"); + +-- 兼容历史预定:如果已有副本保留了 source_booth_id,把旧 booking.booth_id 改为招聘会副本展位ID +UPDATE "cms_outdoor_fair_booth_booking" booking +SET "booth_id" = fair_booth."id", + "booth_number" = fair_booth."booth_number" +FROM "cms_outdoor_fair_booth" fair_booth +WHERE booking."fair_id" = fair_booth."fair_id" + AND booking."booth_id" = fair_booth."source_booth_id" + AND booking."del_flag" = '0' + AND NOT EXISTS ( + SELECT 1 + FROM "cms_outdoor_fair_booth" snapshot_booth + WHERE snapshot_booth."fair_id" = booking."fair_id" + AND snapshot_booth."id" = booking."booth_id" + ); diff --git a/sql/cms_venue_booth_map.sql b/sql/cms_venue_booth_map.sql deleted file mode 100644 index 51f719b..0000000 --- a/sql/cms_venue_booth_map.sql +++ /dev/null @@ -1,86 +0,0 @@ --- 场地展位图与户外招聘会展位预定 --- 瀚高数据库 / PostgreSQL 兼容 - -CREATE SEQUENCE IF NOT EXISTS "cms_venue_booth_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; -CREATE TABLE IF NOT EXISTS "cms_venue_booth" ( - "id" BIGINT NOT NULL DEFAULT nextval('"cms_venue_booth_id_seq"') PRIMARY KEY, - "venue_id" BIGINT NOT NULL, - "booth_number" VARCHAR(50) NOT NULL, - "position_x" INTEGER NOT NULL DEFAULT 0, - "position_y" INTEGER NOT NULL DEFAULT 0, - "create_by" VARCHAR(64) DEFAULT '', - "create_time" TIMESTAMP(0), - "update_by" VARCHAR(64) DEFAULT '', - "update_time" TIMESTAMP(0), - "remark" VARCHAR(500), - "del_flag" CHAR(1) DEFAULT '0' -); - -COMMENT ON TABLE "cms_venue_booth" IS '场地展位图'; -COMMENT ON COLUMN "cms_venue_booth"."venue_id" IS '场地ID'; -COMMENT ON COLUMN "cms_venue_booth"."booth_number" IS '展位号'; -COMMENT ON COLUMN "cms_venue_booth"."position_x" IS 'X坐标'; -COMMENT ON COLUMN "cms_venue_booth"."position_y" IS 'Y坐标'; - -CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_venue_booth_number" ON "cms_venue_booth" ("venue_id", "booth_number"); -CREATE INDEX IF NOT EXISTS "idx_cms_venue_booth_venue" ON "cms_venue_booth" ("venue_id"); - -CREATE SEQUENCE IF NOT EXISTS "cms_outdoor_fair_booth_booking_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; -CREATE TABLE IF NOT EXISTS "cms_outdoor_fair_booth_booking" ( - "id" BIGINT NOT NULL DEFAULT nextval('"cms_outdoor_fair_booth_booking_id_seq"') PRIMARY KEY, - "fair_id" BIGINT NOT NULL, - "booth_id" BIGINT NOT NULL, - "booth_number" VARCHAR(50) NOT NULL, - "company_id" BIGINT, - "company_name" VARCHAR(200), - "status" VARCHAR(30) DEFAULT 'reserved', - "create_by" VARCHAR(64) DEFAULT '', - "create_time" TIMESTAMP(0), - "update_by" VARCHAR(64) DEFAULT '', - "update_time" TIMESTAMP(0), - "remark" VARCHAR(500), - "del_flag" CHAR(1) DEFAULT '0' -); - -COMMENT ON TABLE "cms_outdoor_fair_booth_booking" IS '户外招聘会展位预定'; -CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_outdoor_fair_booth_booking" ON "cms_outdoor_fair_booth_booking" ("fair_id", "booth_id"); -CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_booth_booking_fair" ON "cms_outdoor_fair_booth_booking" ("fair_id"); - -WITH target_venue AS ( - SELECT "id" AS venue_id - FROM "cms_venue_info" - WHERE "venue_name" = '石河子市人民广场户外招聘区' - LIMIT 1 -), -seed AS ( - SELECT - target_venue.venue_id, - 'KJ' || LPAD(gs::text, 2, '0') AS booth_number, - 40 + ((gs - 1) % 20) * 54 AS position_x, - 40 + ((gs - 1) / 20) * 54 AS position_y - FROM target_venue - CROSS JOIN generate_series(1, 80) gs -) -INSERT INTO "cms_venue_booth" ("venue_id", "booth_number", "position_x", "position_y", "create_by", "create_time", "del_flag") -SELECT seed.venue_id, seed.booth_number, seed.position_x, seed.position_y, 'system', NOW(), '0' -FROM seed -WHERE NOT EXISTS ( - SELECT 1 - FROM "cms_venue_booth" existing - WHERE existing."venue_id" = seed.venue_id - AND existing."booth_number" = seed.booth_number -); - -UPDATE "cms_venue_info" venue -SET "floor_count" = booth_counts.total -FROM ( - SELECT "venue_id", COUNT(*) AS total - FROM "cms_venue_booth" - GROUP BY "venue_id" -) booth_counts -WHERE venue."id" = booth_counts."venue_id"; - -UPDATE "cms_outdoor_fair" fair -SET "booth_count" = venue."floor_count" -FROM "cms_venue_info" venue -WHERE fair."venue_id" = venue."id"; diff --git a/sql/drop_cms_venue_booth.sql b/sql/drop_cms_venue_booth.sql new file mode 100644 index 0000000..38f7a29 --- /dev/null +++ b/sql/drop_cms_venue_booth.sql @@ -0,0 +1,5 @@ +-- 移除场地信息维护中的展位图母本表 +-- 执行前请确认招聘会展位图已经迁移到 cms_outdoor_fair_booth + +DROP TABLE IF EXISTS "cms_venue_booth"; +DROP SEQUENCE IF EXISTS "cms_venue_booth_id_seq";