CLAUDE.md 16 KB

CLAUDE.md — 水利安全生产监管信息系统

Project Overview

  • Name: 水利安全生产监管信息系统 (Water Safety Production Supervision System)
  • Group/Artifact: com.goldenwater:slaj:3.9.2
  • Database: Migrating from Oracle → PostgreSQL (HighGo/瀚高)

Tech Stack

Layer Technology Version
Java JDK 17
Framework Spring Boot 4.0.3
ORM MyBatis Spring Boot 4.0.1
Database PostgreSQL (HighGo)
UI Vue 3 + Element Plus 3.5.26 / 2.13.1
Build (FE) Vite
Connection Pool Druid 1.2.28

Module Structure

slaj/
├── gw-admin/       # Admin web module (controllers, composite services) — main entry point
├── gw-common/      # Shared entities, VOs, domain objects
├── gw-framework/   # Framework base classes (BaseController, etc.)
├── gw-quartz/      # Scheduled tasks
├── gw-slaj/        # Slaj business module (mappers, domain, controllers)
├── gw-system/      # System module (common mappers, services, sys_dept, etc.)
├── gw-ui/          # Vue 3 frontend
└── docs/           # DDL scripts, documents

Database Configuration

  • Schema: slaj
  • MyBatis: mapUnderscoreToCamelCase=true → DB columns org_name auto-map to Java orgName
  • PageHelper: helperDialect: postgresql (set in application.yml)
  • Dev DB: 39.98.38.2:5866/highgo
  • Test DB: 192.168.0.149:5866/highgo

Key Conventions

1. Result Mapping in MyBatis XML

Use resultType (auto-mapping) for JOIN query results. DO NOT use resultMap — PostgreSQL returns lowercase column names, but mapUnderscoreToCamelCase=true handles the conversion automatically. When using resultMap, column aliases must match the resultMap column names exactly, which may conflict with PostgreSQL's lower-case behavior.

2. PostgreSQL Compatibility

  • find_in_setANY(string_to_array(col, ','))
  • Upsert: Use INSERT ... ON CONFLICT (guid) DO UPDATE SET ...
  • String concat: '%' || #{param} || '%' (not MySQL CONCAT('%', #{param}, '%'))

3. Organization Types (3 types)

Type 中文名 Tables Description
Water Org 水利机构 sys_dept + att_org_base + att_org_ext Government water departments
Uwat Org 参建单位 sys_dept + att_org_base + att_org_uwat_ext Construction units
Wsub Org 分包单位 sys_dept + att_org_base + att_org_wsub Subcontractor units

Shared UUID pattern: sys_dept.dept_id = att_org_base.guid = extension_table.org_guid

4. Organization Insert Order (CRITICAL)

SysDeptServiceImpl.insertDept() internally regenerates UUID (line 230), so:

  1. Insert sys_dept FIRST
  2. Get the generated deptId from the returned dept object
  3. Use that deptId as the shared UUID for att_org_base.guid and extension table's org_guid

5. Composite Services (gw-admin)

All composite org operations (add/update/delete) are in gw-admin/src/main/java/com/goldenwater/web/service/:

  • OrgCompositeService.java — Water org (水利机构): sys_dept + att_org_base + att_org_ext
  • OrgUwatCompositeService.java — Uwat org (参建单位): sys_dept + att_org_base + att_org_uwat_ext
  • OrgWsubCompositeService.java — Wsub org (分包单位): sys_dept + att_org_base + att_org_wsub

All operations are @Transactional — all succeed or all roll back.

6. Redundant Table Sync (ATT* → OBJ*)

13 pairs of main/redundant tables. On every INSERT/UPDATE/DELETE to a main ATT* table, the corresponding ObjSyncMapper upserts the OBJ* redundant table via ON CONFLICT DO UPDATE.

Main Table Redundant Table
ATT_ENG_BASE OBJ_ENG
ATT_ENG_RES OBJ_RES
ATT_ENG_WAGA OBJ_WAGA
ATT_ENG_PUST OBJ_PUST
ATT_ENG_HYST OBJ_HYST
ATT_ENG_DIKE OBJ_DIKE
ATT_ENG_IRR OBJ_IRR
ATT_ENG_WADI OBJ_WADI
ATT_ENG_SD OBJ_SD
ATT_ENG_CWS OBJ_CWS
ATT_ENG_OTHE OBJ_OTHE
ATT_PROJ_BASE OBJ_PROJ
ATT_AD_BASE OBJ_AD

7. Business Relationship Chain

项目 (ATT_PROJ_BASE) → 工程 (ATT_ENG_BASE) → 标段 (OBJ_TEND) → 参建单位
  • 项目↔工程: Through REL_PROJ_ORG table (proj_guid + org_guid + from_date + to_date)
    • fromDate is auto-set by RelProjOrgServiceImpl.insertRelProjOrg()
  • 工程↔标段: OBJ_TEND.eng_guidATT_ENG_BASE.guid
  • 标段↔参建单位: REL_TEND_ORG table

