feat: mongo联查一版

This commit is contained in:
Guwan 2025-09-14 19:33:41 +08:00
commit 33c9563611
25 changed files with 2998 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
.gitignore vendored Normal file
View File

@ -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/

2
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@ -0,0 +1,2 @@
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip

295
mvnw vendored Normal file
View File

@ -0,0 +1,295 @@
#!/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.3
#
# 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:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -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
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
mvnw.cmd vendored Normal file
View File

@ -0,0 +1,189 @@
<# : 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.3
@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-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/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_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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"

71
pom.xml Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>baseMongo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mongo-simple</name>
<description>mongo-simple</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package org.example.mongosimple;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MongoSimpleApplication {
public static void main(String[] args) {
SpringApplication.run(MongoSimpleApplication.class, args);
}
}

View File

@ -0,0 +1,67 @@
package org.example.mongosimple.controller;
import lombok.RequiredArgsConstructor;
import org.example.mongosimple.entity.ShoppingResult;
import org.example.mongosimple.prepareEntity.MongoAggregationBuilder;
import org.example.mongosimple.prepareEntity.PageResult;
import org.example.mongosimple.prepareEntity.ShoppingSearchParams;
import org.example.mongosimple.service.ShoppingAggregationService;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MongoDB聚合查询框架演示控制器
* @author admin
*/
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
private final ShoppingAggregationService shoppingAggregationService;
@GetMapping("/facet-pagination")
public Map<String, Object> testFacetPagination(
@RequestParam(required = false) String phase,
@RequestParam(required = false) String user,
@RequestParam(required = false) String city,
@RequestParam(required = false) String category,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "5") int size) {
// 构建搜索参数
ShoppingSearchParams params =
new ShoppingSearchParams()
.setPhaseName(phase != null ? phase : "618")
.setUsernamePattern(user != null ? user : "test")
.setCity(city != null ? city : "dezhou")
.setCategoryName(category != null ? category : "电子产品")
.setMaxPrice(new BigDecimal(6000))
.pageNumber(page, size); // 使用新的pageNumber方法
// 使用facet优化分页查询
PageResult<ShoppingResult> pageResult =
shoppingAggregationService.searchWithCriteriaAndPagination(params);
Map<String, Object> response = new HashMap<>();
// 结果数据
response.put("results", pageResult.getContent());
return response;
}
}

View File

@ -0,0 +1,14 @@
package org.example.mongosimple.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ShoppingResult {
private String username;
private String email;
private String productName;
private BigDecimal price;
private String categoryName;
}

View File

@ -0,0 +1,97 @@
package org.example.mongosimple.initializer;
import lombok.RequiredArgsConstructor;
import org.example.mongosimple.mongoObject.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final MongoTemplate mongoTemplate;
@Override
public void run(String... args) throws Exception {
// 检查是否已经初始化过数据
/* if (userRepository.count() > 0) {
System.out.println("数据已存在,跳过初始化");
return;
}*/
// System.out.println("开始初始化测试数据...");
// 初始化用户数据
// initUsers();
// System.out.println("测试数据初始化完成!");
}
private void initUsers() {
// System.out.println("初始化用户数据...");
Address address = new Address();
address.setCity("jinan");
Address address1 = new Address();
address1.setCity("dezhou");
mongoTemplate.save(address);
mongoTemplate.save(address1);
// 创建用户1
User user1 = new User("test1", "test1@example.com", "123456", "68c4f94d7757c98d8116c6a4");
User user2 = new User("test2", "test2@example.com", "123456", "68c4f94d7757c98d8116c6a4");
User user3 = new User("test3", "test3@example.com", "123456", "68c4f94d7757c98d8116c6a5");
List<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
users.add(user3);
// mongoTemplate.insert(user1);
// mongoTemplate.insertAll(users);
ShoppingPhase shoppingPhase = new ShoppingPhase();
shoppingPhase.setName("双十一");
ShoppingPhase shoppingPhase1 = new ShoppingPhase();
shoppingPhase1.setName("618");
// mongoTemplate.save(shoppingPhase);
// mongoTemplate.save(shoppingPhase1);
Category category = new Category();
category.setName("电子产品");
Category category1 = new Category();
category1.setName("家具");
// mongoTemplate.save(category);
// mongoTemplate.save(category1);
Product product = new Product("iPhone 17", new BigDecimal("5999.00"), "68c4fbe9b9de8975d97681b9");
Product product1 = new Product("iPhone 17 pro", new BigDecimal("8999.00"), "68c4fbe9b9de8975d97681b9");
Product product2 = new Product("mac mini", new BigDecimal("4299.00"), "68c4fbe9b9de8975d97681ba");
mongoTemplate.save(product);
mongoTemplate.save(product1);
mongoTemplate.save(product2);
// 来自的dezhou的用户 在618期间买的 价格位于 5000 - 6000 的电子产品z
ShoppingRecord shoppingRecord = new ShoppingRecord("68c4fabe669d40228340eaa9", "68c4fcd9f53fc172ab23f4ac", "68c4fb3a370a3428ec124244");
ShoppingRecord shoppingRecord1 = new ShoppingRecord("68c4fabe669d40228340eaab", "68c4fcd9f53fc172ab23f4ac", "68c4fb3a370a3428ec124244");
ShoppingRecord shoppingRecord2 = new ShoppingRecord("68c4fabe669d40228340eaa9", "68c4fcd9f53fc172ab23f4ad", "68c4fb3a370a3428ec124244");
ShoppingRecord shoppingRecord3 = new ShoppingRecord("68c4fabe669d40228340eaa9", "68c4fcd9f53fc172ab23f4ac", "68c4fb9775d63063d25ed01d");
ShoppingRecord shoppingRecord4 = new ShoppingRecord("68c4fabe669d40228340eaa9", "68c4fcd9f53fc172ab23f4ae", "68c4fb3a370a3428ec124244");
/*mongoTemplate.save(shoppingRecord);
mongoTemplate.save(shoppingRecord1);
mongoTemplate.save(shoppingRecord2);
mongoTemplate.save(shoppingRecord3);
mongoTemplate.save(shoppingRecord4);*/
}
}

View File

@ -0,0 +1,17 @@
package org.example.mongosimple.mongoObject;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author admin
*/
@Data
@Document(collection = "address")
public class Address {
@Id
private String id;
private String city;
}

View File

@ -0,0 +1,21 @@
package org.example.mongosimple.mongoObject;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "categories")
public class Category {
@Id
private String id;
private String name;
private String description;
}

View File

@ -0,0 +1,33 @@
package org.example.mongosimple.mongoObject;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Document(collection = "product")
public class Product {
@Id
private String id;
private String name;
private String description;
private BigDecimal price;
private String categoryId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Product(String name, BigDecimal price, String categoryId) {
this.name = name;
this.price = price;
this.categoryId = categoryId;
}
}

View File

@ -0,0 +1,16 @@
package org.example.mongosimple.mongoObject;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* @author admin
*/
@Data
@Document(collection = "shopping_phase")
public class ShoppingPhase {
@Id
private String id;
private String name;
}

View File

@ -0,0 +1,22 @@
package org.example.mongosimple.mongoObject;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "shopping_record")
public class ShoppingRecord {
@Id
private String id;
private String userId;
private String productId;
private String shoppingPhaseId;
public ShoppingRecord(String userId, String productId, String shoppingPhaseId) {
this.userId = userId;
this.productId = productId;
this.shoppingPhaseId = shoppingPhaseId;
}
}

View File

