Merge branch 'main' of ssh://124.243.245.42:2222/zkr/shz-backend

This commit is contained in:
2026-06-25 14:42:17 +08:00
12 changed files with 551 additions and 13 deletions

10
pom.xml
View File

@@ -33,6 +33,7 @@
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<qiniu.version>7.19.0</qiniu.version>
<aliyun.oss.version>2.5.0</aliyun.oss.version>
<lombok.version>1.18.34</lombok.version>
<qcloud.cos.version>5.5.9</qcloud.cos.version>
</properties>
@@ -129,6 +130,13 @@
<version>${velocity.version}</version>
</dependency>
<!-- Lombok (强制升级以兼容 Java 21) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
@@ -228,7 +236,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>

View File

@@ -0,0 +1,140 @@
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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
/**
* 面试菜单权限自动迁移
* 应用启动后自动检查并添加面试列表菜单及按钮权限到 sys_menu 和 sys_role_menu 表
*/
@Slf4j
@Component
public class InterviewMenuMigration {
@Autowired
private DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void migrate() {
log.info("开始检查面试菜单权限...");
try (Connection conn = dataSource.getConnection()) {
// Step 1: 查找岗位管理目录的 menu_id
Long parentId = null;
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT menu_id FROM sys_menu WHERE path = 'management' AND menu_type = 'M' LIMIT 1")) {
if (rs.next()) {
parentId = rs.getLong("menu_id");
}
}
if (parentId == null) {
log.warn("未找到 management 目录菜单,跳过面试菜单迁移");
return;
}
log.info("找到 management 目录菜单parent_id={}", parentId);
// Step 2: 插入面试列表菜单(幂等)
Long menuId = insertMenuIfNotExists(conn, 2100L, "面试列表", parentId, 10,
"interview-list", "Management/InterviewList", "InterviewList",
"C", "cms:interview:list", "message");
if (menuId == null) {
log.warn("面试列表菜单插入失败(可能已存在但 parent_id 不同)");
return;
}
// Step 3: 插入按钮权限
insertMenuIfNotExists(conn, 2101L, "面试查询", menuId, 1,
"", "", "", "F", "cms:interview:query", "#");
insertMenuIfNotExists(conn, 2102L, "面试新增", menuId, 2,
"", "", "", "F", "cms:interview:add", "#");
insertMenuIfNotExists(conn, 2103L, "面试修改", menuId, 3,
"", "", "", "F", "cms:interview:edit", "#");
insertMenuIfNotExists(conn, 2104L, "面试删除", menuId, 4,
"", "", "", "F", "cms:interview:remove", "#");
// Step 4: 将新菜单赋给所有非管理员角色
try (Statement stmt = conn.createStatement()) {
stmt.execute(
"INSERT INTO sys_role_menu (role_id, menu_id) " +
"SELECT r.role_id, " + menuId + " FROM sys_role r " +
"WHERE r.role_key != 'admin' " +
"AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + menuId + ")");
} catch (Exception e) {
log.debug("角色菜单关联已存在或插入失败: {}", e.getMessage());
}
// 按钮也赋给角色
for (long btnId : new long[]{2101L, 2102L, 2103L, 2104L}) {
try (Statement stmt = conn.createStatement()) {
stmt.execute(
"INSERT INTO sys_role_menu (role_id, menu_id) " +
"SELECT r.role_id, " + btnId + " FROM sys_role r " +
"WHERE r.role_key != 'admin' " +
"AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + btnId + ")");
} catch (Exception e) {
log.debug("按钮角色关联已存在: {}", e.getMessage());
}
}
log.info("面试菜单权限迁移完成");
} catch (Exception e) {
log.error("面试菜单迁移失败: {}", e.getMessage(), e);
}
}
/**
* 幂等插入菜单记录
*/
private Long insertMenuIfNotExists(Connection conn, Long menuId, String menuName,
Long parentId, int orderNum, String path,
String component, String routeName,
String menuType, String perms, String icon) {
try {
// 检查是否已存在
try (PreparedStatement ps = conn.prepareStatement(
"SELECT 1 FROM sys_menu WHERE menu_id = ?")) {
ps.setLong(1, menuId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
log.debug("菜单已存在: {} (id={})", menuName, menuId);
return menuId;
}
}
}
// 插入新菜单
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) " +
"VALUES (?, ?, ?, ?, ?, ?, '', ?, 1, 0, ?, '0', '0', ?, ?, 'admin', CURRENT_TIMESTAMP)")) {
ps.setLong(1, menuId);
ps.setString(2, menuName);
ps.setLong(3, parentId);
ps.setInt(4, orderNum);
ps.setString(5, path);
ps.setString(6, component);
ps.setString(7, routeName);
ps.setString(8, menuType);
ps.setString(9, perms);
ps.setString(10, icon);
ps.executeUpdate();
log.info("菜单创建成功: {} (id={}, perms={})", menuName, menuId, perms);
return menuId;
}
} catch (Exception e) {
log.warn("菜单创建失败: {} (id={}), error={}", menuName, menuId, e.getMessage());
return null;
}
}
}