8. Frontend Page Templates — Standard Styles

ALL new pages MUST follow one of the two patterns below. Reference implementations:

Pattern Example File
Standalone page gw-ui/src/views/slaj/woas/ObjWoas.vue
Tree-sidebar page gw-ui/src/views/system/orgext/index.vue

8a. Standalone Page Pattern (no sidebar)

Wrapper: <div class="system-page config-page"> (NOT app-container).

<template>
  <div ref="containerRef" class="system-page config-page">
    <el-card class="page-card">
      <template #header>
        <div class="card-header">
          <div class="header-left">
            <el-icon :size="20" color="#409EFF"><OfficeBuilding /></el-icon>
            <span class="page-title">页面标题</span>
          </div>
          <div class="search-right">
            <el-button type="primary" plain @click="handleAdd"><el-icon><Plus /></el-icon> 新增</el-button>
            <el-button type="danger" plain :disabled="multiple" @click="handleDelete"><el-icon><Delete /></el-icon> 删除</el-button>
            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" ... />
          </div>
        </div>
      </template>

      <div class="search-bar" v-show="showSearch">
        <el-form :model="queryParams" ref="queryRef" :inline="true">
          <!-- form items use el-icon component syntax for buttons -->
          <el-form-item>
            <el-button type="primary" @click="handleQuery"><el-icon><Search /></el-icon> 搜索</el-button>
            <el-button @click="resetQuery"><el-icon><Refresh /></el-icon> 重置</el-button>
          </el-form-item>
        </el-form>
      </div>

      <el-table :height="tableHeight" v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="50" align="center" />
        <!-- ... -->
        <el-table-column label="操作" align="center" width="160" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="handleUpdate(scope.row)">修改</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <div class="pagination">
        <el-pagination
          v-show="total > 0"
          v-model:current-page="queryParams.pageNum"
          v-model:page-size="queryParams.pageSize"
          :total="total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="getList"
          @current-change="getList"
        />
      </div>
    </el-card>

    <el-dialog :title="title" v-model="open" width="760px" append-to-body>
      <!-- form -->
      <template #footer>
        <el-button @click="cancel">取消</el-button>
        <el-button type="primary" @click="submitForm">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

8b. Tree-Sidebar Page Pattern (with TreePanel)

Wrapper: <div class="app-container tree-sidebar-manage-wrap"> with TreePanel on the left.

Key differences from standalone:

  • content-inner needs style="padding: 0;"
  • el-card needs style="height: 100%;" :body-style="{height: '100%'}"
  • el-table uses height="calc(100% - 100px)" instead of :height="tableHeight"

    <template>
    <div class="app-container tree-sidebar-manage-wrap">
    <tree-panel :key="deptTreeKey" title="组织机构" :tree-data="deptOptions" :tree-props="treeProps"
      search-placeholder="请输入机构名称" storage-key="xxx-sidebar-width"
      node-key="deptId" lazy :load="loadDeptNode"
      @node-click="handleNodeClick" @refresh="getDeptTree" ref="deptTreeRef" />
    <div class="tree-sidebar-content">
      <div class="content-inner" ref="containerRef" style="padding: 0;">
        <el-card class="page-card" style="height: 100%;" :body-style="{height: '100%'}">
          <template #header>
            <div class="card-header">
              <div class="header-left">
                <el-icon :size="20" color="#409EFF"><OfficeBuilding /></el-icon>
                <span class="page-title">页面标题</span>
              </div>
              <div class="search-right">
                <el-button type="primary" plain @click="handleAdd"><el-icon><Plus /></el-icon> 新增</el-button>
                <el-button type="danger" plain :disabled="multiple" @click="handleDelete"><el-icon><Delete /></el-icon> 删除</el-button>
                <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
              </div>
            </div>
          </template>
    
          <div class="search-bar" v-show="showSearch">
            <el-form :model="queryParams" ref="queryRef" :inline="true">
              <!-- ... same as standalone ... -->
            </el-form>
          </div>
    
          <el-table height="calc(100% - 100px)" v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
            <!-- ... -->
          </el-table>
    
          <div class="pagination">
            <el-pagination ... />
          </div>
        </el-card>
    
        <el-dialog :title="title" v-model="open" width="820px" append-to-body>
          <!-- form -->
          <template #footer>
            <el-button @click="cancel">取消</el-button>
            <el-button type="primary" @click="submitForm">确定</el-button>
          </template>
        </el-dialog>
      </div>
    </div>
    </div>
    </template>
    

8c. Shared Styles (global, no per-page duplication)