@ -0,0 +1,43 @@
package org.example.mongosimple.mongoObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
/**
* @author admin
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(collection = "users")
public class User {
@Id
private String id;
private String username;
private String email;
private String phone;
private String addressId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public User(String username, String email, String phone, String addressId) {
this.username = username;
this.email = email;
this.phone = phone;
this.addressId = addressId;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,275 @@
package org.example.mongosimple.prepareEntity;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 动态Criteria构建器 - 根据字段是否为空动态拼接查询条件
*
* @author admin
*/
public class CriteriaBuilder {
private final List<Criteria> criteriaList;
public CriteriaBuilder() {
this.criteriaList = new ArrayList<>();
}
/**
* 创建新的Criteria构建器
*/
public static CriteriaBuilder create() {
return new CriteriaBuilder();
}
/**
* 字符串不为空时添加等值条件
*/
public CriteriaBuilder whenNotEmpty(String value, String field) {
if (StringUtils.hasText(value)) {
criteriaList.add(Criteria.where(field).is(value));
}
return this;
}
/**
* 对象不为空时添加等值条件
*/
public CriteriaBuilder whenNotNull(Object value, String field) {
if (value != null) {
criteriaList.add(Criteria.where(field).is(value));
}
return this;
}
/**
* 字符串不为空时添加模糊匹配条件
*/
public CriteriaBuilder whenNotEmptyLike(String value, String field) {
if (StringUtils.hasText(value)) {
criteriaList.add(Criteria.where(field).regex(".*" + value + ".*", "i"));
}
return this;
}
/**
* 字符串不为空时添加正则表达式条件
*/
public CriteriaBuilder whenNotEmptyRegex(String value, String field, String pattern) {
if (StringUtils.hasText(value)) {
criteriaList.add(Criteria.where(field).regex(pattern.replace("{value}", value), "i"));
}
return this;
}
/**
* 数值范围条件最小值不为空时
*/
public CriteriaBuilder whenMinValue(Number minValue, String field) {
if (minValue != null) {
criteriaList.add(Criteria.where(field).gte(minValue));
}
return this;
}
/**
* 数值范围条件最大值不为空时
*/
public CriteriaBuilder whenMaxValue(Number maxValue, String field) {
if (maxValue != null) {
criteriaList.add(Criteria.where(field).lte(maxValue));
}
return this;
}
/**
* 数值范围条件最小值和最大值
*/
public CriteriaBuilder whenRange(Number minValue, Number maxValue, String field) {
if (minValue != null && maxValue != null) {
criteriaList.add(Criteria.where(field).gte(minValue).lte(maxValue));
} else if (minValue != null) {
criteriaList.add(Criteria.where(field).gte(minValue));
} else if (maxValue != null) {
criteriaList.add(Criteria.where(field).lte(maxValue));
}
return this;
}
/**
* 日期范围条件
*/
public CriteriaBuilder whenDateRange(LocalDateTime startDate, LocalDateTime endDate, String field) {
if (startDate != null && endDate != null) {
criteriaList.add(Criteria.where(field).gte(startDate).lte(endDate));
} else if (startDate != null) {
criteriaList.add(Criteria.where(field).gte(startDate));
} else if (endDate != null) {
criteriaList.add(Criteria.where(field).lte(endDate));
}
return this;
}
/**
* 集合不为空时添加in条件
*/
public CriteriaBuilder whenNotEmptyIn(Collection<?> values, String field) {
if (values != null && !values.isEmpty()) {
criteriaList.add(Criteria.where(field).in(values));
}
return this;
}
/**
* 数组不为空时添加in条件
*/
public CriteriaBuilder whenNotEmptyIn(Object[] values, String field) {
if (values != null && values.length > 0) {
criteriaList.add(Criteria.where(field).in(values));
}
return this;
}
/**
* 布尔值不为空时添加条件
*/
public CriteriaBuilder whenNotNull(Boolean value, String field) {
if (value != null) {
criteriaList.add(Criteria.where(field).is(value));
}
return this;
}
/**
* 自定义条件判断
*/
public CriteriaBuilder when(boolean condition, Function<Criteria, Criteria> criteriaFunction, String field) {
if (condition) {
criteriaList.add(criteriaFunction.apply(Criteria.where(field)));
}
return this;
}
/**
* 自定义条件判断带值
*/
public <T> CriteriaBuilder when(T value, Predicate<T> condition, Function<T, Criteria> criteriaFunction) {
if (value != null && condition.test(value)) {
criteriaList.add(criteriaFunction.apply(value));
}
return this;
}
/**
* 字段存在性检查
*/
public CriteriaBuilder whenFieldExists(Boolean checkExists, String field) {
if (checkExists != null) {
if (checkExists) {
criteriaList.add(Criteria.where(field).exists(true));
} else {
criteriaList.add(Criteria.where(field).exists(false));
}
}
return this;
}
/**
* 字段不为null检查
*/
public CriteriaBuilder whenFieldNotNull(Boolean checkNotNull, String field) {
if (checkNotNull != null) {
if (checkNotNull) {
criteriaList.add(Criteria.where(field).ne(null));
} else {
criteriaList.add(Criteria.where(field).is(null));
}
}
return this;
}
/**
* 添加自定义Criteria
*/
public CriteriaBuilder addCriteria(Criteria criteria) {
if (criteria != null) {
criteriaList.add(criteria);
}
return this;
}
/**
* 条件性添加自定义Criteria
*/
public CriteriaBuilder addCriteriaIf(boolean condition, Criteria criteria) {
if (condition && criteria != null) {
criteriaList.add(criteria);
}
return this;
}
/**
* 构建最终的CriteriaAND逻辑
*/
public Criteria buildAnd() {
if (criteriaList.isEmpty()) {
return new Criteria();
}
if (criteriaList.size() == 1) {
return criteriaList.get(0);
}
return new Criteria().andOperator(criteriaList.toArray(new Criteria[0]));
}
/**
* 构建最终的CriteriaOR逻辑
*/
public Criteria buildOr() {
if (criteriaList.isEmpty()) {
return new Criteria();
}
if (criteriaList.size() == 1) {
return criteriaList.get(0);
}
return new Criteria().orOperator(criteriaList.toArray(new Criteria[0]));
}
/**
* 获取条件数量
*/
public int size() {
return criteriaList.size();
}
/**
* 是否有条件
*/
public boolean hasConditions() {
return !criteriaList.isEmpty();
}
/**
* 清空所有条件
*/
public CriteriaBuilder clear() {
criteriaList.clear();
return this;
}
/**
* 获取所有条件列表
*/
public List<Criteria> getCriteriaList() {
return new ArrayList<>(criteriaList);
}
}

View File