View File

@@ -12,6 +12,8 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.util.List;
@@ -67,7 +69,32 @@ public class AppInterviewController extends BaseController {
*/
@ApiOperation("更新面试邀约状态")
@PutMapping("/status/{id}")
public AjaxResult updateStatus(@PathVariable Long id, @RequestParam String status){
public AjaxResult updateStatus(@PathVariable Long id, HttpServletRequest request) {
String status = request.getParameter("status");
// 如果 URL 参数没有,尝试从 JSON body 中读取
if (status == null || status.isEmpty()) {
try {
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String body = sb.toString();
if (body != null && !body.isEmpty()) {
// 简单解析 JSON: {"status": "accepted"}
status = body.replaceAll(".*\"status\"\\s*:\\s*\"([^\"]+)\".*", "$1");
if (status.equals(body)) {
status = null;
}
}
} catch (Exception e) {
// ignore body read errors
}
}
if (status == null || status.isEmpty()) {
return AjaxResult.error("状态参数不能为空");
}
int rows = interviewInvitationService.updateInterviewStatus(id, status);
if (rows > 0 && "accepted".equals(status)) {
// 接受时返回面试官联系方式,供前端弹窗展示

View File

@@ -1,6 +1,7 @@
package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import com.ruoyi.cms.service.InterviewInvitationService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -38,7 +39,7 @@ public class CmsInterviewController extends BaseController {
@GetMapping("/list")
public TableDataInfo list(InterviewInvitation interviewInvitation){
startPage();
List<InterviewInvitation> list = interviewInvitationService.getInterviewInvitationList(interviewInvitation);
List<InterviewInvitationVO> list = interviewInvitationService.getInterviewInvitationVOList(interviewInvitation);
return getDataTable(list);
}

View File

@@ -77,6 +77,11 @@ public class InterviewInvitation extends BaseEntity {
*/
@ApiModelProperty("面试官联系方式")
private String contactPhone;
/**
* 面试官姓名
*/
@ApiModelProperty("面试官姓名")
private String interviewerName;
/**
* 公司名称冗余存储创建时从company表获取
*/

View File

@@ -18,4 +18,6 @@ public class CandidateVO extends AppUser {
private String companyName;
@Excel(name = "岗位名称", sort = 1)
private String jobName;
private String interviewStatus;
private Long interviewId;
}

View File

@@ -11,4 +11,10 @@ public class InterviewInvitationVO extends InterviewInvitation {
@ApiModelProperty("岗位名称(来自关联查询的实时数据)")
private String jobTitle;
@ApiModelProperty("求职者姓名(来自关联查询的实时数据)")
private String applicantName;
@ApiModelProperty("求职者联系方式(来自关联查询的实时数据)")
private String applicantPhone;
}

View File

@@ -17,12 +17,14 @@ import com.ruoyi.common.core.domain.entity.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import java.util.List;
@@ -43,8 +45,9 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
@Autowired
private DataSource dataSource;
// @PostConstruct
@EventListener(ApplicationReadyEvent.class)
public void initTable() {
log.info("开始检查并初始化 interview_invitation 表...");
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// Check if id column has auto-increment (BIGSERIAL or sequence default)
@@ -80,6 +83,7 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
+ "meeting_password varchar(100) NULL,"
+ "interview_location varchar(500) NULL,"
+ "contact_phone varchar(50) NULL,"
+ "interviewer_name varchar(100) NULL,"
+ "company_name varchar(200) NULL,"
+ "job_name varchar(200) NULL,"
+ "status varchar(20) DEFAULT 'pending' NULL,"
@@ -96,9 +100,16 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
log.info("interview_invitation 表创建成功(序列自增)!");
} else {
log.info("interview_invitation 表结构正常,检查新增列...");
// 为已有数据库添加新列(幂等操作)
try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS company_name varchar(200) NULL"); } catch (Exception ignored) {}
try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS job_name varchar(200) NULL"); } catch (Exception ignored) {}
// 为已有数据库添加新列(幂等操作,使用 DO 块兼容不支持 IF NOT EXISTS 的数据库版本
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN company_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 company_name 列失败: {}", e.getMessage()); }
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN job_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 job_name 列失败: {}", e.getMessage()); }
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN interviewer_name varchar(100) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 interviewer_name 列失败: {}", e.getMessage()); }
}
} catch (Exception e) {
log.error("初始化表失败: {}", e.getMessage());

View File

@@ -16,6 +16,7 @@
<result property="meetingPassword" column="meeting_password"/>
<result property="interviewLocation" column="interview_location"/>
<result property="contactPhone" column="contact_phone"/>
<result property="interviewerName" column="interviewer_name"/>
<result property="companyName" column="company_name"/>
<result property="jobName" column="job_name"/>
<result property="status" column="status"/>
@@ -30,12 +31,14 @@
<!-- VO 带公司名和岗位名继承基础映射jobTitle 来自关联查询的实时数据) -->
<resultMap type="com.ruoyi.cms.domain.vo.InterviewInvitationVO" id="InterviewInvitationVOResult" extends="InterviewInvitationResult">
<result property="jobTitle" column="job_title"/>
<result property="applicantName" column="applicant_name"/>
<result property="applicantPhone" column="applicant_phone"/>
</resultMap>
<sql id="selectInterviewInvitationVo">
select id, company_id, job_id, user_id, apply_id, interview_time,
interview_method, meeting_link, meeting_password, interview_location,
contact_phone, company_name, job_name, status, del_flag,
contact_phone, interviewer_name, company_name, job_name, status, del_flag,
create_by, create_time, update_by, update_time, remark
from interview_invitation
</sql>
@@ -53,13 +56,22 @@
</select>
<!-- 带公司名和岗位名的列表查询COALESCE 优先用关联表实时数据,兜底用冗余存储 -->
<!-- 注意:不用 t.* 避免 company_name 列名冲突,显式列出除 company_name 外的所有列 -->
<select id="getInterviewInvitationVOList" resultMap="InterviewInvitationVOResult" parameterType="InterviewInvitation">
select t.*,
select t.id, t.company_id, t.job_id, t.user_id, t.apply_id,
t.interview_time, t.interview_method, t.meeting_link,
t.meeting_password, t.interview_location,
t.contact_phone, t.interviewer_name,
t.job_name, t.status, t.del_flag,
t.create_by, t.create_time, t.update_by, t.update_time, t.remark,
COALESCE(c.name, t.company_name) as company_name,
COALESCE(j.job_title, t.job_name) as job_title
COALESCE(j.job_title, t.job_name) as job_title,
u.name as applicant_name,
u.phone as applicant_phone
from interview_invitation t
left join company c on c.company_id = t.company_id and c.del_flag = '0'
left join job j on j.job_id = t.job_id and j.del_flag = '0'
left join app_user u on u.user_id = t.user_id and u.del_flag = '0'
<where> t.del_flag = '0'
<if test="companyId != null and companyId != ''"> and t.company_id = #{companyId}</if>
<if test="jobId != null and jobId != ''"> and t.job_id = #{jobId}</if>

View File

@@ -42,10 +42,26 @@
ORDER BY ja.create_time DESC
</select>
<select id="candidates" resultType="com.ruoyi.cms.domain.vo.CandidateVO">
SELECT au.*,jc.create_time as apply_date,jc.matching_degree,jc.id as applyId
SELECT au.*, jc.create_time as apply_date, jc.matching_degree, jc.id as applyId,
latest_ii.status as interviewStatus, latest_ii.id as interviewId
from job_apply as jc
inner join app_user as au on jc.user_id = au.user_id
left join (
select ii1.*
from interview_invitation ii1
inner join (
select user_id, job_id, max(create_time) as max_ct
from interview_invitation
where del_flag = '0'
group by user_id, job_id
) ii2 on ii1.user_id = ii2.user_id
and ii1.job_id = ii2.job_id
and ii1.create_time = ii2.max_ct
) latest_ii on latest_ii.user_id = jc.user_id
and latest_ii.job_id = jc.job_id
and latest_ii.del_flag = '0'
where jc.job_id = #{jobId} and jc.del_flag = '0' and au.del_flag ='0'
order by jc.create_time desc
</select>
<select id="trendChart" resultType="java.util.HashMap" parameterType="JobApply">
@@ -104,10 +120,26 @@
</select>
<select id="selectApplyJobUserList" parameterType="com.ruoyi.common.core.domain.entity.AppUser" resultType="com.ruoyi.cms.domain.vo.CandidateVO">
select a.id applyId, a.hire, b.job_title jobName,l.name companyName,e.* from job_apply a
select a.id applyId, a.hire, b.job_title jobName, l.name companyName, e.*,
latest_ii.status as interviewStatus, latest_ii.id as interviewId
from job_apply a
INNER join job b on a.job_id=b.job_id and b.del_flag='0'
INNER join app_user e on a.user_id =e.user_id and e.del_flag='0'
INNER join company l on b.company_id =l.company_id and l.del_flag='0'
left join (
select ii1.*
from interview_invitation ii1
inner join (
select user_id, job_id, max(create_time) as max_ct
from interview_invitation
where del_flag = '0'
group by user_id, job_id
) ii2 on ii1.user_id = ii2.user_id
and ii1.job_id = ii2.job_id
and ii1.create_time = ii2.max_ct
) latest_ii on latest_ii.user_id = a.user_id
and latest_ii.job_id = a.job_id
and latest_ii.del_flag = '0'
where a.del_flag='0'
<if test="company != null and company.jobTitle != null and company.jobTitle != ''"> and b.job_title like concat('%', cast(#{company.jobTitle, jdbcType=VARCHAR} as varchar), '%')</if>
<if test="company != null and company.education != null and company.education != ''"> and b.education = #{company.education}</if>

View File

@@ -0,0 +1,90 @@
-- =====================================================
-- 面试列表 - 菜单权限配置 SQL
-- 在 岗位管理 菜单下增加"面试列表"子菜单及按钮权限
--
-- 执行前请确认:
-- 1. 确认 parent_id面试列表将放在 "岗位管理" 目录下
-- 如果数据库中岗位管理目录的 menu_id 不同,请修改下面子查询中的条件
-- 2. sys_menu 表使用自增主键auto_increment=2000
-- 本脚本使用 2100+ 的固定ID避免与已有数据冲突
-- 3. 执行后需要在"角色管理"中将新菜单分配给对应角色
-- =====================================================
-- Step 1: 新增菜单项C=菜单页面)
-- parent_id 动态查找path='management' 的目录菜单
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2100, '面试列表',
(SELECT menu_id FROM sys_menu WHERE path = 'management' AND menu_type = 'M' LIMIT 1),
10, 'interview-list', 'Management/InterviewList', '', 'InterviewList', 1, 0, 'C', '0', '0',
'cms:interview:list', 'message', 'admin', CURRENT_TIMESTAMP, '', null, '面试列表菜单'
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE path = 'management' AND menu_type = 'M');
-- Step 2: 新增按钮权限F=按钮)
-- 面试查询
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2101, '面试查询', 2100, 1, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:query', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试新增
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2102, '面试新增', 2100, 2, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:add', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试修改
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2103, '面试修改', 2100, 3, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:edit', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试删除
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2104, '面试删除', 2100, 4, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:remove', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- Step 3: 将新菜单分配给角色
-- 为所有非管理员角色分配面试列表权限(管理员角色无需分配,通配符 *:*:* 自动拥有全部权限)
-- 如需仅限特定角色,请修改 WHERE 条件
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2100
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2101
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2101);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2102
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2102);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2103
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2103);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2104
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2104);
-- =====================================================
-- 验证 SQL可选执行
-- =====================================================
-- 检查菜单是否创建成功
-- SELECT * FROM sys_menu WHERE menu_id BETWEEN 2100 AND 2104;
-- 检查角色菜单关联
-- SELECT r.role_name, m.menu_name FROM sys_role_menu rm
-- JOIN sys_role r ON r.role_id = rm.role_id
-- JOIN sys_menu m ON m.menu_id = rm.menu_id
-- WHERE rm.menu_id BETWEEN 2100 AND 2104;