All shared page styles are defined in gw-ui/src/assets/styles/goldenwater.scss under the /* Shared Page Styles */ comment block. Pages do NOT need <style scoped> blocks for the following — they inherit globally:

  • .system-page — wrapper padding, background, min-height, input/textarea border-radius
  • .page-card — card border-radius, shadow, header/body padding overrides
  • .card-header — flex layout, .header-left (icon + title), .search-right (buttons)
  • .search-bar — light blue background (#f8faff), flex layout, form item spacing
  • .pagination — margin + right-alignment
  • .el-table — header background (#f5f7fa), link button colors

Pages only need a <style scoped> block if they have page-specific overrides (e.g., .projbase-form .el-divider, .tend-form .el-textarea__inner).

<style scoped lang="scss">
// For standalone pages: wrap everything in .system-page { ... }
// For tree-sidebar pages: styles are at root level (no .system-page wrapper needed)

.system-page {                         // ← standalone pages only
  padding: 0px;
  background-color: #f5f7fa;
  min-height: calc(100vh - 10px);

  :deep(.el-input__wrapper)   { border-radius: 4px; }
  :deep(.el-textarea__inner)  { border-radius: 4px; }
}                                      // ← close .system-page

.page-card {
  border-radius: 1px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);

  :deep(.el-card__header) {
    padding: 8px 20px !important;
    border-bottom: 1px solid #ebeef5;
    background-color: #fff;
    border-radius: 8px 8px 0 0;
  }

  :deep(.el-card__body) {
    padding: 0px !important;
    background-color: #fff;
  }
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;

  .header-left {
    display: flex;
    align-items: center;
    gap: 10px;
    .page-title { font-size: 18px; font-weight: 600; color: #303133; }
  }

  .search-right {
    display: flex;
    gap: 10px;
  }
}

.search-bar {
  margin-bottom: 0px;
  background-color: #f8faff;
  padding: 6px 10px;
  border-radius: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  :deep(.el-form) {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;

    .el-form-item {
      margin-bottom: 0;
      margin-right: 15px;
      .el-form-item__label { font-weight: 500; }
    }
  }
}

.pagination {
  margin-top: 8px;
  margin-bottom: 8px;
  display: flex;
  justify-content: flex-end;
}

:deep(.el-table) {
  border-radius: 4px;
  overflow: auto;

  th.el-table__cell {
    background-color: #f5f7fa !important;
    color: #606266;
    font-weight: 600;
  }

  .el-button--primary.is-link { color: #409eff; }
  .el-button--danger.is-link  { color: #f56c6c; }
}
</style>

8d. Key Rules Summary

Rule Do Don't
Page wrapper system-page config-page (standalone) or app-container tree-sidebar-manage-wrap (sidebar) app-container alone
Card el-card class="page-card" wraps table+search+pagination Table outside card
Header card-header > header-left (icon+title) + search-right (buttons) Buttons in a separate toolbar row
Search <div class="search-bar"> with #f8faff bg Plain el-form without wrapper
Icons <el-icon><Search /></el-icon> component syntax icon="Search" attribute syntax
Table header #f5f7fa (light gray) background, #606266 text #d4e7fd (blue) background
Pagination <el-pagination> inside <div class="pagination"> Custom <pagination> component
Dialog footer <el-button @click="cancel">取消</el-button> + <el-button type="primary" @click="submitForm">确定</el-button> directly Wrapped in extra div.dialog-footer
Input size Default Element Plus height Compact 26px override
Select width Explicit style="width: 120px" (or appropriate) on each <el-select> Default or style="width:100%"
Page size :page-sizes="[10, 20, 50, 100]", default pageSize: 10 Other page size arrays

8e. Other Frontend Conventions

  • API files: gw-ui/src/api/ — separate files per module (e.g., slaj/eng.js, system/attOrgWsub.js)
  • OrgSelect component: @/components/OrgSelect/ — supports scope (all/water/uwat) and showScope props
  • Backend does data assembly: JOIN queries return merged VOs; frontend only displays, no multiple API calls
  • Icon imports: Always import used icons explicitly from @element-plus/icons-vue

9. Frontend Router Convention

  • Routes defined in gw-ui/src/router/index.js
  • URL pattern: /system/{entity}/index for list pages
  • Parameterized routes: /system/projbase-eng/index/:projGuid

10. Key Backend Packages

Package Module Contents
com.goldenwater.slaj.hidd gw-slaj Core business mappers/domain/controllers
com.goldenwater.system gw-system Sys dept, shared mappers, common services
com.goldenwater.web gw-admin Composite services, system controllers
com.goldenwater.common.core.domain gw-common Shared entities and VOs
com.goldenwater.common.core.domain.obj gw-common Redundant OBJ_* entities
com.goldenwater.common.core.domain.entity gw-common Entity classes (RelProjOrg, SysDept, etc.)

11. Import Path Aliases (Frontend)

  • @/gw-ui/src/

Common Pitfalls

  1. VO in wrong module: VOs used by gw-system mappers must be in gw-common, not gw-admin
  2. Premature UUID: Don't set UUID before insertDept() — it will be overwritten
  3. OBJ_TEND has no proj_guid: Filter by project via JOIN att_eng_base on eng_guid
  4. Status default: New orgs should have status "0" (enabled), not "1" (disabled)
  5. Parent dept null check: Always validate parent dept exists before insert, or throw clear error