@ -0,0 +1,155 @@
package org.example.mongosimple.prepareEntity;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* 字段映射类 - 手动配置投影和排序字段映射
* @author admin
*/
public class FieldMapping {
// 投影字段映射原始字段路径 -> 投影后字段名
private final Map<String, String> projectionMapping = new HashMap<>();
// 排序字段映射用户输入字段 -> 实际排序字段
private final Map<String, String> sortMapping = new HashMap<>();
// 默认包含的字段
private String[] defaultIncludeFields = {"_id", "createdAt"};
/**
* 空构造函数 - 需要手动添加字段映射
*/
public FieldMapping() {
// 空构造需要手动添加映射
}
/**
* 设置默认包含字段
*/
public FieldMapping setDefaultIncludeFields(String... fields) {
this.defaultIncludeFields = fields;
return this;
}
/**
* 添加投影字段映射
* @param originalField 原始字段路径 "user.username"
* @param projectedField 投影后字段名 "username"
*/
public FieldMapping addProjectionMapping(String originalField, String projectedField) {
projectionMapping.put(originalField, projectedField);
return this;
}
/**
* 批量添加投影字段映射
*/
public FieldMapping addProjectionMappings(Map<String, String> mappings) {
projectionMapping.putAll(mappings);
return this;
}
/**
* 添加排序字段映射
* @param sortField 排序字段可以是原始字段或投影后字段
* @param actualField 实际排序使用的字段名
*/
public FieldMapping addSortMapping(String sortField, String actualField) {
sortMapping.put(sortField, actualField);
return this;
}
/**
* 批量添加排序字段映射
*/
public FieldMapping addSortMappings(Map<String, String> mappings) {
sortMapping.putAll(mappings);
return this;
}
/**
* 创建投影操作函数 - 包含所有已配置的字段映射
*/
public Function<ProjectionOperation, ProjectionOperation> createProjectionFunction() {
return projection -> {
// 先包含默认字段
ProjectionOperation result = projection.andInclude(defaultIncludeFields);
// 添加所有配置的字段映射
for (Map.Entry<String, String> entry : projectionMapping.entrySet()) {
result = result.and(entry.getKey()).as(entry.getValue());
}
return result;
};
}
/**
* 创建选择性投影操作函数 - 只包含指定的字段
*/
public Function<ProjectionOperation, ProjectionOperation> createSelectiveProjectionFunction(String... selectedFields) {
return projection -> {
// 先包含默认字段
ProjectionOperation result = projection.andInclude(defaultIncludeFields);
// 只添加选中的字段
for (String field : selectedFields) {
if (projectionMapping.containsKey(field)) {
result = result.and(field).as(projectionMapping.get(field));
}
}
return result;
};
}
/**
* 获取兼容的排序字段名
* @param originalSortField 原始排序字段
* @param defaultField 默认排序字段如果找不到映射
*/
public String getCompatibleSortField(String originalSortField, String defaultField) {
if (originalSortField == null) {
return defaultField != null ? defaultField : "createdAt";
}
return sortMapping.getOrDefault(originalSortField, defaultField != null ? defaultField : originalSortField);
}
/**
* 检查字段是否支持排序
*/
public boolean isSortFieldSupported(String sortField) {
return sortMapping.containsKey(sortField);
}
/**
* 获取投影后的字段名
*/
public String getProjectedFieldName(String originalField) {
return projectionMapping.getOrDefault(originalField, originalField);
}
/**
* 检查是否有配置的映射
*/
public boolean isEmpty() {
return projectionMapping.isEmpty() && sortMapping.isEmpty();
}
/**
* 获取映射统计信息
*/
public String getStats() {
return String.format("投影映射: %d个, 排序映射: %d个",
projectionMapping.size(), sortMapping.size());
}
public String getSortField(String sortField) {
return sortMapping.getOrDefault(sortField, sortField);
}
}

View File