204
启动指南.md Normal file
View File

@@ -0,0 +1,204 @@
# 石河子后端服务 - 手动启动指南
## 项目概述
- **框架**: RuoYi (若依) v3.8.8 / Spring Boot 2.5.15
- **语言**: Java 1.8 (兼容 Java 21)
- **构建工具**: Maven 3.9+
- **默认端口**: `9091`
- **主启动类**: `com.ruoyi.RuoYiApplication`
---
## 1. 环境要求
| 依赖 | 当前环境 | 备注 |
|------|----------|------|
| **JDK** | 21.0.11 (Temurin) | 路径: `C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot` |
| **Maven** | 3.9.9 | 路径: `C:\Users\franc\apache-maven-3.9.9` |
| **数据库** | Highgo DB (远程) | `39.98.44.136:6023` (local 配置) |
| **Redis** | 远程 | `124.243.245.42:5379` (local 配置) |
| **Elasticsearch** | 远程 | `124.243.245.42:9200` (local 配置) |
---
## 2. 环境变量设置
### Git Bash / WSL
```bash
export PATH="/c/Users/franc/apache-maven-3.9.9/bin:$PATH"
```
### 永久生效 (可选)
将上述 export 命令添加到 `~/.bashrc` 末尾:
```bash
echo 'export PATH="/c/Users/franc/apache-maven-3.9.9/bin:$PATH"' >> ~/.bashrc
```
### 验证安装
```bash
java -version # 应显示 OpenJDK 21
mvn -version # 应显示 Apache Maven 3.9.9
```
---
## 3. 构建项目
```bash
# 进入后端项目目录
cd shz-backend
# 清理并构建跳过测试4线程并行编译
mvn clean install -DskipTests -T 4
```
> **构建产物**: `ruoyi-admin/target/ruoyi-admin.jar`
---
## 4. 启动服务
### 前台运行用于调试Ctrl+C 停止)
```bash
cd shz-backend
java -jar ruoyi-admin/target/ruoyi-admin.jar --spring.profiles.active=local
```
### 后台运行
```bash
cd shz-backend
# 创建日志目录
mkdir -p logs
# 后台启动
nohup java -jar ruoyi-admin/target/ruoyi-admin.jar \
--spring.profiles.active=local \
>> logs/backend.log 2>&1 &
# 跟踪日志
tail -f logs/backend.log
```
### JVM 参数调优(可选)
```bash
java -Xms2048M -Xmx2048M -jar ruoyi-admin/target/ruoyi-admin.jar \
--spring.profiles.active=local
```
---
## 5. 验证服务
```bash
# 检查服务首页
curl http://localhost:9091/
# 返回: 欢迎使用RuoYi后台管理框架当前版本v3.8.8
# 检查 Swagger API 文档
curl -s -o /dev/null -w "HTTP: %{http_code}" http://localhost:9091/swagger-ui/index.html
# 返回: HTTP: 200
# 检查 Druid 监控面板
# 浏览器访问: http://localhost:9091/druid/
# 账号: ruoyi / 123456
# 查看运行中的进程
jps -l | grep ruoyi
```
---
## 6. 停止服务
```bash
# 方法一: 查找并终止(推荐)
jps -l | grep ruoyi # 查找 PID
kill <PID> # 优雅关闭
# 方法二: 按端口查找
netstat -ano | findstr 9091 # Windows
lsof -i :9091 # Linux/Mac
```
---
## 7. 可用配置 Profile
| Profile | 配置文件 | 数据库 | 端口 |
|---------|----------|--------|------|
| `dev` | [`application-dev.yml`](ruoyi-admin/src/main/resources/application-dev.yml) | 内网 `172.31.9.107:8765` | `9091` |
| `local` | [`application-local.yml`](ruoyi-admin/src/main/resources/application-local.yml) | 外网 `39.98.44.136:6023` | `9091` |
默认激活 Profile 在 [`application.yml`](ruoyi-admin/src/main/resources/application.yml) 的 `spring.profiles.active` 中配置。
---
## 8. 构建常见问题
### 编译错误: `NoSuchFieldError: JCTree$JCImport qualid`
Lombok 版本与 JDK 21 不兼容。确保 `pom.xml` 中 Lombok 版本 ≥ 1.18.32
```xml
<lombok.version>1.18.34</lombok.version>
```
### 编译错误: `maven-compiler-plugin` 版本过低
需要 `maven-compiler-plugin` ≥ 3.11.0 才支持 JDK 21
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
```
### Maven 下载依赖缓慢
项目已配置阿里云 Maven 镜像(`maven.aliyun.com`),无需额外配置。
---
## 9. 项目结构
```
shz-backend/
├── pom.xml # 根 POM依赖管理
├── ruoyi-admin/ # Web 入口模块Spring Boot 启动类)
│ └── src/main/resources/
│ ├── application.yml # 主配置
│ ├── application-dev.yml # 开发环境
│ └── application-local.yml # 本地环境
├── ruoyi-common/ # 通用工具模块
├── ruoyi-framework/ # 核心框架模块
├── ruoyi-system/ # 系统管理模块
├── ruoyi-bussiness/ # 业务模块
├── ruoyi-quartz/ # 定时任务模块
├── ruoyi-generator/ # 代码生成模块
├── sql/ # 数据库脚本
├── docs/ # 项目文档
└── buildAndStart.sh # Linux 部署脚本
```
---
## 10. 相关服务地址
| 服务 | 地址 |
|------|------|
| 后端 API | `http://localhost:9091/` |
| Swagger 文档 | `http://localhost:9091/swagger-ui/index.html` |
| Druid 监控 | `http://localhost:9091/druid/` |
| 管理后台前端 | `shz-admin/` 目录(需单独启动) |
| 微信小程序 | `shzApp/` 目录uniapp 项目) |