commit 2efddcfe2e1c48e0cee915b7efc13743b16284a6 Author: Hvemi_han Date: Wed Dec 18 13:30:51 2024 +0800 Initial commit: 课程任务进度管理系统 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6d1eb29 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/API文档-新.md b/API文档-新.md new file mode 100644 index 0000000..26c4422 --- /dev/null +++ b/API文档-新.md @@ -0,0 +1,411 @@ +# 进度管理系统 API 文档 + +## 基础信息 + +- 基础 URL: `http://localhost:1218` +- 所有请求和响应均使用 JSON 格式 +- 所有需要认证的接口都需要在请求头中携带 `Authorization: Bearer {token}` + +## 通用响应格式 + +```json +{ + "code": 10000, // 响应码 + "message": "成功", // 响应消息 + "data": {} // 响应数据 +} +``` + +### 响应码说明 + +| 响应码 | 说明 | 消息 | +| ------ | ---------- | ---------- | +| 10000 | 成功 | 成功 | +| 10001 | 参数错误 | 参数无效 | +| 10002 | 邮箱重复 | 邮箱已存在 | +| 10003 | 用户不存在 | 用户不存在 | +| 10004 | 密码错误 | 密码错误 | +| 10005 | 邮箱已存在 | 邮箱已存在 | +| 10006 | 未授权 | 未授权 | +| 10007 | 令牌过期 | 令牌已过期 | +| 10008 | 令牌无效 | 无效的令牌 | +| 10009 | 系统错误 | 系统错误 | + +## 字段说明 + +### 用户相关字段 + +1. **角色 (roles)** + + - 1: 教师 + - 2: 普通管理员 + - 3: 沟通联络人 + - 4: 系统管理员 + +2. **岗位 (jobs)** + + - 1: 课程制作教师 + - 2: 课程购买方项目负责人 + - 3: 课程制作方沟通联络人 + - 4: 系统制作方项目负责人 + +3. **用户状态 (status)** + - 1: 正常 + - 0: 禁用 + +### 课程任务相关字段 + +1. **进度状态 (progressStatus)** + - 0: 脚本上传 + - 1: 脚本确认 + - 2: 视频拍摄 + - 3: 后期制作 + - 4: 任务完成 + +## 用户接口 + +### 1. 用户注册 + +- **接口**:`POST /api/users` +- **描述**:创建新用户 +- **认证**:不需要 +- **请求体**: + ```json + { + "username": "testuser", // 用户名,不可为空 + "email": "test@example.com", // 邮箱,不可为空且唯一 + "password": "password123", // 密码,不可为空 + "departmentId": 1, // 部门ID,不可为空,关联departments表 + "roles": 1, // 角色:1-教师,2-普通管理员,3-沟通联络人,4-系统管理员 + "jobs": 1, // 岗位:1-课程制作教师,2-课程购买方项目负责人,3-课程制作方沟通联络人,4-系统制作方项目负责人 + "avatar": "http://example.com/avatar.jpg", // 头像URL,可选 + "creatorId": 1 // 创建者ID,不可为空 + } + ``` +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": null + } + ``` +- **错误响应**: + ```json + { + "code": 10005, + "message": "邮箱已存在", + "data": null + } + ``` + +### 2. 用户登录 + +- **接口**:`POST /api/users/login` +- **描述**:用户登录获取 token +- **认证**:不需要 +- **请求体**: + ```json + { + "email": "test@example.com", // 邮箱 + "password": "password123", // 密码 + "remember": true // 是否记住登录(可选) + } + ``` +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "token": "eyJhbGciOiJIUzM4NCJ9..." // JWT令牌 + } + } + ``` + +### 3. 用户登出 + +- **接口**:`POST /api/users/logout` +- **描述**:用户登出,使当前 token 失效 +- **认证**:需要 +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": "登出成功" + } + ``` + +### 4. 获取当前用户信息 + +- **接口**:`GET /api/users/current` +- **描述**:获取当前登录用户信息 +- **认证**:需要 +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "id": 12, + "username": "testuser", + "email": "test@example.com", + "departmentId": 1, + "roles": 1, + "jobs": 1, + "avatar": null, + "creatorId": 1, + "status": 1, + "createdAt": 1734498503690, + "updatedAt": 1734498503690, + "enabled": true, + "authorities": [ + { + "authority": "ROLE_USER" + } + ] + } + } + ``` + +### 5. 获取用户列表 + +- **接口**:`GET /api/users/list` +- **描述**:分页获取用户列表 +- **认证**:需要 +- **查询参数**: + - `page`: 页码(从 1 开始) + - `limit`: 每页数量(默认 10) +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "list": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "departmentId": 1, + "roles": 4, + "jobs": 4, + "avatar": null, + "creatorId": 1, + "status": 1, + "createdAt": 1734491549, + "updatedAt": 1734491549 + } + ], + "total": 12, + "currentPage": 1, + "pageSize": 10 + } + } + ``` + +### 6. 禁用用户 + +- **接口**:`POST /api/users/disable/{userId}` +- **描述**:禁用指定用户 +- **认证**:需要 +- **路径参数**: + - `userId`: 用户 ID +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": "用户已禁用" + } + ``` + +## 课程任务接口 + +### 1. 获取课程任务列表 + +- **接口**:`GET /api/lesson-tasks` +- **描述**:分页获取课程任务列表 +- **认证**:需要 +- **查询参数**: + - `page`: 页码(从 1 开始) + - `size`: 每页数量(默认 10) + - `userId`: 用户 ID(可选) +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "content": [ + { + "id": 6, + "courseName": "Test Course", + "microLessonName": "Test Lesson", + "userId": 12, + "progressStatus": 1, + "scriptUploadTime": 1734498510000, + "scriptConfirmTime": 1734498510000, + "videoCaptureTime": 1734498510000, + "videoConfirmTime": 1734498510000, + "finishTime": 1734498510000, + "advise": "Test advice", + "createdAt": 1734498546322, + "updatedAt": 1734498546322 + } + ], + "totalElements": 4, + "totalPages": 1, + "size": 10, + "number": 0, + "first": true, + "last": true, + "empty": false + } + } + ``` + +### 2. 获取单个课程任务 + +- **接口**:`GET /api/lesson-tasks/{id}` +- **描述**:获取指定 ID 的课程任务 +- **认证**:需要 +- **路径参数**: + - `id`: 课程任务 ID +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "id": 6, + "courseName": "Test Course", + "microLessonName": "Test Lesson", + "userId": 12, + "progressStatus": 1, + "scriptUploadTime": 1734498510000, + "scriptConfirmTime": 1734498510000, + "videoCaptureTime": 1734498510000, + "videoConfirmTime": 1734498510000, + "finishTime": 1734498510000, + "advise": "Test advice", + "createdAt": 1734498546322, + "updatedAt": 1734498546322 + } + } + ``` + +### 3. 创建课程任务 + +- **接口**:`POST /api/lesson-tasks` +- **描述**:创建新的课程任务 +- **认证**:需要 +- **请求体**: + ```json + { + "courseName": "Java基础", // 课程名称,不可为空 + "microLessonName": "Java变量", // 微课名称,不可为空 + "userId": 1, // 负责人ID,不可为空,关联users表 + "advise": "请注意讲解速度" // 任务建议或备注,可选 + } + ``` +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "id": 6, + "courseName": "Java基础", + "microLessonName": "Java变量", + "userId": 1, + "progressStatus": 1, + "scriptUploadTime": , + "scriptConfirmTime": , + "videoCaptureTime": , + "videoConfirmTime": , + "finishTime": , + "advise": "请注意讲解速度", + "createdAt": 1734498546322, + "updatedAt": 1734498546322 + } + } + ``` + +### 4. 更新课程任务 + +- **接口**:`PUT /api/lesson-tasks/{id}` +- **描述**:更新指定 ID 的课程任务 +- **认证**:需要 +- **路径参数**: + - `id`: 课程任务 ID +- **请求体**: + ```json + { + "courseName": "Updated Course", // 课程名称 + "microLessonName": "Updated Lesson", // 微课名称 + "userId": 12, // 用户ID + "progressStatus": 2, // 进度状态 + "scriptUploadTime": 1734498510000, // 脚本上传时间 + "scriptConfirmTime": 1734498510000, // 脚本确认时间 + "videoCaptureTime": 1734498510000, // 视频录制时间 + "videoConfirmTime": 1734498510000, // 视频确认时间 + "finishTime": 1734498510000, // 完成时间 + "advise": "Updated advice" // 建议 + } + ``` +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": { + "id": 6, + "courseName": "Updated Course", + "microLessonName": "Updated Lesson", + "userId": 12, + "progressStatus": 2, + "scriptUploadTime": 1734498510000, + "scriptConfirmTime": 1734498510000, + "videoCaptureTime": 1734498510000, + "videoConfirmTime": 1734498510000, + "finishTime": 1734498510000, + "advise": "Updated advice", + "createdAt": 1734498546322, + "updatedAt": 1734498586574 + } + } + ``` + +### 5. 删除课程任务 + +- **接口**:`DELETE /api/lesson-tasks/{id}` +- **描述**:删除指定 ID 的课程任务 +- **认证**:需要 +- **路径参数**: + - `id`: 课程任务 ID +- **成功响应**: + ```json + { + "code": 10000, + "message": "成功", + "data": null + } + ``` + +## 注意事项 + +1. 所有需要认证的接口必须在请求头中携带有效的 JWT 令牌 +2. 所有时间戳字段均为毫秒级时间戳 +3. 分页接口的页码从 1 开始 +4. 用户密码在传输和存储时都会进行加密处理 +5. 课程任务的 progressStatus 字段状态码说明: + - 0: 脚本上传 + - 1: 脚本确认 + - 2: 视频拍摄 + - 3: 后期制作 + - 4: 任务完成 +6. 用户状态说明: + - 1: 正常 + - 0: 禁用 diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..31b68d2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.1 + + + com.huertian + jinduguanli + 0.0.1-SNAPSHOT + jinduguanli + jinduguanli + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + + + + com.mysql + mysql-connector-j + 8.2.0 + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + 1.18.30 + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..2602222 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000..db2207d Binary files /dev/null and b/src/main/.DS_Store differ diff --git a/src/main/java/com/huertian/jinduguanli/JinduguanliApplication.java b/src/main/java/com/huertian/jinduguanli/JinduguanliApplication.java new file mode 100644 index 0000000..5d7e1f0 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/JinduguanliApplication.java @@ -0,0 +1,13 @@ +package com.huertian.jinduguanli; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JinduguanliApplication { + + public static void main(String[] args) { + SpringApplication.run(JinduguanliApplication.class, args); + } + +} diff --git a/src/main/java/com/huertian/jinduguanli/common/ApiResponse.java b/src/main/java/com/huertian/jinduguanli/common/ApiResponse.java new file mode 100644 index 0000000..79d9d99 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/common/ApiResponse.java @@ -0,0 +1,95 @@ +package com.huertian.jinduguanli.common; + +/** + * + * + * @param + */ +public class ApiResponse { + private int code; + private String message; + private T data; + + public ApiResponse() { + } + + public ApiResponse(int code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static ApiResponse success() { + return new ApiResponse<>(10000, "成功", null); + } + + public static ApiResponse success(T data) { + return new ApiResponse<>(10000, "成功", data); + } + + public static ApiResponse error(int code) { + return new ApiResponse<>(code, getMessageByCode(code), null); + } + + public static ApiResponse error(int code, String message) { + return new ApiResponse<>(code, message, null); + } + + private static String getMessageByCode(int code) { + switch (code) { + case 10001: + return "参数无效"; + case 10002: + return "邮箱已存在"; + case 10003: + return "用户不存在"; + case 10004: + return "密码错误"; + case 10005: + return "用户已禁用"; + case 10006: + return "未授权"; + case 10007: + return "令牌已过期"; + case 10008: + return "无效的令牌"; + case 10009: + return "系统错误"; + default: + return "未知错误"; + } + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + @Override + public String toString() { + return "ApiResponse{" + + "code=" + code + + ", message='" + message + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/common/ErrorCode.java b/src/main/java/com/huertian/jinduguanli/common/ErrorCode.java new file mode 100644 index 0000000..e0e1486 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/common/ErrorCode.java @@ -0,0 +1,14 @@ +package com.huertian.jinduguanli.common; + +public class ErrorCode { + public static final int SUCCESS = 10000; + public static final int SYSTEM_ERROR = 10001; + public static final int INVALID_PARAM = 10002; + public static final int USER_NOT_FOUND = 10003; + public static final int USER_NOT_FOUND_OR_DISABLED = 10004; + public static final int EMAIL_EXISTS = 10005; + public static final int PASSWORD_INCORRECT = 10006; + public static final int INVALID_TOKEN = 10007; + public static final int UNAUTHORIZED = 10008; + public static final int USERNAME_EXISTS = 10009; +} diff --git a/src/main/java/com/huertian/jinduguanli/common/Result.java b/src/main/java/com/huertian/jinduguanli/common/Result.java new file mode 100644 index 0000000..7c909f5 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/common/Result.java @@ -0,0 +1,28 @@ +package com.huertian.jinduguanli.common; + +import lombok.Data; + +@Data +public class Result { + private Integer code; + private String message; + private T data; + + public Result(Integer code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static Result success(T data) { + return new Result<>(0, "success", data); + } + + public static Result error(String message) { + return new Result<>(10009, message, null); + } + + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/config/GlobalExceptionHandler.java b/src/main/java/com/huertian/jinduguanli/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..3b73a68 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/config/GlobalExceptionHandler.java @@ -0,0 +1,44 @@ +package com.huertian.jinduguanli.config; + +import com.huertian.jinduguanli.common.ApiResponse; +import jakarta.persistence.EntityNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(EntityNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handleEntityNotFoundException(EntityNotFoundException e) { + logger.error("Entity not found: {}", e.getMessage()); + return ApiResponse.error(10003, e.getMessage()); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleIllegalArgumentException(IllegalArgumentException e) { + logger.error("Invalid argument: {}", e.getMessage()); + return ApiResponse.error(10001, e.getMessage()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + logger.error("Invalid request body: {}", e.getMessage()); + return ApiResponse.error(10001, "请求体格式不正确"); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception e) { + logger.error("Unexpected error: ", e); + return ApiResponse.error(10009, "系统错误"); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/config/JwtConfig.java b/src/main/java/com/huertian/jinduguanli/config/JwtConfig.java new file mode 100644 index 0000000..08ab9fa --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/config/JwtConfig.java @@ -0,0 +1,27 @@ +package com.huertian.jinduguanli.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jwt") +public class JwtConfig { + private long defaultExpirationMs = 86400000; // 默认24小时 + private long rememberExpirationMs = 604800000; // 默认7天 + + public long getDefaultExpirationMs() { + return defaultExpirationMs; + } + + public void setDefaultExpirationMs(long defaultExpirationMs) { + this.defaultExpirationMs = defaultExpirationMs; + } + + public long getRememberExpirationMs() { + return rememberExpirationMs; + } + + public void setRememberExpirationMs(long rememberExpirationMs) { + this.rememberExpirationMs = rememberExpirationMs; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/config/RedisConfig.java b/src/main/java/com/huertian/jinduguanli/config/RedisConfig.java new file mode 100644 index 0000000..e379502 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/config/RedisConfig.java @@ -0,0 +1,81 @@ +package com.huertian.jinduguanli.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class RedisConfig { + private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class); + + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private int redisPort; + + @Value("${spring.redis.password}") + private String redisPassword; + + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort); + if (redisPassword != null && !redisPassword.isEmpty()) { + config.setPassword(redisPassword); + } + logger.info("Configuring Redis connection to {}:{}", redisHost, redisPort); + return new LettuceConnectionFactory(config); + } + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory factory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(60)) + .disableCachingNullValues(); + + return RedisCacheManager.builder(factory) + .cacheDefaults(config) + .build(); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + // 设置key的序列化方式 + template.setKeySerializer(new StringRedisSerializer()); + // 设置value的序列化方式 + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + // 设置hash key的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + // 设置hash value的序列化方式 + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + template.afterPropertiesSet(); + + // 测试连接 + try { + template.getConnectionFactory().getConnection().ping(); + logger.info("Redis连接测试成功"); + } catch (Exception e) { + logger.error("Redis连接测试失败: {}", e.getMessage(), e); + } + + return template; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/config/SecurityConfig.java b/src/main/java/com/huertian/jinduguanli/config/SecurityConfig.java new file mode 100644 index 0000000..27df27a --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/config/SecurityConfig.java @@ -0,0 +1,73 @@ +package com.huertian.jinduguanli.config; + +import com.huertian.jinduguanli.security.JwtAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers("/api/users/login").permitAll() + .requestMatchers(HttpMethod.POST, "/api/users").permitAll() + .requestMatchers(HttpMethod.GET, "/api/lesson/task/**").authenticated() + .anyRequest().authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setExposedHeaders(Arrays.asList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/controller/LessonTaskController.java b/src/main/java/com/huertian/jinduguanli/controller/LessonTaskController.java new file mode 100644 index 0000000..aa29ba8 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/controller/LessonTaskController.java @@ -0,0 +1,89 @@ +package com.huertian.jinduguanli.controller; + +import com.huertian.jinduguanli.common.ApiResponse; +import com.huertian.jinduguanli.common.ErrorCode; +import com.huertian.jinduguanli.dto.LessonTaskRequest; +import com.huertian.jinduguanli.entity.LessonTask; +import com.huertian.jinduguanli.service.LessonTaskService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/lesson-tasks") +public class LessonTaskController { + private static final Logger logger = LoggerFactory.getLogger(LessonTaskController.class); + private final LessonTaskService lessonTaskService; + + @Autowired + public LessonTaskController(LessonTaskService lessonTaskService) { + this.lessonTaskService = lessonTaskService; + } + + @GetMapping + public ApiResponse> listTasks( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) Long userId) { + logger.info("收到课程任务列表查询请求,页码: {},每页数量: {},用户ID: {}", page, size, userId); + try { + Page tasks = lessonTaskService.findAll(PageRequest.of(page - 1, size), userId); + return ApiResponse.success(tasks); + } catch (Exception e) { + logger.error("查询课程任务列表失败", e); + return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "获取课程任务列表失败", null); + } + } + + @GetMapping("/{id}") + public ApiResponse getTask(@PathVariable Long id) { + logger.info("收到课程任务查询请求,任务ID: {}", id); + try { + LessonTask task = lessonTaskService.findById(id); + return ApiResponse.success(task); + } catch (Exception e) { + logger.error("查询课程任务失败", e); + return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "获取课程任务失败", null); + } + } + + @PostMapping + public ApiResponse createTask(@Valid @RequestBody LessonTaskRequest request) { + logger.info("收到创建课程任务请求"); + try { + LessonTask task = lessonTaskService.create(request); + return ApiResponse.success(task); + } catch (Exception e) { + logger.error("创建课程任务失败", e); + return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "创建课程任务失败", null); + } + } + + @PutMapping("/{id}") + public ApiResponse updateTask(@PathVariable Long id, @Valid @RequestBody LessonTaskRequest request) { + logger.info("收到更新课程任务请求,任务ID: {}", id); + try { + LessonTask task = lessonTaskService.update(id, request); + return ApiResponse.success(task); + } catch (Exception e) { + logger.error("更新课程任务失败", e); + return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "更新课程任务失败", null); + } + } + + @DeleteMapping("/{id}") + public ApiResponse deleteTask(@PathVariable Long id) { + logger.info("收到删除课程任务请求,任务ID: {}", id); + try { + lessonTaskService.delete(id); + return ApiResponse.success(); + } catch (Exception e) { + logger.error("删除课程任务失败", e); + return new ApiResponse<>(ErrorCode.SYSTEM_ERROR, "删除课程任务失败", null); + } + } +} diff --git a/src/main/java/com/huertian/jinduguanli/controller/UserController.java b/src/main/java/com/huertian/jinduguanli/controller/UserController.java new file mode 100644 index 0000000..0dfc8a5 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/controller/UserController.java @@ -0,0 +1,88 @@ +package com.huertian.jinduguanli.controller; + +import com.huertian.jinduguanli.common.ApiResponse; +import com.huertian.jinduguanli.dto.CreateUserRequest; +import com.huertian.jinduguanli.dto.LoginResponse; +import com.huertian.jinduguanli.dto.UserLoginRequest; +import com.huertian.jinduguanli.entity.User; +import com.huertian.jinduguanli.security.service.JwtService; +import com.huertian.jinduguanli.security.service.TokenBlacklistService; +import com.huertian.jinduguanli.service.UserService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/users") +public class UserController { + private static final Logger logger = LoggerFactory.getLogger(UserController.class); + + private final UserService userService; + private final JwtService jwtService; + private final TokenBlacklistService tokenBlacklistService; + + @Autowired + public UserController( + UserService userService, + JwtService jwtService, + TokenBlacklistService tokenBlacklistService) { + this.userService = userService; + this.jwtService = jwtService; + this.tokenBlacklistService = tokenBlacklistService; + } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody UserLoginRequest request) { + logger.info("收到登录请求,邮箱: {}", request.getEmail()); + ApiResponse response = userService.login(request); + logger.info("登录响应: {}", response); + return response; + } + + @PostMapping("/logout") + public ApiResponse logout(@RequestHeader("Authorization") String authHeader) { + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + // 获取token的过期时间 + long expiration = jwtService.extractExpiration(token).getTime(); + // 将token加入黑名单 + tokenBlacklistService.addToBlacklist(token, expiration); + return ApiResponse.success("登出成功"); + } + return ApiResponse.success("登出成功"); + } + + @PostMapping + public ApiResponse createUser(@Valid @RequestBody CreateUserRequest request) { + logger.info("收到创建用户请求,用户名: {}", request.getUsername()); + return userService.createUser(request); + } + + @GetMapping("/current") + public ApiResponse getCurrentUser(@RequestHeader("Authorization") String authHeader) { + logger.info("收到获取当前用户信息请求,Authorization: {}", authHeader); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + logger.info("解析出的token: {}", token); + return userService.getCurrentUser(token); + } + logger.warn("未提供有效的认证令牌"); + return ApiResponse.error(401, "未提供有效的认证令牌"); + } + + @GetMapping("/list") + public ApiResponse listUsers( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer limit) { + logger.info("收到用户列表分页查询请求,页码: {},每页数量: {}", page, limit); + return userService.listUsers(page, limit); + } + + @PostMapping("/disable/{userId}") + public ApiResponse disableUser(@PathVariable Long userId) { + logger.info("收到禁用用户请求,用户ID: {}", userId); + return userService.disableUser(userId); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/dto/CreateUserRequest.java b/src/main/java/com/huertian/jinduguanli/dto/CreateUserRequest.java new file mode 100644 index 0000000..3bad696 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/dto/CreateUserRequest.java @@ -0,0 +1,87 @@ +package com.huertian.jinduguanli.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class CreateUserRequest { + @NotBlank(message = "用户名不能为空") + private String username; + + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + @NotBlank(message = "密码不能为空") + private String password; + + @NotNull(message = "部门ID不能为空") + private Long departmentId; + + @NotNull(message = "角色不能为空") + private Integer roles; + + @NotNull(message = "岗位不能为空") + private Integer jobs; + + @NotNull(message = "创建者ID不能为空") + private Long creatorId; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public Integer getRoles() { + return roles; + } + + public void setRoles(Integer roles) { + this.roles = roles; + } + + public Integer getJobs() { + return jobs; + } + + public void setJobs(Integer jobs) { + this.jobs = jobs; + } + + public Long getCreatorId() { + return creatorId; + } + + public void setCreatorId(Long creatorId) { + this.creatorId = creatorId; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/dto/LessonTaskRequest.java b/src/main/java/com/huertian/jinduguanli/dto/LessonTaskRequest.java new file mode 100644 index 0000000..226e395 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/dto/LessonTaskRequest.java @@ -0,0 +1,99 @@ +package com.huertian.jinduguanli.dto; + +import lombok.Data; +import lombok.ToString; + +@Data +@ToString +public class LessonTaskRequest { + private String courseName; + private String microLessonName; + private Long userId; + private Integer progressStatus; + private Long scriptUploadTime; + private Long scriptConfirmTime; + private Long videoCaptureTime; + private Long videoConfirmTime; + private Long finishTime; + private String advise; + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public String getMicroLessonName() { + return microLessonName; + } + + public void setMicroLessonName(String microLessonName) { + this.microLessonName = microLessonName; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Integer getProgressStatus() { + return progressStatus; + } + + public void setProgressStatus(Integer progressStatus) { + this.progressStatus = progressStatus; + } + + public Long getScriptUploadTime() { + return scriptUploadTime; + } + + public void setScriptUploadTime(Long scriptUploadTime) { + this.scriptUploadTime = scriptUploadTime; + } + + public Long getScriptConfirmTime() { + return scriptConfirmTime; + } + + public void setScriptConfirmTime(Long scriptConfirmTime) { + this.scriptConfirmTime = scriptConfirmTime; + } + + public Long getVideoCaptureTime() { + return videoCaptureTime; + } + + public void setVideoCaptureTime(Long videoCaptureTime) { + this.videoCaptureTime = videoCaptureTime; + } + + public Long getVideoConfirmTime() { + return videoConfirmTime; + } + + public void setVideoConfirmTime(Long videoConfirmTime) { + this.videoConfirmTime = videoConfirmTime; + } + + public Long getFinishTime() { + return finishTime; + } + + public void setFinishTime(Long finishTime) { + this.finishTime = finishTime; + } + + public String getAdvise() { + return advise; + } + + public void setAdvise(String advise) { + this.advise = advise; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/dto/LoginResponse.java b/src/main/java/com/huertian/jinduguanli/dto/LoginResponse.java new file mode 100644 index 0000000..f6f194d --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/dto/LoginResponse.java @@ -0,0 +1,17 @@ +package com.huertian.jinduguanli.dto; + +public class LoginResponse { + private String token; + + public LoginResponse(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/dto/UserLoginRequest.java b/src/main/java/com/huertian/jinduguanli/dto/UserLoginRequest.java new file mode 100644 index 0000000..318389b --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/dto/UserLoginRequest.java @@ -0,0 +1,39 @@ +package com.huertian.jinduguanli.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public class UserLoginRequest { + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + @NotBlank(message = "密码不能为空") + private String password; + + private Boolean remember = false; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getRemember() { + return remember; + } + + public void setRemember(Boolean remember) { + this.remember = remember != null ? remember : false; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/dto/UserPageResponse.java b/src/main/java/com/huertian/jinduguanli/dto/UserPageResponse.java new file mode 100644 index 0000000..f7213db --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/dto/UserPageResponse.java @@ -0,0 +1,46 @@ +package com.huertian.jinduguanli.dto; + +import com.huertian.jinduguanli.entity.User; +import lombok.Data; + +import java.util.List; + +@Data +public class UserPageResponse { + private List list; + private Long total; + private Integer currentPage; + private Integer pageSize; + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public Long getTotal() { + return total; + } + + public void setTotal(Long total) { + this.total = total; + } + + public Integer getCurrentPage() { + return currentPage; + } + + public void setCurrentPage(Integer currentPage) { + this.currentPage = currentPage; + } + + public Integer getPageSize() { + return pageSize; + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/entity/LessonTask.java b/src/main/java/com/huertian/jinduguanli/entity/LessonTask.java new file mode 100644 index 0000000..9d50cca --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/entity/LessonTask.java @@ -0,0 +1,68 @@ +package com.huertian.jinduguanli.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.ToString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.Serializable; + +@Entity +@Table(name = "lesson_tasks") +@Data +@ToString +public class LessonTask implements Serializable { + private static final long serialVersionUID = 1L; + + private static final Logger logger = LoggerFactory.getLogger(LessonTask.class); + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String courseName; + + @Column(nullable = false) + private String microLessonName; + + @Column(nullable = false) + private Long userId; + + @Column(nullable = false) + private Integer progressStatus; + + private Long scriptUploadTime; + private Long scriptConfirmTime; + private Long videoCaptureTime; + private Long videoConfirmTime; + private Long finishTime; + + private String advise; + + @Column(nullable = false) + private Long createdAt; + + @Column(nullable = false) + private Long updatedAt; + + @PrePersist + protected void onCreate() { + logger.info("创建新课程任务 - 课程名称: {}, 小课名称: {}, 用户ID: {}", + this.courseName, this.microLessonName, this.userId); + progressStatus = 1; + long now = System.currentTimeMillis(); + createdAt = now; + updatedAt = now; + logger.info("创建课程任务成功 - 进度状态: {}, 创建时间: {}, 更新时间: {}", + progressStatus, createdAt, updatedAt); + } + + @PreUpdate + protected void onUpdate() { + logger.info("更新课程任务 - ID: {}, 课程名称: {}, 小课名称: {}", + this.id, this.courseName, this.microLessonName); + updatedAt = System.currentTimeMillis(); + logger.info("更新课程任务成功 - ID: {}, 更新时间: {}", id, updatedAt); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/entity/User.java b/src/main/java/com/huertian/jinduguanli/entity/User.java new file mode 100644 index 0000000..b311a38 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/entity/User.java @@ -0,0 +1,173 @@ +package com.huertian.jinduguanli.entity; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +@Entity +@Table(name = "users") +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String username; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String password; + + @Column(name = "department_id") + private Long departmentId; + + @Column + private Integer roles = 0; + + @Column + private Integer jobs = 0; + + private String avatar; + + @Column(name = "creator_id") + private Long creatorId = 0L; + + @Column(nullable = false) + private Integer status; + + @Column(name = "created_at", nullable = false) + private Long createdAt; + + @Column(name = "updated_at", nullable = false) + private Long updatedAt; + + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; // 返回 username 而不是 email + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return status == 1; // 假设 status == 1 表示账户未锁定 + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status == 1; // 假设 status == 1 表示账户启用 + } + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } + + public Integer getRoles() { + return roles; + } + + public void setRoles(Integer roles) { + this.roles = roles; + } + + public Integer getJobs() { + return jobs; + } + + public void setJobs(Integer jobs) { + this.jobs = jobs; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public Long getCreatorId() { + return creatorId; + } + + public void setCreatorId(Long creatorId) { + this.creatorId = creatorId; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Long getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Long createdAt) { + this.createdAt = createdAt; + } + + public Long getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Long updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/repository/LessonTaskRepository.java b/src/main/java/com/huertian/jinduguanli/repository/LessonTaskRepository.java new file mode 100644 index 0000000..1ee1c9b --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/repository/LessonTaskRepository.java @@ -0,0 +1,12 @@ +package com.huertian.jinduguanli.repository; + +import com.huertian.jinduguanli.entity.LessonTask; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LessonTaskRepository extends JpaRepository { + Page findByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/com/huertian/jinduguanli/repository/UserRepository.java b/src/main/java/com/huertian/jinduguanli/repository/UserRepository.java new file mode 100644 index 0000000..b2cab15 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/repository/UserRepository.java @@ -0,0 +1,20 @@ +package com.huertian.jinduguanli.repository; + +import com.huertian.jinduguanli.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findByUsername(String username); + + boolean existsByEmail(String email); + + @Query(value = "SELECT * FROM users LIMIT :limit OFFSET :offset", nativeQuery = true) + List findAllWithPagination(@Param("offset") int offset, @Param("limit") int limit); +} diff --git a/src/main/java/com/huertian/jinduguanli/security/CustomUserDetailsService.java b/src/main/java/com/huertian/jinduguanli/security/CustomUserDetailsService.java new file mode 100644 index 0000000..ee952ab --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/security/CustomUserDetailsService.java @@ -0,0 +1,43 @@ +package com.huertian.jinduguanli.security; + +import com.huertian.jinduguanli.entity.User; +import com.huertian.jinduguanli.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class); + + private final UserRepository userRepository; + + @Autowired + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + logger.info("Loading user details for email: {}", email); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> { + logger.error("User not found with email: {}", email); + return new UsernameNotFoundException("User not found with email: " + email); + }); + + logger.info("User found with email: {}", email); + + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + user.getPassword(), + new ArrayList<>()); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/security/JwtAuthenticationFilter.java b/src/main/java/com/huertian/jinduguanli/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..8348b40 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/security/JwtAuthenticationFilter.java @@ -0,0 +1,109 @@ +package com.huertian.jinduguanli.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.huertian.jinduguanli.common.ApiResponse; +import com.huertian.jinduguanli.common.ErrorCode; +import com.huertian.jinduguanli.security.service.JwtService; +import com.huertian.jinduguanli.security.service.TokenBlacklistService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetailsService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + private final TokenBlacklistService tokenBlacklistService; + private final ObjectMapper objectMapper; + + public JwtAuthenticationFilter( + JwtService jwtService, + UserDetailsService userDetailsService, + TokenBlacklistService tokenBlacklistService, + ObjectMapper objectMapper) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + this.tokenBlacklistService = tokenBlacklistService; + this.objectMapper = objectMapper; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + try { + // 允许OPTIONS请求通过 + if (request.getMethod().equals(HttpMethod.OPTIONS.name())) { + logger.debug("OPTIONS请求,直接放行"); + filterChain.doFilter(request, response); + return; + } + + final String authHeader = request.getHeader("Authorization"); + logger.debug("处理请求认证 - 请求路径: {}", request.getRequestURI()); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + logger.debug("未提供认证令牌或格式不正确"); + filterChain.doFilter(request, response); + return; + } + + final String jwt = authHeader.substring(7); + final String username = jwtService.extractUsername(jwt); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + logger.debug("开始验证用户令牌 - 用户邮箱: {}", username); + + if (tokenBlacklistService.isBlacklisted(jwt)) { + logger.warn("令牌已被加入黑名单"); + handleJwtException(response, "令牌已被加入黑名单"); + return; + } + + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtService.isTokenValid(jwt, userDetails)) { + logger.debug("令牌验证成功,设置安全上下文"); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } else { + logger.warn("令牌验证失败"); + handleJwtException(response, "令牌验证失败"); + return; + } + } + filterChain.doFilter(request, response); + } catch (Exception e) { + logger.error("处理认证时发生错误", e); + handleJwtException(response, "认证处理失败"); + } + } + + private void handleJwtException(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + ApiResponse apiResponse = ApiResponse.error(ErrorCode.INVALID_TOKEN, "token已过期,请重新登录"); + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/security/service/JwtService.java b/src/main/java/com/huertian/jinduguanli/security/service/JwtService.java new file mode 100644 index 0000000..08efebb --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/security/service/JwtService.java @@ -0,0 +1,106 @@ +package com.huertian.jinduguanli.security.service; + +import com.huertian.jinduguanli.entity.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Service +public class JwtService { + private static final Logger logger = LoggerFactory.getLogger(JwtService.class); + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration}") + private long jwtExpiration; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + public String generateToken(UserDetails userDetails) { + return generateToken(new HashMap<>(), userDetails); + } + + public String generateToken(Map extraClaims, UserDetails userDetails) { + Date issuedAt = new Date(System.currentTimeMillis()); + Date expiration = new Date(System.currentTimeMillis() + jwtExpiration); + logger.debug("Generating token for user: {}, expiration: {}, jwtExpiration: {}", userDetails.getUsername(), + expiration, jwtExpiration); + + return Jwts.builder() + .setClaims(extraClaims) + .setSubject(((User) userDetails).getEmail()) + .setIssuedAt(issuedAt) + .setExpiration(expiration) + .signWith(getSignInKey(), SignatureAlgorithm.HS384) + .compact(); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + try { + final String username = extractUsername(token); + Date expiration = extractExpiration(token); + Date now = new Date(); + boolean isExpired = expiration.before(now); + boolean isValid = username.equals(((User) userDetails).getEmail()) && !isExpired; + logger.debug("Token validation for user {}: valid={}, expiration={}, current={}, isExpired={}", + username, isValid, expiration, now, isExpired); + return isValid; + } catch (Exception e) { + logger.error("Token validation failed: {}", e.getMessage()); + return false; + } + } + + // private boolean isTokenExpired(String token) { + // Date expiration = extractExpiration(token); + // Date now = new Date(); + // boolean isExpired = expiration.before(now); + // logger.debug("Token expiration check: expiration={}, current={}, + // isExpired={}", + // expiration, now, isExpired); + // return isExpired; + // } + + private Claims extractAllClaims(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + logger.error("解析token失败: {}", e.getMessage()); + throw e; + } + } + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/com/huertian/jinduguanli/security/service/TokenBlacklistService.java b/src/main/java/com/huertian/jinduguanli/security/service/TokenBlacklistService.java new file mode 100644 index 0000000..01b2c5b --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/security/service/TokenBlacklistService.java @@ -0,0 +1,62 @@ +package com.huertian.jinduguanli.security.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Service +public class TokenBlacklistService { + private final StringRedisTemplate redisTemplate; + private static final String BLACKLIST_PREFIX = "token:blacklist:"; + private static final Logger logger = LoggerFactory.getLogger(TokenBlacklistService.class); + + public TokenBlacklistService(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 将令牌添加到黑名单 + * + * @param token JWT令牌 + * @param expiration 过期时间(毫秒) + */ + public void addToBlacklist(String token, long expiration) { + logger.info("将令牌加入黑名单 - 过期时间: {}", new Date(expiration)); + try { + String key = BLACKLIST_PREFIX + token; + long ttl = expiration - System.currentTimeMillis(); + if (ttl > 0) { + redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS); + logger.info("令牌已成功加入黑名单"); + } else { + logger.warn("令牌已过期,无需加入黑名单"); + } + } catch (Exception e) { + logger.error("将令牌加入黑名单时发生错误", e); + throw new RuntimeException("无法将令牌加入黑名单", e); + } + } + + /** + * 检查令牌是否在黑名单中 + * + * @param token JWT令牌 + * @return 如果在黑名单中返回true + */ + public boolean isBlacklisted(String token) { + logger.debug("检查令牌是否在黑名单中"); + try { + String key = BLACKLIST_PREFIX + token; + Boolean exists = redisTemplate.hasKey(key); + logger.debug("令牌黑名单检查结果: {}", exists); + return Boolean.TRUE.equals(exists); + } catch (Exception e) { + logger.error("检查令牌黑名单状态时发生错误", e); + return false; + } + } +} diff --git a/src/main/java/com/huertian/jinduguanli/security/service/UserDetailsService.java b/src/main/java/com/huertian/jinduguanli/security/service/UserDetailsService.java new file mode 100644 index 0000000..e6f1582 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/security/service/UserDetailsService.java @@ -0,0 +1,33 @@ +package com.huertian.jinduguanli.security.service; + +import com.huertian.jinduguanli.entity.User; +import com.huertian.jinduguanli.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { + private final Logger logger = LoggerFactory.getLogger(UserDetailsService.class); + private final UserRepository userRepository; + + public UserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + logger.info("开始加载用户信息 - 邮箱: {}", email); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> { + logger.error("未找到用户 - 邮箱: {}", email); + return new UsernameNotFoundException("未找到用户"); + }); + + logger.info("成功加载用户信息 - 用户名: {}", user.getUsername()); + return user; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/service/LessonTaskService.java b/src/main/java/com/huertian/jinduguanli/service/LessonTaskService.java new file mode 100644 index 0000000..c318708 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/service/LessonTaskService.java @@ -0,0 +1,108 @@ +package com.huertian.jinduguanli.service; + +import com.huertian.jinduguanli.dto.LessonTaskRequest; +import com.huertian.jinduguanli.entity.LessonTask; +import com.huertian.jinduguanli.repository.LessonTaskRepository; +import jakarta.persistence.EntityNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.BeanUtils; + +@Service +public class LessonTaskService { + private static final Logger logger = LoggerFactory.getLogger(LessonTaskService.class); + private final LessonTaskRepository lessonTaskRepository; + + public LessonTaskService(LessonTaskRepository lessonTaskRepository) { + this.lessonTaskRepository = lessonTaskRepository; + } + + @Cacheable(value = "lessonTasks", key = "#userId != null ? 'user:' + #userId + ':page:' + #pageable.pageNumber : 'all:page:' + #pageable.pageNumber") + public Page findAll(Pageable pageable, Long userId) { + logger.info("查询课程任务 - 页码: {}, 每页数量: {}, 用户ID: {}", + pageable.getPageNumber(), pageable.getPageSize(), userId); + + Page result; + if (userId != null) { + result = lessonTaskRepository.findByUserId(userId, pageable); + } else { + result = lessonTaskRepository.findAll(pageable); + } + + logger.info("查询到 {} 条课程任务", result.getTotalElements()); + return result; + } + + @Cacheable(value = "lessonTask", key = "#id") + public LessonTask findById(Long id) { + logger.info("根据ID查询课程任务 - 任务ID: {}", id); + return lessonTaskRepository.findById(id) + .orElseThrow(() -> { + logger.error("未找到ID为 {} 的课程任务", id); + return new EntityNotFoundException("未找到ID为 " + id + " 的任务"); + }); + } + + @Transactional + @CacheEvict(value = { "lessonTasks", "lessonTask" }, allEntries = true) + public LessonTask create(LessonTaskRequest request) { + logger.info("开始创建课程任务 - 课程名称: {}, 小课名称: {}, 用户ID: {}", + request.getCourseName(), request.getMicroLessonName(), request.getUserId()); + validateRequest(request); + LessonTask task = new LessonTask(); + BeanUtils.copyProperties(request, task); + task.setProgressStatus(1); // 初始状态 + LessonTask savedTask = lessonTaskRepository.save(task); + logger.info("创建课程任务成功 - 任务ID: {}", savedTask.getId()); + return savedTask; + } + + @Transactional + @CacheEvict(value = { "lessonTasks", "lessonTask" }, allEntries = true) + public LessonTask update(Long id, LessonTaskRequest request) { + logger.info("开始更新课程任务 - 任务ID: {}, 课程名称: {}, 小课名称: {}", + id, request.getCourseName(), request.getMicroLessonName()); + LessonTask task = findById(id); + + // 保留原有的进度状态,如果请求中包含新的进度状态则更新 + Integer originalStatus = task.getProgressStatus(); + BeanUtils.copyProperties(request, task); + if (task.getProgressStatus() == null) { + task.setProgressStatus(originalStatus); + } + + LessonTask updatedTask = lessonTaskRepository.save(task); + logger.info("更新课程任务成功 - 任务ID: {}", updatedTask.getId()); + return updatedTask; + } + + @Transactional + @CacheEvict(value = { "lessonTasks", "lessonTask" }, allEntries = true) + public void delete(Long id) { + logger.info("开始删除课程任务 - 任务ID: {}", id); + if (!lessonTaskRepository.existsById(id)) { + logger.error("未找到ID为 {} 的课程任务", id); + throw new EntityNotFoundException("未找到ID为 " + id + " 的任务"); + } + lessonTaskRepository.deleteById(id); + logger.info("删除课程任务成功 - 任务ID: {}", id); + } + + private void validateRequest(LessonTaskRequest request) { + if (request.getCourseName() == null || request.getCourseName().trim().isEmpty()) { + throw new IllegalArgumentException("课程名称不能为空"); + } + if (request.getMicroLessonName() == null || request.getMicroLessonName().trim().isEmpty()) { + throw new IllegalArgumentException("小课名称不能为空"); + } + if (request.getUserId() == null) { + throw new IllegalArgumentException("用户ID不能为空"); + } + } +} diff --git a/src/main/java/com/huertian/jinduguanli/service/UserService.java b/src/main/java/com/huertian/jinduguanli/service/UserService.java new file mode 100644 index 0000000..f8f4111 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/service/UserService.java @@ -0,0 +1,171 @@ +package com.huertian.jinduguanli.service; + +import com.huertian.jinduguanli.common.ApiResponse; +import com.huertian.jinduguanli.common.ErrorCode; +import com.huertian.jinduguanli.dto.CreateUserRequest; +import com.huertian.jinduguanli.dto.LoginResponse; +import com.huertian.jinduguanli.dto.UserLoginRequest; +import com.huertian.jinduguanli.dto.UserPageResponse; +import com.huertian.jinduguanli.entity.User; +import com.huertian.jinduguanli.repository.UserRepository; +import com.huertian.jinduguanli.security.service.JwtService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class UserService implements UserDetailsService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + @Autowired + public UserService(JwtService jwtService, PasswordEncoder passwordEncoder, UserRepository userRepository) { + this.jwtService = jwtService; + this.passwordEncoder = passwordEncoder; + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Optional user = userRepository.findByEmail(email); + if (user.isEmpty()) { + throw new UsernameNotFoundException("User not found with email: " + email); + } + return user.get(); + } + + public ApiResponse login(UserLoginRequest request) { + Optional userOpt = userRepository.findByEmail(request.getEmail()); + if (userOpt.isEmpty() || userOpt.get().getStatus() != 1) { + return new ApiResponse<>(ErrorCode.USER_NOT_FOUND_OR_DISABLED, "用户不存在或已禁用", null); + } + + User user = userOpt.get(); + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + return new ApiResponse<>(ErrorCode.PASSWORD_INCORRECT, "密码错误", null); + } + + String token = jwtService.generateToken(user); + return ApiResponse.success(new LoginResponse(token)); + } + + public ApiResponse createUser(CreateUserRequest request) { + // 检查邮箱是否已存在 + if (userRepository.findByEmail(request.getEmail()).isPresent()) { + return new ApiResponse<>(ErrorCode.EMAIL_EXISTS, "邮箱已存在", null); + } + + User user = new User(); + user.setEmail(request.getEmail()); + user.setUsername(request.getUsername()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setDepartmentId(request.getDepartmentId()); + user.setRoles(request.getRoles()); + user.setJobs(request.getJobs()); + user.setCreatorId(request.getCreatorId()); + user.setStatus(1); + user.setCreatedAt(System.currentTimeMillis()); + user.setUpdatedAt(System.currentTimeMillis()); + + userRepository.save(user); + + return ApiResponse.success(); + } + + public ApiResponse getCurrentUser(String token) { + String email = jwtService.extractUsername(token); + logger.info("从token中提取的邮箱: {}", email); + Optional userOpt = userRepository.findByEmail(email); + if (userOpt.isEmpty()) { + logger.warn("用户不存在: {}", email); + return new ApiResponse<>(ErrorCode.USER_NOT_FOUND, "用户不存在", null); + } + + User user = userOpt.get(); + logger.info("找到用户: {}", user.getUsername()); + user.setPassword(null); + return ApiResponse.success(user); + } + + public ApiResponse disableUser(Long userId) { + Optional userOpt = userRepository.findById(userId); + + if (userOpt.isEmpty()) { + return new ApiResponse<>(ErrorCode.USER_NOT_FOUND, "用户不存在", null); + } + + User user = userOpt.get(); + user.setStatus(0); + userRepository.save(user); + + return ApiResponse.success("用户已禁用"); + } + + public ApiResponse listUsers(Integer currentPage, Integer pageSize) { + // 参数校验 + if (currentPage < 1) { + currentPage = 1; + } + if (pageSize < 1) { + pageSize = 10; + } + + // 查询数据 + Page userPage = userRepository.findAll(PageRequest.of(currentPage - 1, pageSize)); + + // 处理返回数据 + UserPageResponse response = new UserPageResponse(); + response.setTotal(userPage.getTotalElements()); + response.setCurrentPage(currentPage); + response.setPageSize(pageSize); + + List users = userPage.getContent(); + users.forEach(user -> user.setPassword(null)); + response.setList(users); + + return ApiResponse.success(response); + } + + public User register(User user) { + // 检查邮箱是否已存在 + if (userRepository.findByEmail(user.getEmail()).isPresent()) { + throw new RuntimeException("邮箱已存在"); + } + + // 检查用户名是否已存在 + if (userRepository.findByUsername(user.getUsername()).isPresent()) { + throw new RuntimeException("用户名已存在"); + } + + // 设置默认值 + user.setDepartmentId(0L); + user.setRoles(0); + user.setJobs(0); + user.setCreatorId(0L); + + // 加密密码 + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setStatus(1); + user.setCreatedAt(System.currentTimeMillis()); + user.setUpdatedAt(System.currentTimeMillis()); + + // 保存用户 + User savedUser = userRepository.save(user); + + // 清除密码后返回 + savedUser.setPassword(null); + return savedUser; + } +} diff --git a/src/main/java/com/huertian/jinduguanli/utils/JwtUtil.java b/src/main/java/com/huertian/jinduguanli/utils/JwtUtil.java new file mode 100644 index 0000000..eb61067 --- /dev/null +++ b/src/main/java/com/huertian/jinduguanli/utils/JwtUtil.java @@ -0,0 +1,121 @@ +package com.huertian.jinduguanli.utils; + +import com.huertian.jinduguanli.entity.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); + + @Value("${jwt.secret}") + private String SECRET_KEY; + + @Value("${jwt.expiration}") + private long jwtExpiration; + + public String extractUsername(String token) { + logger.debug("开始从token中提取用户名"); + try { + String username = extractClaim(token, Claims::getSubject); + logger.debug("提取的用户名: {}", username); + return username; + } catch (ExpiredJwtException e) { + logger.error("token已过期", e); + throw e; + } catch (Exception e) { + logger.error("从token中提取用户名时发生错误", e); + throw e; + } + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + logger.debug("开始从token中提取所有声明"); + try { + Claims claims = Jwts + .parserBuilder() + .setSigningKey(getSignKey()) + .build() + .parseClaimsJws(token) + .getBody(); + logger.debug("声明提取成功"); + return claims; + } catch (Exception e) { + logger.error("从token中提取声明时发生错误", e); + throw e; + } + } + + private Boolean isTokenExpired(String token) { + try { + Date expiration = extractExpiration(token); + boolean isExpired = expiration.before(new Date()); + logger.debug("token过期检查 - 过期时间: {}, 是否过期: {}", expiration, isExpired); + return isExpired; + } catch (Exception e) { + logger.error("检查token过期时发生错误", e); + throw e; + } + } + + public String generateToken(String userName) { + logger.debug("开始为用户创建token: {}", userName); + try { + Map claims = new HashMap<>(); + String token = Jwts.builder() + .setClaims(claims) + .setSubject(userName) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSignKey(), SignatureAlgorithm.HS384) + .compact(); + logger.debug("token创建成功"); + return token; + } catch (Exception e) { + logger.error("创建token时发生错误", e); + throw e; + } + } + + public Boolean validateToken(String token, User user) { + logger.debug("开始验证用户token: {}", user.getUsername()); + try { + final String username = extractUsername(token); + boolean isValid = username.equals(user.getUsername()) && !isTokenExpired(token); + logger.debug("token验证结果 - 用户名匹配: {}, 未过期: {}", + username.equals(user.getUsername()), !isTokenExpired(token)); + return isValid; + } catch (Exception e) { + logger.error("验证token时发生错误", e); + return false; + } + } + + private Key getSignKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..2381c63 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,21 @@ +# 数据库配置 +spring.datasource.url=jdbc:mysql://172.16.215.132:3306/fenshenzhike?useSSL=false&serverTimezone=UTC +spring.datasource.username=root +spring.datasource.password=123 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +server.port=1218 + +# JPA配置 +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.format_sql=true + +# JWT配置 +jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 +jwt.expiration=604800000 + +# 日志配置 +logging.level.org.springframework.security=DEBUG +logging.level.com.huertian.jinduguanli=DEBUG \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..2e15aba --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,48 @@ +server: + port: 1218 + +spring: + main: + banner-mode: console + datasource: + url: jdbc:mysql://172.16.215.132:3306/fenshenzhike?useSSL=false&serverTimezone=UTC + username: root + password: 123 + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + use_sql_comments: true + redis: + host: 172.16.215.132 + port: 6379 + database: 0 + timeout: 10000 + password: # 如果Redis设置了密码,需要在这里添加 + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + shutdown-timeout: 100ms + client-name: jinduguanli + connect-timeout: 5000 + socket-timeout: 5000 + client-type: lettuce + +jwt: + secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 + expiration: 86400000 # 24小时 + +logging: + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + com.huertian.jinduguanli: DEBUG + org.springframework.data.redis: DEBUG + io.lettuce.core: DEBUG diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..824d93d --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _________________ +< Hi, 我是牛哥。。 > + ----------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || \ No newline at end of file diff --git a/src/test/.DS_Store b/src/test/.DS_Store new file mode 100644 index 0000000..b007f07 Binary files /dev/null and b/src/test/.DS_Store differ diff --git a/src/test/java/.DS_Store b/src/test/java/.DS_Store new file mode 100644 index 0000000..c3fe803 Binary files /dev/null and b/src/test/java/.DS_Store differ diff --git a/src/test/java/com/.DS_Store b/src/test/java/com/.DS_Store new file mode 100644 index 0000000..b0de012 Binary files /dev/null and b/src/test/java/com/.DS_Store differ diff --git a/src/test/java/com/huertian/.DS_Store b/src/test/java/com/huertian/.DS_Store new file mode 100644 index 0000000..fc0df72 Binary files /dev/null and b/src/test/java/com/huertian/.DS_Store differ diff --git a/src/test/java/com/huertian/jinduguanli/JinduguanliApplicationTests.java b/src/test/java/com/huertian/jinduguanli/JinduguanliApplicationTests.java new file mode 100644 index 0000000..c9941df --- /dev/null +++ b/src/test/java/com/huertian/jinduguanli/JinduguanliApplicationTests.java @@ -0,0 +1,13 @@ +package com.huertian.jinduguanli; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JinduguanliApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/数据库-新.md b/数据库-新.md new file mode 100644 index 0000000..e8d0154 --- /dev/null +++ b/数据库-新.md @@ -0,0 +1,145 @@ +# 数据库设计文档 + +## 1. 部门表 (departments) + +用于存储组织的部门信息。 + +```sql +CREATE TABLE departments ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL COMMENT '部门名称', + description TEXT DEFAULT NULL COMMENT '部门描述', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='部门表'; +``` + +### 字段说明 +- `id`: 部门ID,自增主键 +- `name`: 部门名称,不可为空 +- `description`: 部门描述,可为空 +- `created_at`: 创建时间,毫秒级时间戳 +- `updated_at`: 更新时间,毫秒级时间戳 + +## 2. 用户表 (users) + +存储系统用户信息,包括教师、管理员等角色。 + +```sql +CREATE TABLE users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL COMMENT '用户名', + email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱', + password VARCHAR(100) NOT NULL COMMENT '密码', + department_id BIGINT NOT NULL COMMENT '所属部门', + roles INT NOT NULL COMMENT '角色: 1-教师, 2-普通管理员, 3-沟通联络人, 4-系统管理员', + jobs INT NOT NULL COMMENT '岗位: 1-课程制作教师, 2-课程购买方项目负责人, 3-课程制作方沟通联络人, 4-系统制作方项目负责人', + avatar VARCHAR(255) DEFAULT NULL COMMENT '头像', + creator_id BIGINT NOT NULL DEFAULT 1 COMMENT '创建用户的管理员ID', + status INT NOT NULL DEFAULT 1 COMMENT '用户状态: 1-正常, 0-禁用', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)', + FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE RESTRICT, + INDEX idx_users_department_id (department_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='用户表'; +``` + +### 字段说明 +- `id`: 用户ID,自增主键 +- `username`: 用户名,不可为空 +- `email`: 邮箱地址,不可为空,唯一索引 +- `password`: 密码(加密存储),不可为空 +- `department_id`: 所属部门ID,外键关联departments表 +- `roles`: 用户角色,整数枚举: + - 1: 教师 + - 2: 普通管理员 + - 3: 沟通联络人 + - 4: 系统管理员 +- `jobs`: 用户岗位,整数枚举: + - 1: 课程制作教师 + - 2: 课程购买方项目负责人 + - 3: 课程制作方沟通联络人 + - 4: 系统制作方项目负责人 +- `avatar`: 用户头像URL,可为空 +- `creator_id`: 创建该用户的管理员ID +- `status`: 用户状态: + - 1: 正常 + - 0: 禁用 +- `created_at`: 创建时间,毫秒级时间戳 +- `updated_at`: 更新时间,毫秒级时间戳 + +### 索引 +- 主键索引:`id` +- 外键索引:`idx_users_department_id (department_id)` +- 唯一索引:`email` + +## 3. 课程任务表 (lesson_tasks) + +存储课程制作任务的信息和进度。 + +```sql +CREATE TABLE lesson_tasks ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + course_name VARCHAR(100) NOT NULL COMMENT '课程名称', + micro_lesson_name VARCHAR(100) NOT NULL COMMENT '微课名称', + user_id BIGINT NOT NULL COMMENT '负责人ID', + progress_status INT NOT NULL DEFAULT 0 COMMENT '当前任务进度状态: 0-脚本上传, 1-脚本确认, 2-视频拍摄, 3-后期制作, 4-任务完成', + script_upload_time BIGINT DEFAULT NULL COMMENT '脚本上传时间(时间戳)', + script_confirm_time BIGINT DEFAULT NULL COMMENT '脚本确认时间(时间戳)', + video_capture_time BIGINT DEFAULT NULL COMMENT '视频拍摄时间(时间戳)', + video_confirm_time BIGINT DEFAULT NULL COMMENT '视频确认时间(时间戳)', + finish_time BIGINT DEFAULT NULL COMMENT '任务完成时间(时间戳)', + advise TEXT DEFAULT NULL COMMENT '任务建议或备注', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)', + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + INDEX idx_lesson_tasks_user_id (user_id), + INDEX idx_lesson_tasks_progress_status (progress_status) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='课程任务表'; +``` + +### 字段说明 +- `id`: 任务ID,自增主键 +- `course_name`: 课程名称,不可为空 +- `micro_lesson_name`: 微课名称,不可为空 +- `user_id`: 负责人ID,外键关联users表 +- `progress_status`: 任务进度状态: + - 0: 脚本上传 + - 1: 脚本确认 + - 2: 视频拍摄 + - 3: 后期制作 + - 4: 任务完成 +- `script_upload_time`: 脚本上传时间,毫秒级时间戳 +- `script_confirm_time`: 脚本确认时间,毫秒级时间戳 +- `video_capture_time`: 视频拍摄时间,毫秒级时间戳 +- `video_confirm_time`: 视频确认时间,毫秒级时间戳 +- `finish_time`: 任务完成时间,毫秒级时间戳 +- `advise`: 任务相关的建议或备注,文本字段 +- `created_at`: 创建时间,毫秒级时间戳 +- `updated_at`: 更新时间,毫秒级时间戳 + +### 索引 +- 主键索引:`id` +- 外键索引:`idx_lesson_tasks_user_id (user_id)` +- 普通索引:`idx_lesson_tasks_progress_status (progress_status)` + +## 数据库关系 + +1. `users.department_id` -> `departments.id` + - 一个部门可以有多个用户 + - 一个用户只能属于一个部门 + - 使用RESTRICT约束,防止删除仍有用户的部门 + +2. `lesson_tasks.user_id` -> `users.id` + - 一个用户可以负责多个课程任务 + - 一个课程任务只能有一个负责人 + - 使用CASCADE约束,删除用户时自动删除其负责的课程任务 + +## 注意事项 + +1. 所有时间戳字段使用BIGINT类型,存储毫秒级时间戳 +2. 字符编码统一使用utf8mb4,支持完整的Unicode字符集 +3. 所有表都使用InnoDB引擎,支持事务和外键 +4. 关键字段都建立了适当的索引以提高查询性能 +5. 用户密码在存储前需要进行加密处理 +6. 删除用户时会自动删除其关联的课程任务,但不会影响部门数据 diff --git a/设计需求.md b/设计需求.md new file mode 100644 index 0000000..437a1a4 --- /dev/null +++ b/设计需求.md @@ -0,0 +1,313 @@ +``` +CREATE TABLE departments +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL COMMENT '部门名称', + description TEXT DEFAULT NULL COMMENT '部门描述', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT ='部门表'; +CREATE TABLE users +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱', + password VARCHAR(100) NOT NULL COMMENT '密码', + department_id BIGINT NOT NULL COMMENT '所属部门', + roles INT NOT NULL COMMENT '角色: 1-教师, 2-普通管理员, 3-沟通联络人, 4-系统管理员', + jobs INT NOT NULL COMMENT '岗位: 1-课程制作教师, 2-课程购买方项目负责人, 3-课程制作方沟通联络人, 4-系统制作方项目负责人', + avatar VARCHAR(255) DEFAULT NULL COMMENT '头像', + creator_id BIGINT NOT NULL DEFAULT 1 COMMENT '创建用户的管理员ID', + status INT NOT NULL DEFAULT 1 COMMENT '用户状态: 1-正常, 2-禁用', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)', + FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE RESTRICT, + INDEX idx_users_department_id (department_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT ='用户表'; +CREATE TABLE lesson_tasks +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + course_name VARCHAR(100) NOT NULL COMMENT '课程名称', + micro_lesson_name VARCHAR(100) NOT NULL COMMENT '微课名称', + user_id BIGINT NOT NULL COMMENT '负责人ID', + progress_status INT NOT NULL DEFAULT 0 COMMENT '当前任务进度状态: 0-脚本上传, 1-脚本确认, 2-视频拍摄, 3-后期制作, 4-任务完成', + script_upload_time BIGINT DEFAULT NULL COMMENT '脚本上传时间(时间戳)', + script_confirm_time BIGINT DEFAULT NULL COMMENT '脚本确认时间(时间戳)', + video_capture_time BIGINT DEFAULT NULL COMMENT '视频拍摄时间(时间戳)', + video_confirm_time BIGINT DEFAULT NULL COMMENT '视频确认时间(时间戳)', + finish_time BIGINT DEFAULT NULL COMMENT '任务完成时间(时间戳)', + advise TEXT DEFAULT NULL COMMENT '任务建议或备注', + created_at BIGINT NOT NULL COMMENT '创建时间(时间戳)', + updated_at BIGINT NOT NULL COMMENT '更新时间(时间戳)', + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + INDEX idx_lesson_tasks_user_id (user_id), + INDEX idx_lesson_tasks_progress_status (progress_status) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT ='课程任务表'; + +INSERT INTO departments (name, description, created_at, updated_at) +VALUES ('重庆眨生花科技有限公司', '重庆眨生花科技有限公司', NOW(), NOW()), + ('重庆电子科技职业大学', '重庆电子科技职业大学', NOW(), NOW()); +``` + +我修改了 我的表结构 + +接下来我将给你描述我的接口需求 + +**请求失败:** + +返回错误状态码以:`10XX`开头。 + +错误代码`code`返回为 100XX。`message`为错误描述。 + +```json +{ + "code": 10005, + "message": "邮箱已存在" +} +``` + +**请求成功:** + +比失败请求会增加`data`字段。或直接返回最终结果。 + +1、登陆接口 + +## 用户管理 + +### 1.登录 + +接口描述: 使用密码登录 + +接口地址:http://127.0.0.1:8000/api/login + +请求方式:POST + +接口参数: + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| :------- | :----- | :------- | :----- | :--- | :---------------------------- | +| email | 整型 | 是 | | | 用户邮箱 | +| password | 字符串 | 是 | | | 密码 | +| remember | 布尔型 | 是 | false | | 会话 token 有效期,记住则更长 | + +**返回结果** + +成功示例: + +| 参数名字 | 类型 | 说明 | +| -------- | ------ | ---- | +| token | 字符串 | | + +```json +{ + "token": "" +} +``` + +失败示例: + +```json +{ + "code": 10005, + "message": "登录失败!请检查邮箱或者密码" +} +``` + +### 2.添加用户 + +接口描述: 手动添加用户 + +接口地址:http://127.0.0.1:8000/api/users + +请求方式:POST + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| ------------- | ------ | -------- | ------ | ---- | ------ | +| username | 字符串 | 是 | | | 用户名 | +| email | 字符串 | 是 | | | 邮箱 | +| password | 字符串 | 是 | | | 密码 | +| roles | 整型 | 是 | | | 角色 | +| department_id | 整型 | 是 | | | 部门 | +| jobs | 整型 | 是 | | | 岗位 | + +```json +{ + "username": "test2", + "email": "root2@cyqsd.cn", + "password": "Abc123456", + "roles": 4, + "jobs": 4, + "department_id": 1 +} +``` + +**返回结果** + +| 参数名字 | 类型 | 说明 | +| -------- | ------ | ------------------ | +| code | 整型 | 提示码 | +| message | 字符串 | 是否成功的提示信息 | +| data | 字符串 | 数据 ID | + +```json +{ + "code": 10000, + "message": "操作成功", + "data": "3" +} +``` + +### 3.查看用户列表 + +接口描述: 查看用户列表 + +接口地址:http://127.0.0.1:8000/api/users?page=1&limit=10 + +请求方式:GET + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| -------- | ---- | -------- | ------ | ---- | ---- | +| page | 整型 | 是 | 1 | | | +| limit | 整型 | 是 | 10 | | | + +### 4、查看单个用户 + +接口描述: 查看用户列表 + +接口地址:http://127.0.0.1:8000/api/users + +请求方式:GET + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| -------- | ---- | -------- | ------ | ---- | ------- | +| id | 整型 | 是 | | | 用户 id | + +### 5、删除用户 + +通过改变用户状态的方式 来将这个账户停止使用 + +将用户表的用户状态改变 + +### 6、更新账户 + +填了什么字段 就修改什么字段 没填写的字段 还是保留之前的数据 + +## 课程管理 + +### 1.查询列表 + +接口地址:http://127.0.0.1:8000/api/lesson/task?page=1&limit=1 + +接口描述: 查询课程管理列表。 + +请求方式:GET + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| -------- | ---- | -------- | ------ | ----------------- | -------- | +| page | 整型 | 可选 | 1 | 最小:1 | 第几页 | +| perpage | 整型 | 可选 | 10 | 最小:1;最大:20 | 分页数量 | +| user_id | 整型 | 可选 | | | 用户 ID | + +**返回结果** + +| 参数名字 | 类型 | 说明 | +| ------------------- | ------ | -------- | +| course_name | 字符串 | 见下表 | +| m_lesson_name | 字符串 | | +| user_id | 整型 | | +| schedule_status | 整型 | | +| script_confirm_time | 整型 | | +| video_confirm_time | 整型 | | +| finish_time | 整型 | | +| script_upload_time | 整型 | | +| video_capture_time | 整型 | | +| script_file | 字符串 | | +| material_file | 字符串 | | +| capture_file | 字符串 | | +| advise | 字符串 | | +| created_at | 整型 | 创建时间 | + +### 2.新增数据 + +接口地址:http://127.0.0.1:8000/api/lesson/task + +接口描述: 新增一条课程记录 + +请求方式:POST + +Content-Type:application/json + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| ------------------- | ------ | -------- | --------- | ---- | ------------------------------ | +| course_name | 字符串 | 是 | 最大:25 | | 课程名 | +| m_lesson_name | 字符串 | 是 | 最大:25 | | 微课名 | +| user_id | 整型 | 是 | 最大:10 | | 用户 ID | +| schedule_status | 整型 | 否 | 最大:10 | | 当前进度 ID | +| script_confirm_time | 整型 | 否 | 最大:10 | | 脚本确认时间 | +| video_confirm_time | 整型 | 否 | 最大:10 | | 视频确认时间 | +| finish_time | 整型 | 否 | 最大:10 | | 任务完成时间 | +| script_upload_time | 整型 | 否 | 最大:10 | | 脚本上传时间 | +| video_capture_time | 整型 | 否 | 最大:10 | | 视频拍摄时间 | +| script_file | 字符串 | 否 | 最大:255 | | 脚本文件地址 | +| material_file | 字符串 | 否 | 最大:255 | | 素材文件地址 | +| capture_file | 字符串 | 否 | 最大:255 | | 拍摄结果文件地址 | +| advise | 字符串 | 否 | 最大:255 | | 沟通建议列表,自行构建保存格式 | + +**返回结果** + +| 参数名字 | 类型 | 说明 | +| -------- | ---- | ---- | +| | 整型 | ID | + +### [#](https://aigc-doc.cqrthny.com/pages/ppmp/#_3-修改数据)3.修改数据 + +接口地址:http://127.0.0.1:8000/api/lesson/task/2 + +接口描述: 修改一条课程记录 + +请求方式:PUT + +Content-Type:application/json + +**接口参数** + +其他需要修改的字段,按`新增数据`小节中的内容修改。 + +### [#](https://aigc-doc.cqrthny.com/pages/ppmp/#_4-删除数据)4.删除数据 + +接口地址:http://127.0.0.1:8000/api/lesson/task/1 + +接口描述: 删除一条课程记录 + +请求方式:DELETE + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| -------- | ---- | -------- | ------ | ---- | ------------------- | +| 无 | 整型 | 是 | 否 | | 待删除的课程记录 ID | + +### 5.查询单条数据 + +接口地址:http://127.0.0.1:8000/api/lesson/task/1 + +接口描述: 查询单条课程记录 + +请求方式:GET + +**接口参数** + +| 参数名字 | 类型 | 是否必须 | 默认值 | 其他 | 说明 | +| -------- | ---- | -------- | ------ | ---- | ---- | +| 无 | | | | | |