@ -0,0 +1,657 @@
package org.example.mongosimple.prepareEntity;
import com.mongodb.BasicDBObject;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import java.util.*;
import java.util.function.Function;
/**
* MongoDB聚合查询构建器 - 提供流式API构建复杂聚合查询
*
* 特性
* - 链式调用代码简洁
* - 自动处理ObjectId转换
* - 支持动态条件构建
* - 内置常用查询模式
*
* @author admin
*/
public class MongoAggregationBuilder {
private final MongoTemplate mongoTemplate;
private final List<AggregationOperation> operations;
private final String sourceCollection;
public MongoAggregationBuilder(MongoTemplate mongoTemplate, String sourceCollection) {
this.mongoTemplate = mongoTemplate;
this.sourceCollection = sourceCollection;
this.operations = new ArrayList<>();
}
/**
* 创建聚合构建器
*/
public static MongoAggregationBuilder from(MongoTemplate mongoTemplate, String collection) {
return new MongoAggregationBuilder(mongoTemplate, collection);
}
/**
* 添加匹配条件
*/
public MongoAggregationBuilder match(Criteria criteria) {
operations.add(Aggregation.match(criteria));
return this;
}
/**
* 添加匹配条件 - 便捷方法
*/
public MongoAggregationBuilder match(String field, Object value) {
return match(Criteria.where(field).is(value));
}
/**
* 添加范围匹配
*/
public MongoAggregationBuilder matchRange(String field, Object min, Object max) {
return match(Criteria.where(field).gte(min).lte(max));
}
/**
* 添加正则匹配
*/
public MongoAggregationBuilder matchRegex(String field, String regex) {
return match(Criteria.where(field).regex(regex, "i"));
}
/**
* 条件性添加Criteria匹配条件
*/
public MongoAggregationBuilder matchIf(boolean condition, Criteria criteria) {
if (condition && criteria != null) {
operations.add(Aggregation.match(criteria));
}
return this;
}
/**
* 使用CriteriaBuilder添加动态匹配条件
*/
public MongoAggregationBuilder matchDynamic(Function<CriteriaBuilder, CriteriaBuilder> builderFunction) {
CriteriaBuilder builder = builderFunction.apply(CriteriaBuilder.create());
if (builder.hasConditions()) {
operations.add(Aggregation.match(builder.buildAnd()));
}
return this;
}
/**
* 关联查询 - 自动处理ObjectId转换
*/
public MongoAggregationBuilder lookup(String fromCollection, String localField, String foreignField, String as) {
// 自动添加ObjectId转换
String convertedField = localField + "Obj";
operations.add(AddFieldsOperation.builder()
.addField(convertedField)
.withValue(ConvertOperators.ToObjectId.toObjectId("$" + localField))
.build());
// 执行lookup
operations.add(LookupOperation.newLookup()
.from(fromCollection)
.localField(convertedField)
.foreignField(foreignField)
.as(as));
return this;
}
/**
* 关联查询 - 不进行ObjectId转换用于已经是ObjectId的字段
*/
public MongoAggregationBuilder lookupDirect(String fromCollection, String localField, String foreignField, String as) {
operations.add(LookupOperation.newLookup()
.from(fromCollection)
.localField(localField)
.foreignField(foreignField)
.as(as));
return this;
}
/**
* 展开数组字段
*/
public MongoAggregationBuilder unwind(String field) {
operations.add(Aggregation.unwind(field));
return this;
}
/**
* 关联并展开 - 常用组合操作
*/
public MongoAggregationBuilder lookupAndUnwind(String fromCollection, String localField, String foreignField, String as) {
return lookup(fromCollection, localField, foreignField, as).unwind(as);
}
/**
* 投影字段
*/
public MongoAggregationBuilder project(String... fields) {
ProjectionOperation projection = Aggregation.project(fields);
operations.add(projection);
return this;
}
/**
* 自定义投影
*/
public MongoAggregationBuilder project(Function<ProjectionOperation, ProjectionOperation> projectionBuilder) {
ProjectionOperation projection = projectionBuilder.apply(Aggregation.project());
operations.add(projection);
return this;
}
/**
* 分组聚合
*/
public MongoAggregationBuilder group(String... fields) {
operations.add(Aggregation.group(fields));
return this;
}
/**
* 自定义分组
*/
public MongoAggregationBuilder group(Function<GroupOperation, GroupOperation> groupBuilder) {
GroupOperation group = groupBuilder.apply(Aggregation.group());
operations.add(group);
return this;
}
/**
* 排序
*/
public MongoAggregationBuilder sort(String field, boolean ascending) {
operations.add(Aggregation.sort(ascending ?
org.springframework.data.domain.Sort.Direction.ASC :
org.springframework.data.domain.Sort.Direction.DESC, field));
return this;
}
/**
* 限制结果数量
*/
public MongoAggregationBuilder limit(int count) {
operations.add(Aggregation.limit(count));
return this;
}
/**
* 跳过指定数量
*/
public MongoAggregationBuilder skip(int count) {
operations.add(Aggregation.skip(count));
return this;
}
/**
* 分页
*/
public MongoAggregationBuilder page(int pageNumber, int pageSize) {
return skip(pageNumber * pageSize).limit(pageSize);
}
/**
* 添加自定义操作
*/
public MongoAggregationBuilder addOperation(AggregationOperation operation) {
operations.add(operation);
return this;
}
/**
* 添加多个自定义聚合操作
*/
public MongoAggregationBuilder addOperations(List<AggregationOperation> operationList) {
operations.addAll(operationList);
return this;
}
/**
* 执行聚合查询并返回指定类型结果
*/
public <T> List<T> execute(Class<T> resultClass) {
Aggregation aggregation = Aggregation.newAggregation(operations);
return mongoTemplate.aggregate(aggregation, sourceCollection, resultClass).getMappedResults();
}
/**
* 执行聚合查询并返回Map结果
*/
public List<Map> executeAsMap() {
return execute(Map.class);
}
/**
* 获取构建的聚合操作列表用于调试
*/
public List<AggregationOperation> getOperations() {
return new ArrayList<>(operations);
}
/**
* 执行分页查询 - 使用facet优化一次查询获取总数和分页数据
* Spring Boot 2/3兼容版本
*/
public <T> PageResult<T> executeWithPagination(Class<T> resultClass, int page, int size) {
try {
// 尝试使用facet分页Spring Boot 3风格
return executeWithFacetPagination(resultClass, page, size);
} catch (Exception e) {
System.err.println("Facet分页失败使用传统分页: " + e.getMessage());
// 降级到传统分页Spring Boot 2兼容
return executeWithTraditionalPagination(resultClass, page, size);
}
}
/**
* Facet分页实现
*/
private <T> PageResult<T> executeWithFacetPagination(Class<T> resultClass, int page, int size) {
// 创建facet操作
FacetOperation facetOperation = Aggregation.facet(
// 计算总数的管道
Aggregation.count().as("total")
).as("totalCount").and(
// 分页数据的管道
Aggregation.skip((long) page * size),
Aggregation.limit(size)
).as("pagedResults");
// 添加facet操作到现有管道
List<AggregationOperation> facetOperations = new ArrayList<>(operations);
facetOperations.add(facetOperation);
Aggregation aggregation = Aggregation.newAggregation(facetOperations);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, sourceCollection, Map.class);
return parseFacetResults(results, resultClass, page, size);
}
/**
* 传统分页实现 - Spring Boot 2兼容
*/
private <T> PageResult<T> executeWithTraditionalPagination(Class<T> resultClass, int page, int size) {
// 1. 获取总数
List<AggregationOperation> countOperations = new ArrayList<>(operations);
countOperations.add(Aggregation.count().as("total"));
Aggregation countAggregation = Aggregation.newAggregation(countOperations);
AggregationResults<Map> countResults = mongoTemplate.aggregate(countAggregation, sourceCollection, Map.class);
long total = 0;
if (!countResults.getMappedResults().isEmpty()) {
Map<String, Object> countResult = countResults.getMappedResults().get(0);
Object totalObj = countResult.get("total");
if (totalObj instanceof Number) {
total = ((Number) totalObj).longValue();
}
}
// 2. 获取分页数据
List<AggregationOperation> dataOperations = new ArrayList<>(operations);
dataOperations.add(Aggregation.skip((long) page * size));
dataOperations.add(Aggregation.limit(size));
Aggregation dataAggregation = Aggregation.newAggregation(dataOperations);
AggregationResults<T> dataResults = mongoTemplate.aggregate(dataAggregation, sourceCollection, resultClass);
return new PageResult<>(dataResults.getMappedResults(), total, page, size);
}
/**
* 执行分页查询并返回Map结果
*/
public PageResult<Map> executeWithPaginationAsMap(int page, int size) {
return executeWithPagination(Map.class, page, size);
}
/**
* 解析facet查询结果
*/
@SuppressWarnings("unchecked")
private <T> PageResult<T> parseFacetResults(AggregationResults<Map> results, Class<T> resultClass, int page, int size) {
if (results.getMappedResults().isEmpty()) {
return new PageResult<>(Collections.emptyList(), 0, page, size);
}
Map<String, Object> facetResult = results.getMappedResults().get(0);
// 解析总数
long total = 0;
List<Map<String, Object>> totalCountList = (List<Map<String, Object>>) facetResult.get("totalCount");
if (totalCountList != null && !totalCountList.isEmpty()) {
Object totalObj = totalCountList.get(0).get("total");
if (totalObj instanceof Number) {
total = ((Number) totalObj).longValue();
}
}
// 解析分页数据
List<T> content = new ArrayList<>();
List<Map<String, Object>> pagedResultsList = (List<Map<String, Object>>) facetResult.get("pagedResults");
if (pagedResultsList != null) {
if (resultClass == Map.class) {
content = (List<T>) pagedResultsList;
} else {
// 转换为目标类型
for (Map<String, Object> item : pagedResultsList) {
T convertedItem = mongoTemplate.getConverter().read(resultClass, new BasicDBObject(item));
content.add(convertedItem);
}
}
}
return new PageResult<>(content, total, page, size);
}
/**
* 聚合统计查询 - 使用facet同时执行多种统计
*/
public Map<String, Object> executeWithStatistics(String... statisticTypes) {
// 创建facet操作正确的API使用方式
FacetOperation facetOperation = null;
if (statisticTypes.length > 0) {
// 开始构建facet
String firstType = statisticTypes[0].toLowerCase();
switch (firstType) {
case "count":
facetOperation = Aggregation.facet(Aggregation.count().as("total")).as("count");
break;
case "sum":
facetOperation = Aggregation.facet(
Aggregation.group().sum("product.price").as("totalAmount")
).as("sum");
break;
case "avg":
facetOperation = Aggregation.facet(
Aggregation.group().avg("product.price").as("avgPrice")
).as("avg");
break;
case "minmax":
facetOperation = Aggregation.facet(
Aggregation.group()
.min("product.price").as("minPrice")
.max("product.price").as("maxPrice")
).as("minmax");
break;
case "groupby":
facetOperation = Aggregation.facet(
Aggregation.group("category.name")
.count().as("count")
.sum("product.price").as("totalAmount"),
Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count")
).as("groupby");
break;
default:
facetOperation = Aggregation.facet(Aggregation.count().as("total")).as("count");
}
// 添加其余的统计类型
for (int i = 1; i < statisticTypes.length; i++) {
String type = statisticTypes[i].toLowerCase();
switch (type) {
case "count":
facetOperation = facetOperation.and(Aggregation.count().as("total")).as("count");
break;
case "sum":
facetOperation = facetOperation.and(
Aggregation.group().sum("product.price").as("totalAmount")
).as("sum");
break;
case "avg":
facetOperation = facetOperation.and(
Aggregation.group().avg("product.price").as("avgPrice")
).as("avg");
break;
case "minmax":
facetOperation = facetOperation.and(
Aggregation.group()
.min("product.price").as("minPrice")
.max("product.price").as("maxPrice")
).as("minmax");
break;
case "groupby":
facetOperation = facetOperation.and(
Aggregation.group("category.name")
.count().as("count")
.sum("product.price").as("totalAmount"),
Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count")
).as("groupby");
break;
}
}
} else {
// 默认统计
facetOperation = Aggregation.facet(Aggregation.count().as("total")).as("count");
}
List<AggregationOperation> statOperations = new ArrayList<>(operations);
statOperations.add(facetOperation);
Aggregation aggregation = Aggregation.newAggregation(statOperations);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, sourceCollection, Map.class);
return results.getMappedResults().isEmpty() ?
Collections.emptyMap() : results.getMappedResults().get(0);
}
/**
* 简化的统计查询 - 分别执行不同的统计操作
* Spring Boot 2/3兼容版本
*/
public Map<String, Object> executeWithSimpleStatistics() {
Map<String, Object> statistics = new HashMap<>();
try {
// 1. 总数统计
List<AggregationOperation> countOps = new ArrayList<>(operations);
countOps.add(Aggregation.count().as("total"));
Aggregation countAgg = Aggregation.newAggregation(countOps);
AggregationResults<Map> countResults = mongoTemplate.aggregate(countAgg, sourceCollection, Map.class);
if (!countResults.getMappedResults().isEmpty()) {
statistics.put("count", countResults.getMappedResults().get(0));
}
// 2. 价格统计 - 使用兼容的字段引用
try {
List<AggregationOperation> priceOps = new ArrayList<>(operations);
priceOps.add(Aggregation.group()
.sum("product.price").as("totalAmount")
.avg("product.price").as("avgPrice")
.min("product.price").as("minPrice")
.max("product.price").as("maxPrice"));
Aggregation priceAgg = Aggregation.newAggregation(priceOps);
AggregationResults<Map> priceResults = mongoTemplate.aggregate(priceAgg, sourceCollection, Map.class);
if (!priceResults.getMappedResults().isEmpty()) {
statistics.put("priceStats", priceResults.getMappedResults().get(0));
}
} catch (Exception e) {
System.err.println("价格统计失败,尝试备用字段: " + e.getMessage());
// 备用方案使用不同的字段引用方式
try {
List<AggregationOperation> priceOps = new ArrayList<>(operations);
priceOps.add(Aggregation.group()
.sum("price").as("totalAmount")
.avg("price").as("avgPrice")
.min("price").as("minPrice")
.max("price").as("maxPrice"));
Aggregation priceAgg = Aggregation.newAggregation(priceOps);
AggregationResults<Map> priceResults = mongoTemplate.aggregate(priceAgg, sourceCollection, Map.class);
if (!priceResults.getMappedResults().isEmpty()) {
statistics.put("priceStats", priceResults.getMappedResults().get(0));
}
} catch (Exception e2) {
statistics.put("priceStatsError", "价格统计失败: " + e2.getMessage());
}
}
// 3. 分类统计 - 使用兼容的字段引用
try {
List<AggregationOperation> categoryOps = new ArrayList<>(operations);
categoryOps.add(Aggregation.group("category.name")
.count().as("count")
.sum("product.price").as("totalAmount"));
categoryOps.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count"));
categoryOps.add(Aggregation.limit(10));
Aggregation categoryAgg = Aggregation.newAggregation(categoryOps);
AggregationResults<Map> categoryResults = mongoTemplate.aggregate(categoryAgg, sourceCollection, Map.class);
statistics.put("categoryStats", categoryResults.getMappedResults());
} catch (Exception e) {
System.err.println("分类统计失败: " + e.getMessage());
statistics.put("categoryStatsError", "分类统计失败: " + e.getMessage());
}
} catch (Exception e) {
System.err.println("统计查询执行失败: " + e.getMessage());
statistics.put("error", e.getMessage());
}
return statistics;
}
/**
* Spring Boot 2兼容的构建方法
* 自动添加字段展平投影
*/
public static MongoAggregationBuilder fromWithCompatibility(MongoTemplate mongoTemplate, String sourceCollection) {
MongoAggregationBuilder builder = new MongoAggregationBuilder(mongoTemplate, sourceCollection);
// 检查是否需要兼容性处理
if (!SpringBootCompatibilityHelper.supportsNestedFieldReferences()) {
System.out.println("检测到Spring Boot 2环境启用兼容性模式");
// 在后续的lookup操作后自动添加字段展平
}
return builder;
}
/**
* 兼容性lookup操作 - 自动添加字段展平
*/
public MongoAggregationBuilder lookupAndUnwindWithCompatibility(String from, String localField, String foreignField, String as) {
// 执行标准的lookup和unwind
this.lookupAndUnwind(from, localField, foreignField, as);
// 如果是Spring Boot 2环境添加字段展平投影
if (!SpringBootCompatibilityHelper.supportsNestedFieldReferences()) {
try {
// 根据lookup的目标集合添加相应的字段展平
if ("users".equals(from)) {
operations.add(Aggregation.project()
.andInclude("_id", "userId", "productId", "createdAt")
.and("user._id").as("userId")
.and("user.username").as("username")
.and("user.email").as("email"));
} else if ("product".equals(from)) {
operations.add(Aggregation.project()
.andInclude("_id", "userId", "productId", "createdAt")
.and("product._id").as("productId")
.and("product.name").as("productName")
.and("product.price").as("price"));
} else if ("categories".equals(from)) {
operations.add(Aggregation.project()
.andInclude("_id", "userId", "productId", "createdAt")
.and("category._id").as("categoryId")
.and("category.name").as("categoryName"));
}
} catch (Exception e) {
System.err.println("字段展平失败,继续使用原始字段: " + e.getMessage());
}
}
return this;
}
/**
* 兼容性统计操作
*/
public Map<String, Object> executeWithCompatibleStatistics() {
Map<String, Object> statistics = new HashMap<>();
try {
// 如果是Spring Boot 2环境使用展平字段策略
if (!SpringBootCompatibilityHelper.supportsNestedFieldReferences()) {
// 先添加字段展平操作
List<AggregationOperation> compatibleOps = new ArrayList<>(operations);
compatibleOps.addAll(SpringBootCompatibilityHelper.createCompatiblePipeline("shopping_record"));
// 1. 总数统计
List<AggregationOperation> countOps = new ArrayList<>(compatibleOps);
countOps.add(Aggregation.count().as("total"));
Aggregation countAgg = Aggregation.newAggregation(countOps);
AggregationResults<Map> countResults = mongoTemplate.aggregate(countAgg, sourceCollection, Map.class);
if (!countResults.getMappedResults().isEmpty()) {
statistics.put("count", countResults.getMappedResults().get(0));
}
// 2. 使用展平字段的价格统计
List<AggregationOperation> priceOps = new ArrayList<>(compatibleOps);
priceOps.addAll(SpringBootCompatibilityHelper.createFlatFieldStatistics());
Aggregation priceAgg = Aggregation.newAggregation(priceOps);
AggregationResults<Map> priceResults = mongoTemplate.aggregate(priceAgg, sourceCollection, Map.class);
if (!priceResults.getMappedResults().isEmpty()) {
statistics.put("priceStats", priceResults.getMappedResults().get(0));
}
// 3. 使用展平字段的分类统计
List<AggregationOperation> categoryOps = new ArrayList<>(compatibleOps);
categoryOps.addAll(SpringBootCompatibilityHelper.createFlatFieldCategoryStatistics());
categoryOps.add(Aggregation.limit(10));
Aggregation categoryAgg = Aggregation.newAggregation(categoryOps);
AggregationResults<Map> categoryResults = mongoTemplate.aggregate(categoryAgg, sourceCollection, Map.class);
statistics.put("categoryStats", categoryResults.getMappedResults());
} else {
// Spring Boot 3环境使用原始方法
return executeWithSimpleStatistics();
}
} catch (Exception e) {
System.err.println("兼容性统计查询失败: " + e.getMessage());
statistics.put("error", e.getMessage());
}
return statistics;
}
/**
* 数组操作支持 - 保留空数组的unwind
*/
public MongoAggregationBuilder unwindWithPreserveEmpty(String arrayField) {
UnwindOperation unwind = Aggregation.unwind(arrayField, true);
operations.add(unwind);
return this;
}
/**
* 采样支持 - 随机采样指定数量的文档
*/
public MongoAggregationBuilder sample(int size) {
operations.add(Aggregation.sample(size));
return this;
}
/**
* 数据去重 - 基于指定字段
*/
public MongoAggregationBuilder distinct(String... fields) {
GroupOperation groupOp = Aggregation.group(fields);
operations.add(groupOp);
return this;
}
}

View File

@ -0,0 +1,151 @@
package org.example.mongosimple.prepareEntity;
import java.util.Collections;
import java.util.List;
/**
* 分页结果包装类
* 用于封装MongoDB聚合查询的分页结果
*
* @param <T> 结果数据类型
* @author admin
*/
public class PageResult<T> {
private final List<T> content;
private final long total;
private final int page;
private final int size;
private final int totalPages;
private final boolean hasNext;
private final boolean hasPrevious;
/**
* 构造分页结果
*
* @param content 当前页数据
* @param total 总记录数
* @param page 当前页码从0开始
* @param size 每页大小
*/
public PageResult(List<T> content, long total, int page, int size) {
this.content = content != null ? content : Collections.emptyList();
this.total = total;
this.page = page;
this.size = size;
this.totalPages = size > 0 ? (int) Math.ceil((double) total / size) : 0;
this.hasNext = page < totalPages - 1;
this.hasPrevious = page > 0;
}
/**
* 获取当前页数据
*/
public List<T> getContent() {
return content;
}
/**
* 获取总记录数
*/
public long getTotal() {
return total;
}
/**
* 获取当前页码从0开始
*/
public int getPage() {
return page;
}
/**
* 获取每页大小
*/
public int getSize() {
return size;
}
/**
* 获取总页数
*/
public int getTotalPages() {
return totalPages;
}
/**
* 是否有下一页
*/
public boolean hasNext() {
return hasNext;
}
/**
* 是否有上一页
*/
public boolean hasPrevious() {
return hasPrevious;
}
/**
* 当前页是否为空
*/
public boolean isEmpty() {
return content.isEmpty();
}
/**
* 获取当前页元素数量
*/
public int getNumberOfElements() {
return content.size();
}
/**
* 是否为第一页
*/
public boolean isFirst() {
return page == 0;
}
/**
* 是否为最后一页
*/
public boolean isLast() {
return page >= totalPages - 1;
}
/**
* 获取下一页页码
*/
public int getNextPage() {
return hasNext ? page + 1 : page;
}
/**
* 获取上一页页码
*/
public int getPreviousPage() {
return hasPrevious ? page - 1 : page;
}
/**
* 获取分页信息摘要
*/
public String getSummary() {
if (total == 0) {
return "无数据";
}
int start = page * size + 1;
int end = Math.min((page + 1) * size, (int) total);
return String.format("第%d页共%d页显示第%d-%d条共%d条记录",
page + 1, totalPages, start, end, total);
}
@Override
public String toString() {
return String.format("PageResult{page=%d, size=%d, total=%d, totalPages=%d, elements=%d, hasNext=%s, hasPrevious=%s}",
page, size, total, totalPages, content.size(), hasNext, hasPrevious);
}
}

View File

@ -0,0 +1,265 @@
package org.example.mongosimple.prepareEntity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 搜索参数类 - 支持更多字段的动态查询
*/
@Data
@Accessors(chain = true)
public class ShoppingSearchParams {
// 购物阶段
private String phaseName;
// 用户相关
private String usernamePattern;
private String email;
private String phone;
private String city;
private List<String> cities;
private Boolean isActive;
private Integer minAge;
private Integer maxAge;
private LocalDateTime regStartDate;
private LocalDateTime regEndDate;
// 产品相关
private String productName;
private String brand;
private BigDecimal minPrice;
private BigDecimal maxPrice;
private Integer minStock;
private Integer maxStock;
private Boolean inStock;
private String categoryName;
private List<String> categoryNames;
private List<String> productTags;
// 时间范围
private LocalDateTime startDate;
private LocalDateTime endDate;
// 排序和分页
private String sortField = "product.price";
private boolean ascending = false;
private int offset = 0;
private int limit = 20;
// 便捷构造方法
public static ShoppingSearchParams builder() {
return new ShoppingSearchParams();
}
public ShoppingSearchParams phase(String phaseName) {
this.phaseName = phaseName;
return this;
}
public ShoppingSearchParams userLike(String usernamePattern) {
this.usernamePattern = usernamePattern;
return this;
}
public ShoppingSearchParams email(String email) {
this.email = email;
return this;
}
public ShoppingSearchParams city(String city) {
this.city = city;
return this;
}
public ShoppingSearchParams cities(String... cities) {
this.cities = List.of(cities);
return this;
}
public ShoppingSearchParams productLike(String productName) {
this.productName = productName;
return this;
}
public ShoppingSearchParams priceRange(BigDecimal min, BigDecimal max) {
this.minPrice = min;
this.maxPrice = max;
return this;
}
public ShoppingSearchParams brand(String brand) {
this.brand = brand;
return this;
}
public ShoppingSearchParams categoryName(String categoryName) {
this.categoryName = categoryName;
return this;
}
public ShoppingSearchParams maxPrice(BigDecimal maxPrice) {
this.maxPrice = maxPrice;
return this;
}
public ShoppingSearchParams categories(String... categories) {
this.categoryNames = List.of(categories);
return this;
}
public ShoppingSearchParams inStock(Boolean inStock) {
this.inStock = inStock;
return this;
}
public ShoppingSearchParams sort(String field, boolean asc) {
this.sortField = field;
this.ascending = asc;
return this;
}
public ShoppingSearchParams page(int offset, int limit) {
this.offset = offset;
this.limit = limit;
return this;
}
public ShoppingSearchParams sortField(String sortField) {
this.sortField = sortField;
return this;
}
public ShoppingSearchParams ascending(boolean ascending) {
this.ascending = ascending;
return this;
}
// 添加分页相关的便捷方法
public int getPage() {
return limit > 0 ? offset / limit : 0;
}
public int getSize() {
return limit;
}
public ShoppingSearchParams pageNumber(int page, int size) {
this.offset = page * size;
this.limit = size;
return this;
}
// 手动添加getter方法以确保访问权限
public String getSortField() {
return sortField;
}
public boolean isAscending() {
return ascending;
}
public int getOffset() {
return offset;
}
public int getLimit() {
return limit;
}
// 其他可能需要的getter方法
public String getPhaseName() {
return phaseName;
}
public String getUsernamePattern() {
return usernamePattern;
}
public String getEmail() {
return email;
}
public String getPhone() {
return phone;
}
public String getCity() {
return city;
}
public List<String> getCities() {
return cities;
}
public Boolean getIsActive() {
return isActive;
}
public Integer getMinAge() {
return minAge;
}
public Integer getMaxAge() {
return maxAge;
}
public LocalDateTime getRegStartDate() {
return regStartDate;
}
public LocalDateTime getRegEndDate() {
return regEndDate;
}
public String getProductName() {
return productName;
}
public String getBrand() {
return brand;
}
public BigDecimal getMinPrice() {
return minPrice;
}
public BigDecimal getMaxPrice() {
return maxPrice;
}
public Integer getMinStock() {
return minStock;
}
public Integer getMaxStock() {
return maxStock;
}
public Boolean getInStock() {
return inStock;
}
public String getCategoryName() {
return categoryName;
}
public List<String> getCategoryNames() {
return categoryNames;
}
public List<String> getProductTags() {
return productTags;
}
public LocalDateTime getStartDate() {
return startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
}

View File

@ -0,0 +1,400 @@
package org.example.mongosimple.prepareEntity;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.GroupOperation;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation;
import java.util.ArrayList;
import java.util.List;
/**
* Spring Boot 2/3兼容性助手
* 解决不同版本间的API差异
*
* @author admin
*/
public class SpringBootCompatibilityHelper {
/**
* 创建兼容的价格统计group操作
* Spring Boot 2需要明确的字段路径
*/
public static List<AggregationOperation> createPriceStatisticsOperations() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 方案1: 尝试使用完整的嵌套字段路径
GroupOperation groupOp = Aggregation.group()
.sum("product.price").as("totalAmount")
.avg("product.price").as("avgPrice")
.min("product.price").as("minPrice")
.max("product.price").as("maxPrice")
.count().as("orderCount");
operations.add(groupOp);
} catch (Exception e) {
System.err.println("嵌套字段引用失败,尝试投影后的字段: " + e.getMessage());
try {
// 方案2: 先添加投影操作将嵌套字段提升到顶层
operations.add(Aggregation.project()
.and("product.price").as("price")
.and("product.name").as("productName")
.and("product._id").as("productId")
.andInclude("_id", "userId", "createdAt"));
// 然后使用顶层字段进行group操作
GroupOperation groupOp = Aggregation.group()
.sum("price").as("totalAmount")
.avg("price").as("avgPrice")
.min("price").as("minPrice")
.max("price").as("maxPrice")
.count().as("orderCount");
operations.add(groupOp);
} catch (Exception e2) {
System.err.println("投影方案也失败,使用最简单的统计: " + e2.getMessage());
// 方案3: 最简单的统计只计算数量
operations.add(Aggregation.group().count().as("orderCount"));
}
}
return operations;
}
/**
* 创建兼容的分类统计group操作
*/
public static List<AggregationOperation> createCategoryStatisticsOperations() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 尝试Spring Boot 3风格
GroupOperation groupOp = Aggregation.group("category.name")
.count().as("count")
.sum("product.price").as("totalAmount")
.avg("product.price").as("avgPrice");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count"));
} catch (Exception e) {
// 降级到Spring Boot 2风格
try {
GroupOperation groupOp = Aggregation.group("categoryName")
.count().as("count")
.sum("price").as("totalAmount");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count"));
} catch (Exception e2) {
// 最简单的分组
operations.add(Aggregation.group("_id").count().as("count"));
}
}
return operations;
}
/**
* 创建兼容的用户统计group操作
*/
public static List<AggregationOperation> createUserStatisticsOperations() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 尝试Spring Boot 3风格
GroupOperation groupOp = Aggregation.group("user._id")
.first("user.username").as("username")
.first("address.city").as("city")
.count().as("totalOrders")
.sum("product.price").as("totalSpent")
.avg("product.price").as("avgOrderValue");
operations.add(groupOp);
} catch (Exception e) {
// 降级到Spring Boot 2风格
try {
GroupOperation groupOp = Aggregation.group("userId")
.first("username").as("username")
.first("city").as("city")
.count().as("totalOrders")
.sum("price").as("totalSpent");
operations.add(groupOp);
} catch (Exception e2) {
// 最简单的用户分组
operations.add(Aggregation.group("userId").count().as("totalOrders"));
}
}
return operations;
}
/**
* 创建兼容的产品统计group操作
* 使用明确的字段投影策略
*/
public static List<AggregationOperation> createProductStatisticsOperations() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 方案1: 尝试直接使用嵌套字段
GroupOperation groupOp = Aggregation.group("product._id")
.first("product.name").as("productName")
.first("product.price").as("price")
.first("category.name").as("categoryName")
.count().as("purchaseCount")
.sum("product.price").as("totalRevenue");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "purchaseCount"));
} catch (Exception e) {
System.err.println("产品嵌套字段分组失败,使用投影方案: " + e.getMessage());
try {
// 方案2: 先投影再分组
operations.add(Aggregation.project()
.and("product._id").as("productId")
.and("product.name").as("productName")
.and("product.price").as("price")
.and("category.name").as("categoryName")
.andInclude("_id", "userId", "createdAt"));
GroupOperation groupOp = Aggregation.group("productId")
.first("productName").as("productName")
.first("price").as("price")
.first("categoryName").as("categoryName")
.count().as("purchaseCount")
.sum("price").as("totalRevenue");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "purchaseCount"));
} catch (Exception e2) {
System.err.println("投影分组也失败,使用原始字段: " + e2.getMessage());
try {
// 方案3: 使用原始字段名
GroupOperation groupOp = Aggregation.group("productId")
.count().as("purchaseCount");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "purchaseCount"));
} catch (Exception e3) {
// 方案4: 最简单的分组
operations.add(Aggregation.group("_id").count().as("purchaseCount"));
}
}
}
return operations;
}
/**
* 创建兼容的投影操作
*/
public static ProjectionOperation createCompatibleProjection() {
try {
// 尝试Spring Boot 3风格
return Aggregation.project()
.and("user.username").as("username")
.and("product.name").as("productName")
.and("product.price").as("price")
.and("category.name").as("categoryName")
.and("address.city").as("city")
.and("phase.name").as("phaseName");
} catch (Exception e) {
// 降级到Spring Boot 2风格
try {
return Aggregation.project()
.and("username").as("username")
.and("productName").as("productName")
.and("price").as("price")
.and("categoryName").as("categoryName")
.and("city").as("city")
.and("phaseName").as("phaseName");
} catch (Exception e2) {
// 最简单的投影
return Aggregation.project()
.andInclude("_id", "username", "productName", "price");
}
}
}
/**
* 检测Spring Boot版本并返回适当的字段引用
*/
public static String getCompatibleFieldReference(String nestedField, String fallbackField) {
try {
// 尝试使用嵌套字段引用
// 这里可以添加实际的版本检测逻辑
return nestedField;
} catch (Exception e) {
return fallbackField;
}
}
/**
* 创建兼容的排序操作
*/
public static AggregationOperation createCompatibleSort(String field, boolean ascending) {
try {
return Aggregation.sort(
ascending ?
org.springframework.data.domain.Sort.Direction.ASC :
org.springframework.data.domain.Sort.Direction.DESC,
field
);
} catch (Exception e) {
// 降级方案
return Aggregation.sort(
org.springframework.data.domain.Sort.Direction.DESC,
"_id"
);
}
}
/**
* 获取Spring Data MongoDB版本信息
*/
public static String getSpringDataMongoVersion() {
try {
Package pkg = Aggregation.class.getPackage();
return pkg.getImplementationVersion() != null ?
pkg.getImplementationVersion() : "Unknown";
} catch (Exception e) {
return "Unknown";
}
}
/**
* 检查是否支持复杂的字段引用
*/
public static boolean supportsNestedFieldReferences() {
try {
// 尝试创建一个包含嵌套字段引用的操作
Aggregation.group().sum("nested.field").as("test");
return true;
} catch (Exception e) {
return false;
}
}
/**
* 创建Spring Boot 2兼容的聚合管道
* 核心策略先投影展平字段再进行复杂操作
*/
public static List<AggregationOperation> createCompatiblePipeline(String targetCollection) {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 策略1: 创建标准化的投影将所有嵌套字段展平
ProjectionOperation projection = Aggregation.project()
// 用户相关字段
.and("user._id").as("userId")
.and("user.username").as("username")
.and("user.email").as("email")
// 产品相关字段
.and("product._id").as("productId")
.and("product.name").as("productName")
.and("product.price").as("price")
.and("product.brand").as("brand")
// 分类相关字段
.and("category._id").as("categoryId")
.and("category.name").as("categoryName")
// 地址相关字段
.and("address._id").as("addressId")
.and("address.city").as("city")
.and("address.province").as("province")
// 阶段相关字段
.and("phase._id").as("phaseId")
.and("phase.name").as("phaseName")
// 保留原始字段
.andInclude("_id", "createdAt", "updatedAt");
operations.add(projection);
} catch (Exception e) {
System.err.println("创建标准化投影失败: " + e.getMessage());
// 降级方案只保留基本字段
try {
ProjectionOperation simpleProjection = Aggregation.project()
.andInclude("_id", "userId", "productId", "createdAt")
.and("user").as("userInfo")
.and("product").as("productInfo");
operations.add(simpleProjection);
} catch (Exception e2) {
System.err.println("简单投影也失败,跳过投影步骤: " + e2.getMessage());
}
}
return operations;
}
/**
* 创建基于展平字段的统计操作
*/
public static List<AggregationOperation> createFlatFieldStatistics() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 使用展平后的字段进行统计
GroupOperation groupOp = Aggregation.group()
.sum("price").as("totalAmount")
.avg("price").as("avgPrice")
.min("price").as("minPrice")
.max("price").as("maxPrice")
.count().as("orderCount");
operations.add(groupOp);
} catch (Exception e) {
System.err.println("展平字段统计失败: " + e.getMessage());
operations.add(Aggregation.group().count().as("orderCount"));
}
return operations;
}
/**
* 创建基于展平字段的分类统计
*/
public static List<AggregationOperation> createFlatFieldCategoryStatistics() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 使用展平后的字段进行分类统计
GroupOperation groupOp = Aggregation.group("categoryName")
.count().as("count")
.sum("price").as("totalAmount")
.avg("price").as("avgPrice");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "count"));
} catch (Exception e) {
System.err.println("展平字段分类统计失败: " + e.getMessage());
operations.add(Aggregation.group("categoryId").count().as("count"));
}
return operations;
}
/**
* 创建基于展平字段的产品统计
*/
public static List<AggregationOperation> createFlatFieldProductStatistics() {
List<AggregationOperation> operations = new ArrayList<>();
try {
// 使用展平后的字段进行产品统计
GroupOperation groupOp = Aggregation.group("productId")
.first("productName").as("productName")
.first("price").as("price")
.first("categoryName").as("categoryName")
.count().as("purchaseCount")
.sum("price").as("totalRevenue");
operations.add(groupOp);
operations.add(Aggregation.sort(org.springframework.data.domain.Sort.Direction.DESC, "purchaseCount"));
} catch (Exception e) {
System.err.println("展平字段产品统计失败: " + e.getMessage());
operations.add(Aggregation.group("productId").count().as("purchaseCount"));
}
return operations;
}
}

View File

@ -0,0 +1,143 @@
package org.example.mongosimple.service;
import lombok.RequiredArgsConstructor;
import org.example.mongosimple.entity.ShoppingResult;
import org.example.mongosimple.prepareEntity.FieldMapping;
import org.example.mongosimple.prepareEntity.MongoAggregationBuilder;
import org.example.mongosimple.prepareEntity.PageResult;
import org.example.mongosimple.prepareEntity.ShoppingSearchParams;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ShoppingAggregationService {
private final MongoTemplate mongoTemplate;
private FieldMapping createShoppingFieldMapping() {
return new FieldMapping()
.setDefaultIncludeFields("_id", "createdAt")
// 添加常用的投影字段映射
.addProjectionMapping("user.username", "username")
.addProjectionMapping("user.email", "email")
.addProjectionMapping("user.phone", "phone")
.addProjectionMapping("user.age", "age")
.addProjectionMapping("user.isActive", "isActive")
.addProjectionMapping("product.name", "productName")
.addProjectionMapping("product.price", "price")
.addProjectionMapping("product.brand", "brand")
.addProjectionMapping("product.stock", "stock")
.addProjectionMapping("category.name", "categoryName")
.addProjectionMapping("address.city", "city")
.addProjectionMapping("phase.name", "phaseName")
// 添加排序字段映射
.addSortMapping("product.price", "price");
}
public PageResult<ShoppingResult> searchWithCriteriaAndPagination(ShoppingSearchParams params) {
try {
// 创建字段映射配置
FieldMapping fieldMapping = new FieldMapping()
.setDefaultIncludeFields("_id", "createdAt")
// 添加常用的投影字段映射
.addProjectionMapping("user.username", "username")
.addProjectionMapping("user.email", "email")
.addProjectionMapping("user.phone", "phone")
.addProjectionMapping("user.age", "age")
.addProjectionMapping("user.isActive", "isActive")
.addProjectionMapping("product.name", "productName")
.addProjectionMapping("product.price", "price")
.addProjectionMapping("product.brand", "brand")
.addProjectionMapping("product.stock", "stock")
.addProjectionMapping("category.name", "categoryName")
.addProjectionMapping("address.city", "city")
.addProjectionMapping("phase.name", "phaseName")
// 添加排序字段映射
.addSortMapping("product.price", "price");
// 获取兼容的排序字段
String compatibleSortField = fieldMapping.getCompatibleSortField(params.getSortField(), "price");
return MongoAggregationBuilder.fromWithCompatibility(mongoTemplate, "shopping_record")
// 关联所有需要的表
.lookupAndUnwind("shopping_phase", "shoppingPhaseId", "_id", "phase")
.lookupAndUnwind("users", "userId", "_id", "user")
.lookupAndUnwind("address", "user.addressId", "_id", "address")
.lookupAndUnwind("product", "productId", "_id", "product")
.lookupAndUnwind("categories", "product.categoryId", "_id", "category")
// 使用动态Criteria构建器
.matchDynamic(criteria -> criteria
.whenNotEmpty(params.getPhaseName(), "phase.name")
.whenNotEmptyLike(params.getUsernamePattern(), "user.username")
.whenNotEmpty(params.getEmail(), "user.email")
.whenNotEmpty(params.getCity(), "address.city")
.whenNotEmptyIn(params.getCities(), "address.city")
.whenNotEmptyLike(params.getProductName(), "product.name")
.whenNotEmpty(params.getBrand(), "product.brand")
.whenRange(params.getMinPrice(), params.getMaxPrice(), "product.price")
.whenRange(params.getMinStock(), params.getMaxStock(), "product.stock")
.whenNotNull(params.getInStock(), "product.inStock")
.whenNotEmpty(params.getCategoryName(), "category.name")
.whenNotEmptyIn(params.getCategoryNames(), "category.name")
.whenNotEmptyIn(params.getProductTags(), "product.tags")
.whenDateRange(params.getStartDate(), params.getEndDate(), "createdAt")
.whenNotNull(params.getIsActive(), "user.isActive")
.whenRange(params.getMinAge(), params.getMaxAge(), "user.age")
.whenDateRange(params.getRegStartDate(), params.getRegEndDate(), "user.registrationDate")
)
// 使用手动配置的投影映射
.project(fieldMapping.createProjectionFunction())
// 排序 - 使用配置获取的兼容字段名
.sort(compatibleSortField, params.isAscending())
// 使用facet分页 - 一次查询获取总数和分页数据
.executeWithPagination(ShoppingResult.class, params.getPage(), params.getSize());
} catch (Exception e) {
System.err.println("Facet分页查询失败使用传统分页: " + e.getMessage());
System.err.println("错误详情: " + e.getClass().getSimpleName() + " - " + e.getMessage());
// 降级到传统分页
// return searchWithTraditionalPagination(params);
return null;
}
}
/* *//**
* 传统分页查询 - 降级方案
*//*
private PageResult<ShoppingResult> searchWithTraditionalPagination(ShoppingSearchParams params) {
try {
// 1. 获取总数
List<ShoppingResult> allResults = searchWithCriteria(params);
long total = allResults.size();
// 2. 获取分页数据
int offset = params.getPage() * params.getSize();
int limit = params.getSize();
List<ShoppingResult> pagedResults;
if (offset >= allResults.size()) {
pagedResults = new ArrayList<>();
} else {
int endIndex = Math.min(offset + limit, allResults.size());
pagedResults = allResults.subList(offset, endIndex);
}
return new PageResult<>(pagedResults, total, params.getPage(), params.getSize());
} catch (Exception e) {
System.err.println("传统分页也失败: " + e.getMessage());
return new PageResult<>(new ArrayList<>(), 0, params.getPage(), params.getSize());
}
}*/
}

View File

@ -0,0 +1,4 @@
spring:
data:
mongodb:
uri: mongodb://localhost:27017/test # 数据库名 test

View File

@ -0,0 +1,13 @@
package org.example.mongosimple;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MongoSimpleApplicationTests {
@Test
void contextLoads() {
}
}