Merge v3.5 (#682)

* feat: Batch approve 1 (#654)

* InternalPermission

* feat: batch-approve

* Share assign n 90 (#657)

* fix css

* be: styles

* feat: 导入仅更新不要新建

* feat: meetByFormula

* feat: img for ContentWithFieldVars

* feat: 表单引用 mob

* feat: UTYPE_ACCOUNT20

* Feat datalist btn 80 (#658)

* feat: regRowAction

* be: protable width

* feat: pdfjs 

* be: pdf preview

* feat: proxy-download

* be: message with files

* Quick query and more (#661)

* enh: show isDisabled in trigger

* Gitee#I818F8

* enh: 记录转换3级字段

* enh: advfilter deep3 in `admin`

* enh: buildFieldsWithRefs

* be: $pages

* be: countdown for 批量修改

* fix: trigger timer

* Gitee#I7VHJX

* be: FIELDAGGREGATION=GROUPAGGREGATION; RBJOIN FILE

* Members in dept view (#663)

* feat: RBJOIN2

* enh: LiteFormModal

* feat: Member show on view

* Diagramming for trigger RB-93 (#664)

* be:evalTriggerTimes

* be: RbFormRefform

* be: ObservableService order

* entities er

* be: user delete checks

* be: row selected

* be: .dataTables_oper.invisible

* be: BatchOperator default selected

* feat: mermaid

* be: comp {@NOW}

* be: passwd 10

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>

* Api logs view (#667)

* be: show nums&dims

* feat: api-logs

* Update QueryFactory.java

* Bump js libs

* be:RRL

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>

* tmpl better (#668)

* be: isSame

* socketTimeout=0

* feat: front

* WordReportGenerator

* style: print

* be: common-save

* be: 本地加载PDF

* fops

* inTriggerTime2

* report 100w

* word tmpl

* word table

* style

* Update @rbv

* AppHome._InTab

* IsNullFunction

* modal onDoubleClick

* Datasync 94 (#671)

* feat: buildFormData:retAll, onProTableLineUpdated

* feat:getLineFrom

* api:KnownExceptionConverter

* be:复制打印设置

* be:doc2html

* be:form hook 传参

* ToHtml

* be: html all sheets, colspan

* trigger KnownExceptionConverter

* getCurrentForm

* fix: clearFields

* AUTOTRANSFORM following

* fix: trigger on share use ID and getFixedRecordId

* 1.RevisionHistory AutoId;

* uc bind

* Update OpenApiSDK.java

* styles

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>

* Mode2 fields 19 (#673)

* readExcelRows

* feat: match-fields

* fix: ntext riching fields

* devMode return null

* fix: keep request-data raw

* mvnw

* guide new

* nav-style35

* feat: sort n

* be: backup

* ignoreTables

* fix: tag default value

* DatabaseFixer

* fixV346

* feat: _PageMourningMode

* be: zoom

* DATEPICKAT

* $select2MatcherAll,

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>

* File n select (#676)

* feat: details ahowAt2

* enh: file preview on form

* be: readonlyw  自动值

* Be files (#677)

* fix: upload error 413

* be: files styles

* _Notification

* be: 驳回默认上一级

* error-container

* enh: 审批成功后通知提交人

* word img

* be: fields

* be: file preview

* feat: view-add n2nref

* Update nginx-rebuild.conf

* api error format

* be: styles

* file-share forever

* checkNullable35

* Lazy wait details finished (#678)

* lazy HookUrl

* fix: $same

* enh: 记录转换 AnyRef

* Be v3.5 (#680)

* frontjs

* be: 公式日期值

* IMP: 明细使用自有 Service

* 公开注册

* be: this._defaultBackPath

* style: dataTables_oper.invisible2

* varRecord

* fix: 明细未配置或出错

* feat: autoLocation

* Fix 3.5.0 (#681)

* __LAB_CHARTANIMATION

* feat: RBJOIN3

* fix: miss logs

* 3.5.0-beta1

* better

* fix: TextFunction

* fix: api

---------

Co-authored-by: devezhao <zhaofang123@gmail.com>
This commit is contained in:
REBUILD 企业管理系统 2023-11-24 00:46:55 +08:00 committed by GitHub
parent dba501d616
commit 67e12fb774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
730 changed files with 198027 additions and 34186 deletions

View file

@ -1,23 +1,33 @@
# include nginx-rebuild.conf;
server {
server_name YOUR_DOMAIN;
listen 80;
server_name YOUR_IP_OR_DOMAIN;
listen 80;
# HTTPS
#listen 443 ssl http2;
#ssl_certificate /path/to/ssl.crt;
#ssl_certificate_key /path/to/ssl.key;
#listen 443 ssl http2;
#ssl_certificate /path/to/ssl.crt;
#ssl_certificate_key /path/to/ssl.key;
# PROXY
proxy_redirect http:// $scheme://;
proxy_set_header Host $host;
#proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect http:// $scheme://;
proxy_set_header Host $host:$server_port;
proxy_set_header Remote-Host $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
#proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 90;
proxy_send_timeout 300;
proxy_read_timeout 300;
proxy_buffer_size 8k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
client_max_body_size 200m; # Max of file upload
client_body_buffer_size 128k;
# PASS
location / {
proxy_pass http://127.0.0.1:18080;
etag on;
proxy_pass http://127.0.0.1:18080;
etag on;
}
location /assets {
proxy_pass http://127.0.0.1:18080/assets;
expires 90d;
proxy_pass http://127.0.0.1:18080/assets;
expires 90d;
}
}

BIN
.mvn/wrapper/maven-wrapper.jar vendored Normal file

Binary file not shown.

18
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View file

@ -0,0 +1,18 @@
# 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.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

2
@rbv

@ -1 +1 @@
Subproject commit 0f5ad73c12a1f5a617314efe665074687f17e490
Subproject commit 15d7f439a84cef4be39dadada2d51e2e6ab4f375

View file

@ -13,23 +13,23 @@ REBUILD 侧重于业务需求实现,而非基础技术框架或项目启动模
更多详情介绍 [https://getrebuild.com/](https://getrebuild.com/)
> **福利:加入 REBUILD QQ 交流群 819865721(满) 1013051587 GET 使用技能**
> **福利:加入 REBUILD VIP 用户 QQ 交流群 819865721 1013051587 GET 使用技能**
## V3.4 新特性
## V3.5 新特性
本次更新为你带来众多功能增强与优化。
1. [新增] 发送通知触发器支持发送钉钉/企业微信(机器人)群消息
2. [新增] 表单回填支持启用后端回填
3. [新增] 角色扩展权限“允许撤销审批”
4. [新增] 字段聚合、分组聚合触发器支持多引用字段“连接”模式
5. [新增] 标签类字段支持自定义颜色
6. [新增] 手机版对 HTML5+ 环境的支持
8. [新增] 字段“允许重复”选项支持配置检查数据范围、逻辑条件
7. [新增] 日期条件支持去年/明年、上季度/下季度、上月/下月、上周/下周
1. [新增] 批量审批
2. [新增] 表单引用组件
3. [新增] 触发器执行流程图
4. [新增] 触发器高级表达式函数 `ISNULL` `DATEPICKAT`
5. [新增] WORD 模板
6. [新增] 手机版支持导出报表
7. [优化] 手机版导航样式优化
8. [优化] 明细实体支持显示在视图页下方
9. ...
更多新特性请参见 [更新日志](https://getrebuild.com/docs/dev/changelog)
更多更新详情请参见 [更新日志](https://getrebuild.com/docs/dev/changelog)
## 在线体验

308
mvnw vendored Executable file
View file

@ -0,0 +1,308 @@
#!/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.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
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"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

205
mvnw.cmd vendored Normal file
View file

@ -0,0 +1,205 @@
@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.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

49
pom.xml
View file

@ -5,12 +5,12 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<version>2.7.17</version>
<relativePath/>
</parent>
<groupId>com.rebuild</groupId>
<artifactId>rebuild</artifactId>
<version>3.4.6</version>
<version>3.5.0-beta1</version>
<name>rebuild</name>
<description>Building your business-systems freely!</description>
<!-- UNCOMMENT USE TOMCAT -->
@ -257,7 +257,6 @@
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -271,7 +270,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>commons</artifactId>
<version>1.3.10</version>
<version>1.3.13</version>
<exclusions>
<exclusion>
<artifactId>httpclient</artifactId>
@ -291,7 +290,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>persist4j</artifactId>
<version>1.7.2</version>
<version>1.7.5</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
@ -316,17 +315,17 @@
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
<version>1.9.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
@ -336,12 +335,12 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.1</version>
<version>4.4.3</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.13.1</version>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
@ -351,7 +350,7 @@
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.2</version>
<version>6.4.5</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
@ -399,15 +398,20 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
@ -427,7 +431,7 @@
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.19</version>
<version>0.4.20</version>
</dependency>
<dependency>
<groupId>es.moki.ratelimitj</groupId>
@ -437,12 +441,12 @@
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
<version>3.23.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@ -468,12 +472,12 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.19</version>
<version>5.8.21</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.13.0</version>
</dependency>
<!-- Need JDK11+ -->
<!--
@ -488,17 +492,22 @@
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.zwobble.mammoth</groupId>
<artifactId>mammoth</artifactId>
<version>1.5.0</version>
</dependency>
<!-- fix: CVEs -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.12.0</version>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.23.0</version>
<version>1.24.0</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>

View file

@ -11,10 +11,12 @@ import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.privileges.UserService;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.util.Collections;
import java.util.Map;
/**
@ -52,7 +54,7 @@ public class ApiContext {
* @param bindUser
*/
public ApiContext(Map<String, String> reqParams, JSON postData, String appId, ID bindUser) {
this.reqParams = reqParams;
this.reqParams = Collections.unmodifiableMap(reqParams);
this.postData = postData;
this.appId = appId;
this.bindUser = bindUser;
@ -88,7 +90,7 @@ public class ApiContext {
* @return
*/
public JSON getPostData() {
return postData == null ? new JSONObject() : postData;
return postData == null ? new JSONObject() : JSONUtils.clone(postData);
}
/**
@ -97,7 +99,7 @@ public class ApiContext {
* @throws ApiInvokeException
*/
public String getParameterNotBlank(String name) throws ApiInvokeException {
String value = getParameterMap().get(name);
String value = reqParams.get(name);
if (StringUtils.isBlank(value)) {
throw new ApiInvokeException(ApiInvokeException.ERR_BADPARAMS, "Parameter [" + name + "] cannot be null");
}
@ -109,7 +111,7 @@ public class ApiContext {
* @return
*/
public String getParameter(String name) {
return getParameterMap().get(name);
return reqParams.get(name);
}
/**
@ -128,29 +130,18 @@ public class ApiContext {
* @return
*/
public int getParameterAsInt(String name, int defaultValue) {
String value = getParameterMap().get(name);
String value = reqParams.get(name);
if (NumberUtils.isNumber(value)) return NumberUtils.toInt(value);
else return defaultValue;
}
/**
* @param name
* @param defaultValue
* @return
*/
public long getParameterAsLong(String name, long defaultValue) {
String value = getParameterMap().get(name);
if (NumberUtils.isNumber(value)) return NumberUtils.toLong(value);
else return defaultValue;
}
/**
* @param name
* @param defaultValue
* @return
*/
public boolean getParameterAsBool(String name, boolean defaultValue) {
String value = getParameterMap().get(name);
String value = reqParams.get(name);
if (StringUtils.isBlank(value)) return defaultValue;
else return BooleanUtils.toBoolean(value);
}

View file

@ -10,6 +10,7 @@ package com.rebuild.api;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.EncryptUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.commons.ThrowableUtils;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
@ -60,7 +61,7 @@ public class ApiGateway extends Controller implements Initialization {
// 基于 IP 限流
private static final RequestRateLimiter RRL = RateLimiters.createRateLimiter(
new int[] { 10, 60 },
new int[] { 600, 6000 });
new int[] { 600, 3000 });
private static final Map<String, Class<? extends BaseApi>> API_CLASSES = new HashMap<>();
@ -96,7 +97,7 @@ public class ApiGateway extends Controller implements Initialization {
final String requestId = CommonsUtils.randomHex();
response.addHeader("X-RB-Server", ServerStatus.STARTUP_ONCE + "/" + Application.BUILD);
response.setHeader("X-Request-Id", requestId);
response.setHeader("X-RB-RequestId", requestId);
if (RRL.overLimitWhenIncremented("ip:" + remoteIp)) {
JSON error = formatFailure("Request frequency exceeded", ApiInvokeException.ERR_FREQUENCY);
@ -111,7 +112,9 @@ public class ApiGateway extends Controller implements Initialization {
ApiContext context = null;
try {
final BaseApi api = createApi(apiName);
context = verfiy(request, api);
context = buildBaseApiContext(request);
context = verfiy(request, context, api);
UserContextHolder.setReqip(remoteIp);
UserContextHolder.setUser(context.getBindUser());
@ -130,7 +133,7 @@ public class ApiGateway extends Controller implements Initialization {
errorMsg = ex.getLocalizedMessage();
} catch (Throwable ex) {
errorCode = Controller.CODE_SERV_ERROR;
errorMsg = ex.getLocalizedMessage();
errorMsg = ThrowableUtils.getRootCause(ex).getLocalizedMessage();
log.error("Server Internal Error ({})", requestId, ex);
String knownError = KnownExceptionConverter.convert2ErrorMsg(ex);
@ -154,16 +157,12 @@ public class ApiGateway extends Controller implements Initialization {
* 验证请求并构建请求上下文
*
* @param request
* @param base
* @param useApi
* @return
*/
protected ApiContext verfiy(HttpServletRequest request, @SuppressWarnings("unused") BaseApi useApi) {
final Map<String, String> sortedMap = new TreeMap<>();
for (Map.Entry<String, String[]> e : request.getParameterMap().entrySet()) {
String[] vv = e.getValue();
sortedMap.put(e.getKey(), vv == null || vv.length == 0 ? null : vv[0]);
}
protected ApiContext verfiy(HttpServletRequest request, ApiContext base, @SuppressWarnings("unused") BaseApi useApi) {
final Map<String, String> sortedMap = new TreeMap<>(base.getParameterMap());
final String appid = getParameterNotNull(sortedMap, "appid");
final String sign = getParameterNotNull(sortedMap, "sign");
@ -228,15 +227,40 @@ public class ApiGateway extends Controller implements Initialization {
}
}
// 组合请求数据
String postData = ServletUtils.getRequestString(request);
JSON postJson = postData != null ? (JSON) JSON.parse(postData) : null;
ID bindUser = apiConfig.getID("bindUser");
// 默认绑定系统用户
if (bindUser == null) bindUser = UserService.SYSTEM_USER;
return new ApiContext(sortedMap, postJson, appid, bindUser);
return new ApiContext(sortedMap, base.getPostData(), appid, bindUser);
}
/**
* @param apiName
* @return
*/
private BaseApi createApi(String apiName) {
if (!API_CLASSES.containsKey(apiName)) {
throw new ApiInvokeException(ApiInvokeException.ERR_BADAPI, "Unknown API : " + apiName);
}
return (BaseApi) ReflectUtils.newInstance(API_CLASSES.get(apiName));
}
/**
* @param request
* @return
*/
private ApiContext buildBaseApiContext(HttpServletRequest request) {
Map<String, String> sortedMap = new TreeMap<>();
for (Map.Entry<String, String[]> e : request.getParameterMap().entrySet()) {
String[] item = e.getValue();
sortedMap.put(e.getKey(), item == null || item.length == 0 ? null : item[0]);
}
String appid = getParameterNotNull(sortedMap, "appid");
String postData = ServletUtils.getRequestString(request);
JSON postJson = postData != null ? (JSON) JSON.parse(postData) : null;
return new ApiContext(sortedMap, postJson, appid, null);
}
/**
@ -252,17 +276,6 @@ public class ApiGateway extends Controller implements Initialization {
return v;
}
/**
* @param apiName
* @return
*/
private BaseApi createApi(String apiName) {
if (!API_CLASSES.containsKey(apiName)) {
throw new ApiInvokeException(ApiInvokeException.ERR_BADAPI, "Unknown API : " + apiName);
}
return (BaseApi) ReflectUtils.newInstance(API_CLASSES.get(apiName));
}
/**
* 记录请求日志
*
@ -277,15 +290,16 @@ public class ApiGateway extends Controller implements Initialization {
Record record = EntityHelper.forNew(EntityHelper.RebuildApiRequest, UserService.SYSTEM_USER);
record.setString("requestUrl", apiName);
record.setString("remoteIp", remoteIp);
record.setString("responseBody", requestId + ":" + (result == null ? "{}" : CommonsUtils.maxstr(result.toJSONString(), 10000)));
record.setString("responseBody",
requestId + ":" + (result == null ? "{}" : CommonsUtils.maxstr(result.toJSONString(), 32767)));
record.setDate("requestTime", requestTime);
record.setDate("responseTime", CalendarUtils.now());
if (context != null) {
record.setString("appId", context.getAppId());
if (context.getPostData() != null) {
record.setString("requestBody",
CommonsUtils.maxstr(context.getPostData().toJSONString(), 10000));
JSON post;
if ((post = context.getPostData()) != null) {
record.setString("requestBody", CommonsUtils.maxstr(post.toJSONString(), 32767));
}
if (!context.getParameterMap().isEmpty()) {
record.setString("requestUrl",

View file

@ -49,11 +49,11 @@ public class AuthTokenManager {
* 生成 Token
*
* @param user
* @param expires
* @param seconds
* @param type
* @return
*/
protected static String generateToken(ID user, int expires, String type) {
protected static String generateToken(ID user, int seconds, String type) {
// Type,User,Time,Version
String desc = String.format("%s,%s,%d,v2",
ObjectUtils.defaultIfNull(type, TYPE_ACCESS_TOKEN),
@ -61,7 +61,7 @@ public class AuthTokenManager {
System.nanoTime());
String token = EncryptUtils.toSHA1Hex(desc);
Application.getCommonsCache().put(TOKEN_PREFIX + token, desc, expires);
Application.getCommonsCache().put(TOKEN_PREFIX + token, desc, seconds);
return token;
}
@ -84,12 +84,12 @@ public class AuthTokenManager {
}
/**
* @param expires
* @param seconds
* @return
* @see #TYPE_CSRF_TOKEN
*/
public static String generateCsrfToken(int expires) {
return generateToken(null, expires, TYPE_CSRF_TOKEN);
public static String generateCsrfToken(int seconds) {
return generateToken(null, seconds, TYPE_CSRF_TOKEN);
}
/**

View file

@ -30,16 +30,16 @@ import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter;
public class LoginToken extends BaseApi {
// 基于用户限流
private static final RequestRateLimiter RRL = RateLimiters.createRateLimiter(
new int[] { 30, 60, 3600 },
new int[] { 5, 10, 100 });
private static final RequestRateLimiter RRL_4USER = RateLimiters.createRateLimiter(
new int[] { 60, 600, 3600 },
new int[] { 5, 15, 30 });
@Override
public JSON execute(ApiContext context) throws ApiInvokeException {
final String user = context.getParameterNotBlank("user");
final String password = context.getParameterNotBlank("password");
if (RRL.overLimitWhenIncremented("user:" + user)) {
if (RRL_4USER.overLimitWhenIncremented("user:" + user)) {
return formatFailure(Language.L("请求过于频繁,请稍后重试"), ApiInvokeException.ERR_FREQUENCY);
}

View file

@ -48,6 +48,7 @@ import com.rebuild.web.OnlineSessionStore;
import com.rebuild.web.RebuildWebConfigurer;
import lombok.extern.slf4j.Slf4j;
import net.sf.ehcache.CacheManager;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
@ -73,11 +74,11 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
/**
* Rebuild Version
*/
public static final String VER = "3.4.6";
public static final String VER = "3.5.0-beta1";
/**
* Rebuild Build [MAJOR]{1}[MINOR]{2}[PATCH]{2}[BUILD]{2}
*/
public static final int BUILD = 3040611;
public static final int BUILD = 3050000;
static {
// Driver for DB
@ -219,9 +220,14 @@ public class Application implements ApplicationListener<ApplicationStartedEvent>
RebuildConfiguration.set(ConfigurationItem.AppBuild, BUILD);
}
StringBuilder logConf = new StringBuilder();
// 刷新配置缓存
for (ConfigurationItem item : ConfigurationItem.values()) {
RebuildConfiguration.get(item, true);
String v = RebuildConfiguration.get(item, true);
logConf.append(StringUtils.rightPad(item.name(), 31)).append(" : ").append(v == null ? "" : v).append("\n");
}
if (log.isDebugEnabled() || Application.devMode()) {
log.info("Use RebuildConfiguration :\n----------\n{}----------", logConf);
}
// 加载自定义实体

View file

@ -147,6 +147,14 @@ public class UserContextHolder {
return false;
}
/**
* @return
* @see #replaceUser(ID)
*/
public static ID getRestoreUser() {
return CALLER_PREV.get();
}
// --
/**

View file

@ -18,7 +18,6 @@ import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.service.InternalPersistService;
import com.rebuild.core.service.query.QueryHelper;
import com.rebuild.core.support.CommonsLock;
import com.rebuild.core.support.i18n.Language;
import org.apache.commons.lang3.ObjectUtils;
@ -43,11 +42,6 @@ public abstract class BaseConfigurationService extends InternalPersistService {
@Override
public Record update(Record record) {
ID locked = hasLock() ? CommonsLock.getLockedUser(record.getPrimary()) : null;
if (locked != null && !locked.equals(UserContextHolder.getUser())) {
throw new DataSpecificationException(Language.L("操作失败 (已被锁定)"));
}
throwIfNotSelf(record.getPrimary());
cleanCache(record.getPrimary());
return super.update(putCreateBy4ShareTo(record));
@ -55,11 +49,6 @@ public abstract class BaseConfigurationService extends InternalPersistService {
@Override
public int delete(ID recordId) {
ID locked = hasLock() ? CommonsLock.getLockedUser(recordId) : null;
if (locked != null && !locked.equals(UserContextHolder.getUser())) {
throw new DataSpecificationException(Language.L("操作失败 (已被锁定)"));
}
throwIfNotSelf(recordId);
cleanCache(recordId);
return super.delete(recordId);

View file

@ -15,6 +15,7 @@ import com.rebuild.utils.JSONable;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -120,7 +121,7 @@ public class ConfigBean implements Serializable, Cloneable, JSONable {
/**
* @return
*/
public Map<String, Object> toMap() {
return data;
public Map<String, Object> getRawData() {
return Collections.unmodifiableMap(data);
}
}

View file

@ -182,6 +182,16 @@ public class AutoFillinManager implements ConfigManager {
return fillin;
}
/**
* 表单后端回填
*
* @param record
* @return
*/
public int fillinRecord(Record record) {
return fillinRecord(record, false);
}
/**
* 表单后端回填
*

View file

@ -71,6 +71,8 @@ public class FormsBuilder extends FormsManager {
// 分割线
public static final String DIVIDER_LINE = "$DIVIDER$";
// 引用
public static final String REFFORM_LINE = "$REFFORM$";
// 引用主记录
public static final String DV_MAINID = "$MAINID$";
@ -121,7 +123,7 @@ public class FormsBuilder extends FormsManager {
final Entity entityMeta = MetadataHelper.getEntity(entity);
if (record != null) {
Assert.isTrue(entityMeta.getEntityCode().equals(record.getEntityCode()), "[entity] and [record] do not match");
Assert.isTrue(entityMeta.getEntityCode().equals(record.getEntityCode()), "[entity] and [record] do not matchs");
if (MetadataHelper.isBizzEntity(entityMeta) && !UserFilters.allowAccessBizz(user, record)) {
return formatModelError(Language.L("无权读取此记录或记录已被删除"));
@ -290,13 +292,12 @@ public class FormsBuilder extends FormsManager {
return RobotApprovalManager.instance.hadApproval(entity, null);
}
// 普通实体
// 普通实体非明细
if (entity.getMainEntity() == null) {
return RobotApprovalManager.instance.hadApproval(entity, recordId);
}
// 明细实体
ID mainid = FormsBuilderContextHolder.getMainIdOfDetail(false);
if (mainid == null) {
Field dtmField = MetadataHelper.getDetailToMainField(entity);
@ -335,6 +336,7 @@ public class FormsBuilder extends FormsManager {
JSONObject el = (JSONObject) iter.next();
String fieldName = el.getString("field");
if (DIVIDER_LINE.equalsIgnoreCase(fieldName)) continue;
if (REFFORM_LINE.equalsIgnoreCase(fieldName)) continue;
// 已删除字段
if (!MetadataHelper.checkAndWarnField(entity, fieldName)) {
@ -343,12 +345,13 @@ public class FormsBuilder extends FormsManager {
}
// v2.2 高级控制
Object displayOnCreate = el.remove("displayOnCreate");
Object displayOnUpdate = el.remove("displayOnUpdate");
Object requiredOnCreate = el.remove("requiredOnCreate");
Object requiredOnUpdate = el.remove("requiredOnUpdate");
if (viewModel) useAdvControl = false;
if (useAdvControl) {
Object displayOnCreate = el.remove("displayOnCreate");
Object displayOnUpdate = el.remove("displayOnUpdate");
Object requiredOnCreate = el.remove("requiredOnCreate");
Object requiredOnUpdate = el.remove("requiredOnUpdate");
// fix v3.3.4 跟随主记录新建/更新
boolean isNew2 = isNew;
if (entity.getMainEntity() != null) {
@ -650,14 +653,10 @@ public class FormsBuilder extends FormsManager {
* @param initialVal 此值优先级大于字段默认值
*/
public void setFormInitialValue(Entity entity, JSON formModel, JSONObject initialVal) {
if (initialVal == null || initialVal.isEmpty()) {
return;
}
if (initialVal == null || initialVal.isEmpty()) return;
JSONArray elements = ((JSONObject) formModel).getJSONArray("elements");
if (elements == null || elements.isEmpty()) {
return;
}
if (elements == null || elements.isEmpty()) return;
// 已布局字段字段是否布局会影响返回值
Set<String> inFormFields = new HashSet<>();
@ -702,7 +701,7 @@ public class FormsBuilder extends FormsManager {
// 其他
else if (entity.containsField(field)) {
EasyField easyField = EasyMetaFactory.valueOf(entity.getField(field));
if (easyField.getDisplayType() == DisplayType.REFERENCE) {
if (easyField.getDisplayType() == DisplayType.REFERENCE || easyField.getDisplayType() == DisplayType.N2NREFERENCE) {
// v3.4 如果字段设置了附加过滤条件从相关项新建时要检查是否符合
String dataFilter = easyField.getExtraAttr(EasyFieldConfigProps.REFERENCE_DATAFILTER);
@ -720,9 +719,16 @@ public class FormsBuilder extends FormsManager {
Object mixValue = inFormFields.contains(field) ? getReferenceMixValue(value) : value;
if (mixValue != null) {
initialValReady.put(field, mixValue);
if (easyField.getDisplayType() == DisplayType.REFERENCE) {
initialValReady.put(field, mixValue);
} else {
// N2N 是数组
initialValReady.put(field,
inFormFields.contains(field) ? new Object[] { mixValue } : value);
}
}
}
} else {
log.warn("Unknown value pair : " + field + " = " + value);
}

View file

@ -17,6 +17,8 @@ import com.rebuild.utils.JSONUtils;
/**
* 轻量级表单
* Issue1. 针对自动只读字段无效
* Issue2. 表单高级控制无效
*
* @author devezhao
* @since 2022/12/28

View file

@ -20,11 +20,11 @@ import com.rebuild.core.UserContextHolder;
import com.rebuild.core.configuration.BaseConfigurationService;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.AdminGuard;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
@ -114,7 +114,7 @@ public class PickListService extends BaseConfigurationService implements AdminGu
r.setString("text", item.getString("text"));
r.setBoolean("isHide", false);
r.setBoolean("isDefault", item.getBoolean("default"));
r.setString("color", StringUtils.defaultString(item.getString("color"), ""));
r.setString("color", Objects.toString(item.getString("color"), ""));
if (id2id == null) {
r.setString("belongEntity", field.getOwnEntity().getName());
r.setString("belongField", field.getName());

View file

@ -21,11 +21,14 @@ import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.MetadataSorter;
import com.rebuild.core.metadata.easymeta.EasyEntity;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyEntityConfigProps;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@ -79,9 +82,11 @@ public class ViewAddonsManager extends BaseLayoutManager {
if (ifMain.getDetailEntity() != null) {
JSONArray tabsFluent = new JSONArray();
for (Entity de : MetadataSorter.sortDetailEntities(ifMain)) {
JSONObject deJson = EasyMetaFactory.toJSON(de);
EasyEntity deEasy = EasyMetaFactory.valueOf(de);
JSONObject deJson = (JSONObject) deEasy.toJSON();
deJson.put("entity", de.getName() + "." + MetadataHelper.getDetailToMainField(de).getName());
deJson.put("_showAtBottom", false);
// 显示位置
deJson.put("showAt2", BooleanUtils.toBoolean(deEasy.getExtraAttr(EasyEntityConfigProps.DETAILS_SHOWAT2)) ? 2 : 1);
tabsFluent.add(deJson);
}
@ -158,9 +163,9 @@ public class ViewAddonsManager extends BaseLayoutManager {
if (!MetadataHelper.isBusinessEntity(e)) continue;
if (ArrayUtils.contains(entityMeta.getDetialEntities(), e)) continue;
// 新建项无明细多引用
// 新建项排除明细
if (TYPE_ADD.equals(applyType)) {
if (e.getMainEntity() != null || field.getType() != FieldType.REFERENCE) continue;
if (e.getMainEntity() != null) continue;
}
Entity eCheck = ObjectUtils.defaultIfNull(e.getMainEntity(), e);

View file

@ -16,6 +16,8 @@ import cn.devezhao.persist4j.record.FieldValueException;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.general.AutoFillinManager;
import com.rebuild.core.privileges.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
@ -26,6 +28,7 @@ import java.util.Date;
* @see MetadataHelper
* @since 1.0, 2018-6-26
*/
@Slf4j
public class EntityHelper {
// 虚拟 ID 后缀
@ -34,19 +37,31 @@ public class EntityHelper {
public static final ID UNSAVED_ID = ID.valueOf("000" + UNSAVED_ID_SUFFIX);
/**
* 解析 JSON Record
* 解析 JSON Record
*
* @param data
* @return
* @see #parse(JSONObject, ID, boolean, boolean)
*/
public static Record parse(JSONObject data) {
log.info("Use SYSTEM_USER do parse");
return parse(data, UserService.SYSTEM_USER, true, false);
}
/**
* 解析 JSON Record
*
* @param data
* @param user
* @return
* @see EntityRecordCreator
* @see #parse(JSONObject, ID, boolean, boolean)
*/
public static Record parse(JSONObject data, ID user) {
return parse(data, user, true, false);
}
/**
* 解析 JSON Record
* 解析 JSON Record
*
* @param data
* @param user
@ -88,12 +103,25 @@ public class EntityHelper {
return record;
}
/**
* 构建更新 Record
*
* @param recordId
* @return
* @see #forUpdate(ID, ID, boolean)
*/
public static Record forUpdate(ID recordId) {
log.info("Use SYSTEM_USER do forUpdate");
return forUpdate(recordId, UserService.SYSTEM_USER, true);
}
/**
* 构建更新 Record
*
* @param recordId
* @param user
* @return
* @see #forUpdate(ID, ID, boolean)
*/
public static Record forUpdate(ID recordId, ID user) {
return forUpdate(recordId, user, true);
@ -120,12 +148,25 @@ public class EntityHelper {
return record;
}
/**
* 构建新建 Record
*
* @param entity
* @return
* @see #forNew(int, ID, boolean)
*/
public static Record forNew(int entity) {
log.info("Use SYSTEM_USER do forNew");
return forNew(entity, UserService.SYSTEM_USER, true);
}
/**
* 构建新建 Record
*
* @param entity
* @param user
* @return
* @see #forNew(int, ID, boolean)
*/
public static Record forNew(int entity, ID user) {
return forNew(entity, user, true);
@ -136,24 +177,14 @@ public class EntityHelper {
*
* @param entity
* @param user
* @param bindCommons
* @return
*/
public static Record forNew(int entity, ID user, boolean bindCommons) {
return forNew(MetadataHelper.getEntity(entity), user, bindCommons);
}
/**
* 构建新建 Record
*
* @param entity
* @param user
* @return
*/
private static Record forNew(Entity entity, ID user, boolean bindCommons) {
Assert.notNull(entity, "[entity] cannot be null");
Assert.isTrue(MetadataHelper.containsEntity(entity), "[entity] does not exists : " + entity);
Assert.notNull(user, "[user] cannot be null");
Record record = new StandardRecord(entity, user);
Record record = new StandardRecord(MetadataHelper.getEntity(entity), user);
if (bindCommons) {
bindCommonsFieldsValue(record, true);
}

View file

@ -68,7 +68,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
@Override
public boolean onSetFieldValueWarn(Field field, String value, Record record) {
// 非业务实体
if (MetadataHelper.isBusinessEntity(field.getOwnEntity())) return true;
if (!MetadataHelper.isBusinessEntity(field.getOwnEntity())) return true;
final boolean isNew = record.getPrimary() == null;
@ -76,15 +76,15 @@ public class EntityRecordCreator extends JsonRecordCreator {
if (isNew && isDtmField(field)) return true;
// 公共字段前台可能会布局出来
// 此处忽略检查没问题因为最后还会复写 #bindCommonsFieldsValue
// 此处忽略检查没问题因为最后还会复写 EntityHelper#bindCommonsFieldsValue
boolean isCommonField = MetadataHelper.isCommonsField(field);
if (!isCommonField) return false;
String fieldName = field.getName();
return isNew || (!EntityHelper.OwningUser.equalsIgnoreCase(fieldName)
&& !EntityHelper.OwningDept.equalsIgnoreCase(fieldName)
&& !EntityHelper.CreatedBy.equalsIgnoreCase(fieldName)
&& !EntityHelper.CreatedOn.equalsIgnoreCase(fieldName));
if (isNew) return true;
String n = field.getName();
return !(EntityHelper.OwningUser.equalsIgnoreCase(n) || EntityHelper.OwningDept.equalsIgnoreCase(n)
|| EntityHelper.CreatedBy.equalsIgnoreCase(n) || EntityHelper.CreatedOn.equalsIgnoreCase(n));
}
@Override
@ -172,7 +172,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
if (!notNulls.isEmpty()) {
throw new DataSpecificationException(
Language.L("%s 不允许为空", StringUtils.join(notNulls, " / ")));
Language.L("%s 不为空", StringUtils.join(notNulls, " / ")));
}
if (!notWells.isEmpty()) {
throw new DataSpecificationException(
@ -212,6 +212,7 @@ public class EntityRecordCreator extends JsonRecordCreator {
return patt == null || patt.matcher((CharSequence) val).matches();
}
// 是否需要去除外链
private void keepFieldValueSafe(Record record) {
for (String fieldName : record.getAvailableFields()) {
final Object value = record.getObjectValue(fieldName);

View file

@ -111,7 +111,8 @@ public class MetadataSorter {
List<BaseMeta> entities = new ArrayList<>();
CollectionUtils.addAll(entities, mainEntity.getDetialEntities());
sortByLabel(entities);
// SORT: 名称默认是返回按CODE大小
if (entities.size() > 1) sortByLabel(entities);
return entities.toArray(new Entity[0]);
}

View file

@ -40,4 +40,9 @@ public class EasyBarCode extends EasyField {
if (value != null) log.warn("Cannot wrap value of EasyBarCode : " + value);
return null;
}
@Override
public Object exprDefaultValue() {
return null;
}
}

View file

@ -117,6 +117,6 @@ public class EasyDecimal extends EasyField {
*/
public static String clearFlaged(Object flagedValue) {
if (flagedValue == null) return null;
return flagedValue.toString().replaceAll("[^\\d^.^-]", "");
return flagedValue.toString().replaceAll("[^\\d.-]", "");
}
}

View file

@ -37,4 +37,9 @@ public class EasyFile extends EasyField {
if (value instanceof JSONArray) return value;
return JSON.parseArray(value.toString());
}
@Override
public Object exprDefaultValue() {
return null;
}
}

View file

@ -30,4 +30,9 @@ public class EasySeries extends EasyField {
throw new UnsupportedOperationException();
}
@Override
public Object exprDefaultValue() {
return null;
}
}

View file

@ -28,4 +28,5 @@ public class EasySign extends EasyField {
Assert.isTrue(targetField.getDisplayType() == getDisplayType(), "type-by-type is must");
return value;
}
}

View file

@ -17,9 +17,9 @@ import com.rebuild.core.rbstore.MetaSchemaGenerator;
import com.rebuild.core.rbstore.MetaschemaImporter;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.core.support.task.TaskExecutors;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.RbAssert;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.RandomUtils;
/**
* 复制实体
@ -83,7 +83,7 @@ public class CopyEntity extends Entity2Schema {
String uniqueEntityName = toPinyinName(entityName);
for (int i = 0; i < 6; i++) {
if (MetadataHelper.containsEntity(uniqueEntityName)) {
uniqueEntityName += RandomUtils.nextInt(0, 9);
uniqueEntityName += CommonsUtils.randomInt(0, 9);
} else {
break;
}

View file

@ -29,6 +29,10 @@ public class EasyEntityConfigProps {
* 明细重复判断模式为全部数据否则为主记录下的
*/
public static final String DETAILS_GLOBALREPEAT = "detailsGlobalRepeat";
/**
* 明细显示位置
*/
public static final String DETAILS_SHOWAT2 = "detailsShowAt2";
/**
* 隐藏常用查询面板

View file

@ -25,9 +25,9 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.support.License;
import com.rebuild.core.support.NeedRbvException;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.RandomUtils;
/**
* 创建实体
@ -74,7 +74,7 @@ public class Entity2Schema extends Field2Schema {
entityName = toPinyinName(entityLabel);
for (int i = 0; i < 6; i++) {
if (MetadataHelper.containsEntity(entityName)) {
entityName += RandomUtils.nextInt(0, 9);
entityName += CommonsUtils.randomInt(0, 9);
} else {
break;
}

View file

@ -37,7 +37,6 @@ import com.rebuild.utils.RbAssert;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.CharSet;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.RandomUtils;
import java.text.MessageFormat;
import java.util.Collection;
@ -109,7 +108,7 @@ public class Field2Schema extends SetUser {
for (int i = 0; i < 6; i++) {
if (entity.containsField(fieldName) || MetadataHelper.isCommonsField(fieldName)) {
fieldName += RandomUtils.nextInt(1, 99);
fieldName += CommonsUtils.randomInt(1, 99);
} else {
break;
}
@ -396,7 +395,7 @@ public class Field2Schema extends SetUser {
identifier = HanLP.convertToPinyinString(identifier, "", false);
identifier = identifier.replaceAll("[^a-zA-Z0-9]", "");
if (StringUtils.isBlank(identifier)) {
identifier = "rb" + RandomUtils.nextInt(1000, 9999);
identifier = "rb" + CommonsUtils.randomInt(1000, 9999);
}
if (!CharSet.ASCII_ALPHA.contains(identifier.charAt(0))) {
@ -407,7 +406,7 @@ public class Field2Schema extends SetUser {
if (identifier.length() > 40) {
identifier = identifier.substring(0, 40);
} else if (identifier.length() < 4) {
identifier += RandomUtils.nextInt(1000, 9999);
identifier += CommonsUtils.randomInt(1000, 9999);
}
return identifier;

View file

@ -19,6 +19,7 @@ import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.privileges.bizz.InternalPermission;
import com.rebuild.core.service.CommonsService;
import com.rebuild.core.service.general.BulkContext;
import com.rebuild.core.service.general.EntityService;
@ -190,7 +191,7 @@ public class PrivilegesGuardInterceptor implements MethodInterceptor, Guard {
} else if (action.startsWith("share")) {
return BizzPermission.SHARE;
} else if (action.startsWith("unshare")) {
return EntityService.UNSHARE;
return InternalPermission.UNSHARE;
}
throw new PrivilegesException("No such Permission found : " + action);
}
@ -213,8 +214,10 @@ public class PrivilegesGuardInterceptor implements MethodInterceptor, Guard {
actionHuman = Language.L("分配");
} else if (action == BizzPermission.SHARE) {
actionHuman = Language.L("共享");
} else if (action == EntityService.UNSHARE) {
} else if (action == InternalPermission.UNSHARE) {
actionHuman = Language.L("取消共享");
} else if (action == InternalPermission.APPROVAL) {
actionHuman = Language.L("审批");
}
if (target == null) {

View file

@ -30,7 +30,6 @@ import com.rebuild.core.privileges.bizz.User;
import com.rebuild.core.privileges.bizz.ZeroEntry;
import com.rebuild.core.privileges.bizz.ZeroPrivileges;
import com.rebuild.core.service.NoRecordFoundException;
import com.rebuild.core.service.general.EntityService;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
@ -220,10 +219,11 @@ public class PrivilegesManager {
return true;
}
// FIXME v35 批量审批无权限
if (action == InternalPermission.APPROVAL) return true;
Boolean a = userAllow(user);
if (a != null) {
return a;
}
if (a != null) return a;
Role role = theUserStore.getUser(user).getOwningRole();
if (RoleService.ADMIN_ROLE.equals(role.getIdentity())) {
@ -233,7 +233,7 @@ public class PrivilegesManager {
}
// 取消共享与共享共用权限
if (action == EntityService.UNSHARE) {
if (action == InternalPermission.UNSHARE) {
action = BizzPermission.SHARE;
}
@ -285,9 +285,7 @@ public class PrivilegesManager {
}
Boolean a = userAllow(user);
if (a != null) {
return a;
}
if (a != null) return a;
Role role = theUserStore.getUser(user).getOwningRole();
if (RoleService.ADMIN_ROLE.equals(role.getIdentity())) {
@ -305,7 +303,7 @@ public class PrivilegesManager {
}
// 取消共享与共享共用权限
if (action == EntityService.UNSHARE) {
if (action == InternalPermission.UNSHARE) {
action = BizzPermission.SHARE;
}
@ -492,9 +490,7 @@ public class PrivilegesManager {
*/
public boolean allow(ID user, ZeroEntry entry) {
Boolean a = userAllow(user);
if (a != null) {
return a;
}
if (a != null) return a;
Role role = theUserStore.getUser(user).getOwningRole();
if (RoleService.ADMIN_ROLE.equals(role.getIdentity())) {
@ -543,7 +539,7 @@ public class PrivilegesManager {
BizzPermission.READ,
BizzPermission.ASSIGN,
BizzPermission.SHARE,
EntityService.UNSHARE
InternalPermission.UNSHARE
};
/**

View file

@ -132,9 +132,14 @@ public class UserService extends BaseService {
String dsql = String.format("delete from `layout_config` where `CREATED_BY` = '%s'", recordId);
Application.getSqlExecutor().execute(dsql);
// 2.删除并刷新缓存
// 2.删除三方
String dsql2 = String.format("delete from `external_user` where `BIND_USER` = '%s'", recordId);
Application.getSqlExecutor().execute(dsql2);
// 3.删除并刷新缓存
super.delete(recordId);
Application.getUserStore().removeUser(recordId);
return 1;
}
@ -214,9 +219,7 @@ public class UserService extends BaseService {
}
int policy = RebuildConfiguration.getInt(ConfigurationItem.PasswordPolicy);
if (policy <= 1) {
return;
}
if (policy <= 1) return;
int countUpper = 0;
int countLower = 0;
@ -237,8 +240,8 @@ public class UserService extends BaseService {
if (countUpper == 0 || countLower == 0 || countDigit == 0) {
throw new DataSpecificationException(Language.L("密码不能小于 6 位,且必须包含数字和大小写字母"));
}
if (policy >= 3 && (countSpecial == 0 || password.length() < 8)) {
throw new DataSpecificationException(Language.L("密码不能小于 8 位,且必须包含数字和大小写字母及特殊字符"));
if (policy >= 3 && (countSpecial == 0 || password.length() < 10)) {
throw new DataSpecificationException(Language.L("密码不能小于 10 位,且必须包含数字和大小写字母及特殊字符"));
}
}
@ -489,13 +492,6 @@ public class UserService extends BaseService {
"select user from LoginLog where user = ?")
.setParameter(1, user)
.unique();
if (hasLogin != null) return true;
// 绑定
Object[] hasBind = Application.createQueryNoFilter(
"select bindUser from ExternalUser where bindUser = ?")
.setParameter(1, user)
.unique();
return hasBind != null;
return hasLogin != null;
}
}

View file

@ -32,4 +32,11 @@ public class InternalPermission {
*/
public static final Permission APPROVAL = new BizzPermission("APPROVAL", 0, false);
/**
* 取消共享跟随共享权限
*
* @see BizzPermission
*/
public static final Permission UNSHARE = new BizzPermission("UNSHARE", 1 << 6, true);
}

View file

@ -24,7 +24,6 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.easymeta.EasyTag;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.general.QuickCodeReindexTask;
import com.rebuild.utils.Callable2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
@ -147,33 +146,38 @@ public class BaseService extends InternalPersistService {
final Map<String, ID[]> holdN2NValues = new HashMap<>();
for (Field n2nField : n2nFields) {
ID[] newRefs;
ID[] newValue;
if (isNew) {
Object maybeNull = record.getObjectValue(n2nField.getName());
newRefs = NullValue.is(maybeNull) ? ID.EMPTY_ID_ARRAY : (ID[]) maybeNull;
if (newRefs == null || newRefs.length == 0) continue;
if (maybeNull == null) continue;
newValue = NullValue.is(maybeNull) ? ID.EMPTY_ID_ARRAY : (ID[]) maybeNull;
if (newValue.length == 0) {
record.setNull(n2nField.getName());
continue;
}
} else {
if (record.hasValue(n2nField.getName())) {
Object maybeNull = record.getObjectValue(n2nField.getName());
newRefs = NullValue.is(maybeNull) ? ID.EMPTY_ID_ARRAY : (ID[]) maybeNull;
newValue = NullValue.is(maybeNull) ? ID.EMPTY_ID_ARRAY : (ID[]) maybeNull;
} else {
continue;
}
}
// 保持值
holdN2NValues.put(n2nField.getName(), newRefs);
holdN2NValues.put(n2nField.getName(), newValue);
// 仅保留第一个用于标识是否为空
if (newRefs.length == 0) record.setNull(n2nField.getName());
else record.setIDArray(n2nField.getName(), new ID[] { newRefs[0] });
if (newValue.length == 0) record.setNull(n2nField.getName());
else record.setIDArray(n2nField.getName(), new ID[] { newValue[0] });
// 哪个字段
n2nRecord.setString("belongField", n2nField.getName());
// 新建
if (isNew) {
for (ID refId : newRefs) {
for (ID refId : newValue) {
Record clone = n2nRecord.clone();
clone.setID("referenceId", refId);
addItems.add(clone);
@ -193,7 +197,7 @@ public class BaseService extends InternalPersistService {
}
Set<ID> afterRefs = new LinkedHashSet<>();
CollectionUtils.addAll(afterRefs, newRefs);
CollectionUtils.addAll(afterRefs, newValue);
for (Iterator<ID> iter = afterRefs.iterator(); iter.hasNext(); ) {
ID a = iter.next();
@ -278,31 +282,37 @@ public class BaseService extends InternalPersistService {
final Map<String, String[]> holdTagValues = new HashMap<>();
for (Field tagField : tagFields) {
String[] newTags;
String[] newValue;
if (isNew) {
newTags = cleanNameArray(record.getObjectValue(tagField.getName()));
if (newTags.length == 0) continue;
Object maybeNull = record.getObjectValue(tagField.getName());
if (maybeNull == null) continue;
newValue = cleanNameArray(maybeNull);
if (newValue.length == 0) {
record.setNull(tagField.getName());
continue;
}
} else {
if (record.hasValue(tagField.getName())) {
newTags = cleanNameArray(record.getObjectValue(tagField.getName()));
newValue = cleanNameArray(record.getObjectValue(tagField.getName()));
} else {
continue;
}
}
// 保持值
holdTagValues.put(tagField.getName(), newTags);
holdTagValues.put(tagField.getName(), newValue);
// 仅保留第一个用于标识是否为空
if (newTags.length == 0) record.setNull(tagField.getName());
else record.setString(tagField.getName(), newTags[0]);
if (newValue.length == 0) record.setNull(tagField.getName());
else record.setString(tagField.getName(), newValue[0]);
// 哪个字段
tagRecord.setString("belongField", tagField.getName());
// 新建
if (isNew) {
for (String tagName : newTags) {
for (String tagName : newValue) {
Record clone = tagRecord.clone();
clone.setString("tagName", tagName);
addItems.add(clone);
@ -322,7 +332,7 @@ public class BaseService extends InternalPersistService {
}
Set<String> afterTags = new LinkedHashSet<>();
CollectionUtils.addAll(afterTags, newTags);
CollectionUtils.addAll(afterTags, newValue);
for (Iterator<String> iter = afterTags.iterator(); iter.hasNext(); ) {
String a = iter.next();

View file

@ -5,7 +5,7 @@ rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.utils;
package com.rebuild.core.service;
/**
* @author devezhao

View file

@ -25,7 +25,7 @@ import org.springframework.util.Assert;
* <br>- 有权限的实体使用此类需要指定 <tt>strictMode=false</tt>
*
* @author Zixin (RB)
* @since 11/06/2017
* @since 11/06/2019
*/
@Service
public class CommonsService extends InternalPersistService {

View file

@ -16,7 +16,7 @@ import com.rebuild.core.Application;
* 持久化服务
*
* @author Zixin (RB)
* @since 05/21/2017
* @since 05/21/2019
*/
public abstract class InternalPersistService implements ServiceSpec {

View file

@ -13,6 +13,7 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* 手动事物管理默认事务管理见 `application-bean.xml`
@ -27,6 +28,8 @@ public class TransactionManual {
* 开启一个事物
*
* @return
* @see #commit(TransactionStatus)
* @see #rollback(TransactionStatus)
*/
public static TransactionStatus newTransaction() {
DefaultTransactionAttribute attr = new DefaultTransactionAttribute();
@ -34,15 +37,6 @@ public class TransactionManual {
return getTxManager().getTransaction(attr);
}
/**
* Shadow for TransactionAspectSupport#currentTransactionStatus
*
* @return
*/
public static TransactionStatus currentTransaction() {
return TransactionAspectSupport.currentTransactionStatus();
}
/**
* 提交
*
@ -72,4 +66,21 @@ public class TransactionManual {
return Application.getBean(DataSourceTransactionManager.class);
}
/**
* Shadow for <tt>TransactionAspectSupport#currentTransactionStatus</tt>
*
* @return
*/
public static TransactionStatus currentTransactionStatus() {
return TransactionAspectSupport.currentTransactionStatus();
}
/**
* Shadow for <tt>TransactionSynchronizationManager.getCurrentTransactionName</tt>
*
* @return
*/
public static String currentTransactionName() {
return TransactionSynchronizationManager.getCurrentTransactionName();
}
}

View file

@ -64,7 +64,7 @@ public class ApprovalHelper {
Object[] o = Application.getQueryFactory().uniqueNoFilter(recordId,
EntityHelper.ApprovalId, EntityHelper.ApprovalId + ".name", EntityHelper.ApprovalState, EntityHelper.ApprovalStepNode);
if (o == null) {
throw new NoRecordFoundException(recordId, Boolean.TRUE);
throw new NoRecordFoundException(recordId, true);
}
return new ApprovalStatus((ID) o[0], (String) o[1], (Integer) o[2], (String) o[3], recordId);
}

View file

@ -11,6 +11,7 @@ import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
@ -122,7 +123,7 @@ public class ApprovalProcessor extends SetUser {
* @throws ApprovalException
*/
public void approve(ID approver, ApprovalState state, String remark, JSONObject selectNextUsers) throws ApprovalException {
approve(approver, state, remark, selectNextUsers, null, null, null);
approve(approver, state, remark, selectNextUsers, null, null, null, false);
}
/**
@ -135,13 +136,14 @@ public class ApprovalProcessor extends SetUser {
* @param addedData
* @param checkUseGroup
* @param rejectNode
* @param batchMode
* @throws ApprovalException
*/
public void approve(ID approver, ApprovalState state, String remark, JSONObject selectNextUsers, Record addedData, String checkUseGroup, String rejectNode) throws ApprovalException {
public void approve(ID approver, ApprovalState state, String remark, JSONObject selectNextUsers, Record addedData, String checkUseGroup, String rejectNode, boolean batchMode) throws ApprovalException {
final ApprovalStatus status = checkApprovalState(ApprovalState.PROCESSING);
final Object[] stepApprover = Application.createQueryNoFilter(
"select stepId,state,node,approvalId from RobotApprovalStep where recordId = ? and approver = ? and node = ? and isCanceled = 'F' order by createdOn desc")
"select stepId,state,node,approvalId,attrMore from RobotApprovalStep where recordId = ? and approver = ? and node = ? and isCanceled = 'F' order by createdOn desc")
.setParameter(1, this.record)
.setParameter(2, approver)
.setParameter(3, getCurrentNodeId(status))
@ -159,6 +161,13 @@ public class ApprovalProcessor extends SetUser {
approvedStep.setString("remark", remark);
}
if (batchMode) {
JSONObject attrMore = JSONUtils.wellFormat((String) stepApprover[4])
? JSON.parseObject((String) stepApprover[4]) : new JSONObject();
attrMore.put("batchMode", true);
approvedStep.setString("attrMore", attrMore.toJSONString());
}
this.approval = (ID) stepApprover[3];
FlowNodeGroup nextNodes = getNextNodes((String) stepApprover[2]);
@ -575,6 +584,9 @@ public class ApprovalProcessor extends SetUser {
// 加签
String countersignFrom = attrMored.getString("countersignFrom");
s.put("countersignFrom", ID.isId(countersignFrom) ? UserHelper.getName(ID.valueOf(countersignFrom)) : null);
// 批量
String batchMode = attrMored.getString("batchMode");
s.put("batchMode", batchMode != null);
}
return s;

View file

@ -265,6 +265,10 @@ public class ApprovalStepService extends InternalPersistService {
if (goNextNode && (nextApprovers == null || nextNode == null)) {
super.update(recordOfMain);
String approvedMsg = Language.L("你提交的 %s 已审批通过", entityLabel);
if (StringUtils.isNotBlank(remark)) approvedMsg += "\n > " + remark;
sendNotification(submitter, approvedMsg, recordId);
Application.getEntityService(recordId.getEntityCode()).approve(recordId, ApprovalState.APPROVED, approver);
return;
}
@ -460,6 +464,8 @@ public class ApprovalStepService extends InternalPersistService {
setApprovalLastX(recordOfMain, useApprover, Language.L("自动审批"));
super.update(recordOfMain);
// NOTE 自动审批不会给提交人发通知
Application.getEntityService(recordId.getEntityCode()).approve(recordId, ApprovalState.APPROVED, useApprover);
return true;
}

View file

@ -21,7 +21,11 @@ import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang.StringUtils;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 流程节点包括审批抄送
@ -148,6 +152,14 @@ public class FlowNode {
return b != null && b;
}
/**
* @return
*/
public boolean allowBatch() {
Boolean b = getDataMap().getBoolean("allowBatch");
return b != null && b;
}
/**
* 获取相关人员提交人/审批人/抄送人
*

View file

@ -19,6 +19,8 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.service.query.QueryHelper;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@ -29,6 +31,7 @@ import java.util.List;
* @author devezhao zhaofang123@gmail.com
* @since 2019/06/24
*/
@Slf4j
public class RobotApprovalManager implements ConfigManager {
public static final RobotApprovalManager instance = new RobotApprovalManager();
@ -42,16 +45,22 @@ public class RobotApprovalManager implements ConfigManager {
* 获取实体/记录流程状态
*
* @param entity
* @param record
* @param recordId
* @return <tt>null</tt> 表示没有流程
*/
public ApprovalState hadApproval(Entity entity, ID record) {
public ApprovalState hadApproval(Entity entity, ID recordId) {
if (entity.getMainEntity() != null || !MetadataHelper.hasApprovalField(entity)) return null;
// 记录的
if (record != null) {
Object[] o = Application.getQueryFactory().unique(
record, EntityHelper.ApprovalId, EntityHelper.ApprovalState);
if (recordId != null) {
if (!recordId.getEntityCode().equals(entity.getEntityCode())) {
log.warn("Entity and Record/ID mismatch : {}, {}", entity, recordId);
CommonsUtils.printStackTrace();
return null;
}
Object[] o = Application.getQueryFactory().uniqueNoFilter(
recordId, EntityHelper.ApprovalId, EntityHelper.ApprovalState);
if (o != null && o[0] != null) {
return (ApprovalState) ApprovalState.valueOf((Integer) o[1]);
}
@ -103,17 +112,17 @@ public class RobotApprovalManager implements ConfigManager {
/**
* 获取用户可用流程
*
* @param record
* @param recordId
* @param user
* @return
*/
public FlowDefinition[] getFlowDefinitions(ID record, ID user) {
FlowDefinition[] defs = getFlowDefinitions(MetadataHelper.getEntity(record.getEntityCode()));
public FlowDefinition[] getFlowDefinitions(ID recordId, ID user) {
FlowDefinition[] defs = getFlowDefinitions(MetadataHelper.getEntity(recordId.getEntityCode()));
if (defs.length == 0) {
return new FlowDefinition[0];
}
ID owning = Application.getRecordOwningCache().getOwningUser(record);
ID owning = Application.getRecordOwningCache().getOwningUser(recordId);
// 过滤可用的
List<FlowDefinition> workable = new ArrayList<>();
for (FlowDefinition def : defs) {
@ -130,11 +139,11 @@ public class RobotApprovalManager implements ConfigManager {
if (FlowNode.USER_ALL.equals(users.getString(0))
|| (FlowNode.USER_OWNS.equals(users.getString(0)) && owning.equals(user))
|| UserHelper.parseUsers(users, record).contains(user)) {
|| UserHelper.parseUsers(users, recordId).contains(user)) {
// 过滤条件
JSONObject filter = root.getDataMap().getJSONObject("filter");
if (QueryHelper.isMatchAdvFilter(record, filter)) {
if (QueryHelper.isMatchAdvFilter(recordId, filter)) {
workable.add(def);
}
}

View file

@ -62,9 +62,9 @@ import java.util.List;
public class DataExporter extends SetUser {
/**
* 最大行数
* 最大导出行数
*/
public static final int MAX_ROWS = 65535 - 1;
public static final int MAX_ROWS = 1000000;
final private JSONObject queryData;
// 字段

View file

@ -96,7 +96,13 @@ public class DataImporter extends HeavyTask<Integer> {
traceLogs.add(new Object[] { firstCell.getRowNo(), "SKIP" });
} else {
boolean isNew = record.getPrimary() == null;
final boolean isNew = record.getPrimary() == null;
if (isNew && rule.getRepeatOpt() == ImportRule.REPEAT_OPT_UPDATE && rule.isOnlyUpdate()) {
traceLogs.add(new Object[] { firstCell.getRowNo(), "SKIP" });
continue;
}
if (!isViaAdmin) {
String error = null;
if (isNew) {

View file

@ -13,8 +13,7 @@ import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.support.RebuildConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.io.File;
@ -31,10 +30,9 @@ import java.util.Set;
* @author devezhao
* @since 01/10/2019
*/
@Slf4j
public class ImportRule {
private static final Logger LOG = LoggerFactory.getLogger(ImportRule.class);
public static final int REPEAT_OPT_UPDATE = 1;
public static final int REPEAT_OPT_SKIP = 2;
public static final int REPEAT_OPT_IGNORE = 3;
@ -44,6 +42,7 @@ public class ImportRule {
private int repeatOpt;
private Field[] repeatFields;
private boolean onlyUpdate;
private ID defaultOwningUser;
@ -54,15 +53,17 @@ public class ImportRule {
* @param toEntity
* @param repeatOpt
* @param repeatFields
* @param onlyUpdate
* @param defaultOwningUser
* @param filedsMapping
*/
protected ImportRule(File sourceFile, Entity toEntity, int repeatOpt, Field[] repeatFields, ID defaultOwningUser,
ImportRule(File sourceFile, Entity toEntity, int repeatOpt, Field[] repeatFields, boolean onlyUpdate, ID defaultOwningUser,
Map<Field, Integer> filedsMapping) {
this.sourceFile = sourceFile;
this.toEntity = toEntity;
this.repeatOpt = repeatOpt;
this.repeatFields = repeatFields;
this.onlyUpdate = onlyUpdate;
this.defaultOwningUser = defaultOwningUser;
this.filedsMapping = filedsMapping;
}
@ -91,6 +92,10 @@ public class ImportRule {
return filedsMapping;
}
public boolean isOnlyUpdate() {
return onlyUpdate;
}
// --
/**
@ -119,7 +124,7 @@ public class ImportRule {
throw new IllegalArgumentException("File not found : " + file, e);
}
}
LOG.warn("Use file from TestCase : " + file);
log.warn("Use file from TestCase : " + file);
}
if (!file.exists()) {
throw new IllegalArgumentException("File not found : " + file);
@ -146,6 +151,7 @@ public class ImportRule {
filedsMapping.put(entity.getField(e.getKey()), (Integer) e.getValue());
}
return new ImportRule(file, entity, repeatOpt, repeatFields, ownUser, filedsMapping);
return new ImportRule(
file, entity, repeatOpt, repeatFields, rule.getBooleanValue("only_update"), ownUser, filedsMapping);
}
}

View file

@ -335,8 +335,7 @@ public class RecordCheckout {
while (num >= 0) {
int remainder = num % 26;
name.insert(0, (char) (remainder + 65));
//noinspection IntegerDivisionInFloatingPointContext
num = (int) Math.floor(num / 26) - 1;
num = (int) (double) (num / 26) - 1;
}
name.append(cell.getRowNo() + 1);

View file

@ -19,6 +19,7 @@ import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.ConfigManager;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.general.ContentWithFieldVars;
import com.rebuild.utils.JSONUtils;
@ -26,7 +27,9 @@ import org.apache.commons.lang.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* 数据报表
@ -42,22 +45,42 @@ public class DataReportManager implements ConfigManager {
public static final int TYPE_RECORD = 1;
public static final int TYPE_LIST = 2;
public static final int TYPE_HTML5 = 3;
public static final int TYPE_WORD = 4;
/**
* 获取表列
* 获取可用报表
*
* @param entity
* @param type
* @param type 指定类型
* @param user
* @return
*/
public JSONArray getReports(Entity entity, int type) {
JSONArray list = new JSONArray();
public JSONArray getReports(Entity entity, int type, ID user) {
JSONArray alist = new JSONArray();
for (ConfigBean e : getReportsRaw(entity)) {
if (!e.getBoolean("disabled") && e.getInteger("type") == type) {
list.add(e.toJSON("id", "name", "outputType"));
if (e.getBoolean("disabled")) continue;
boolean can;
int aType = e.getInteger("type");
if (type == DataReportManager.TYPE_LIST) {
can = aType == type;
} else {
can = aType == DataReportManager.TYPE_RECORD || aType == DataReportManager.TYPE_WORD;
}
if (can) {
// v3.5
String vuDef = e.getString("visibleUsers");
if (StringUtils.isNotBlank(vuDef)) {
Set<ID> users = UserHelper.parseUsers(Arrays.asList(vuDef.split(",")), null);
if (!users.contains(user)) continue;
}
alist.add(e.toJSON("id", "name", "outputType"));
}
}
return list;
return alist;
}
/**
@ -67,14 +90,12 @@ public class DataReportManager implements ConfigManager {
* @return
*/
public ConfigBean[] getReportsRaw(Entity entity) {
final String cKey = "DataReportManager33-" + entity.getName();
final String cKey = "DataReportManager35-" + entity.getName();
ConfigBean[] cached = (ConfigBean[]) Application.getCommonsCache().getx(cKey);
if (cached != null) {
return cached;
}
if (cached != null) return cached;
Object[][] array = Application.createQueryNoFilter(
"select configId,name,isDisabled,templateFile,templateType,extraDefinition from DataReportConfig where belongEntity = ?")
"select configId,name,isDisabled,templateFile,templateType,extraDefinition,templateContent from DataReportConfig where belongEntity = ?")
.setParameter(1, entity.getName())
.array();
@ -83,15 +104,21 @@ public class DataReportManager implements ConfigManager {
JSONObject extra = o[5] == null ? JSONUtils.EMPTY_OBJECT : JSON.parseObject((String) o[5]);
String outputType = StringUtils.defaultIfBlank(extra.getString("outputType"), "excel");
int templateVersion = extra.containsKey("templateVersion") ? extra.getInteger("templateVersion") : 2;
String visibleUsersDef = extra.getString("visibleUsers");
int type = ObjectUtils.toInt(o[4], TYPE_RECORD);
if (type == TYPE_WORD && outputType.contains("excel")) outputType += ",word";
ConfigBean cb = new ConfigBean()
.set("id", o[0])
.set("name", o[1])
.set("disabled", o[2])
.set("template", o[3])
.set("type", ObjectUtils.toInt(o[4], TYPE_RECORD))
.set("type", type)
.set("outputType", outputType)
.set("templateVersion", templateVersion);
.set("templateVersion", templateVersion)
.set("visibleUsers", visibleUsersDef)
.set("templateContent", o[6]);
alist.add(cb);
}
@ -106,29 +133,36 @@ public class DataReportManager implements ConfigManager {
* @return
*/
public TemplateFile getTemplateFile(Entity entity, ID reportId) {
String template = null;
boolean isList = false;
String templateFile = null;
String templateContent = null;
int type = DataReportManager.TYPE_RECORD;
boolean isV33 = false;
for (ConfigBean e : getReportsRaw(entity)) {
if (e.getID("id").equals(reportId)) {
template = e.getString("template");
isList = e.getInteger("type") == TYPE_LIST;
templateFile = e.getString("template");
templateContent = e.getString("templateContent");
type = e.getInteger("type");
isV33 = e.getInteger("templateVersion") == 3;
break;
}
}
if (template == null) {
// v35 HTML5
if (templateContent != null) {
return new TemplateFile(templateContent, entity);
}
if (templateFile == null) {
throw new ConfigurationException("No template of report found : " + reportId);
}
File file = RebuildConfiguration.getFileOfData(template);
File file = RebuildConfiguration.getFileOfData(templateFile);
if (!file.exists()) {
throw new ConfigurationException("File of template not extsts : " + file);
}
return new TemplateFile(file, entity, isList, isV33);
return new TemplateFile(file, entity, type, isV33);
}
/**
@ -137,23 +171,22 @@ public class DataReportManager implements ConfigManager {
* @see #getTemplateFile(Entity, ID) 性能好
*/
public TemplateFile getTemplateFile(ID reportId) {
Object[] report = Application.createQueryNoFilter(
"select belongEntity from DataReportConfig where configId = ?")
.setParameter(1, reportId)
.unique();
if (report == null || !MetadataHelper.containsEntity((String) report[0])) {
Object[] o = Application.getQueryFactory().uniqueNoFilter(reportId, "belongEntity");
if (o == null || !MetadataHelper.containsEntity((String) o[0])) {
throw new ConfigurationException("No config of report found : " + reportId);
}
return getTemplateFile(MetadataHelper.getEntity((String) report[0]), reportId);
return getTemplateFile(MetadataHelper.getEntity((String) o[0]), reportId);
}
@Override
public void clean(Object entity) {
final String cKey = "DataReportManager33-" + ((Entity) entity).getName();
final String cKey = "DataReportManager35-" + ((Entity) entity).getName();
Application.getCommonsCache().evict(cKey);
}
// --
/**
* 获取报表名称
*
@ -163,7 +196,8 @@ public class DataReportManager implements ConfigManager {
* @return
*/
public static String getReportName(ID reportId, Object idOrEntity, String fileName) {
Entity be = idOrEntity instanceof ID ? MetadataHelper.getEntity(((ID) idOrEntity).getEntityCode())
final Entity be = idOrEntity instanceof ID
? MetadataHelper.getEntity(((ID) idOrEntity).getEntityCode())
: MetadataHelper.getEntity((String) idOrEntity);
String name = null;
@ -178,6 +212,7 @@ public class DataReportManager implements ConfigManager {
// suffix
if (fileName.endsWith(".pdf")) name += ".pdf";
else if (fileName.endsWith(".docx")) name += ".docx";
else name += fileName.endsWith(".xlsx") ? ".xlsx" : ".xls";
break;
}

View file

@ -79,7 +79,7 @@ import static com.rebuild.core.service.datareport.TemplateExtractor.PLACEHOLDER;
@Slf4j
public class EasyExcelGenerator extends SetUser {
protected File template;
protected File templateFile;
protected Integer writeSheetAt = null;
protected ID recordId;
@ -92,7 +92,7 @@ public class EasyExcelGenerator extends SetUser {
* @param recordId
*/
protected EasyExcelGenerator(File template, ID recordId) {
this.template = getFixTemplate(template);
this.templateFile = getFixTemplate(template);
this.recordId = recordId;
}
@ -116,7 +116,7 @@ public class EasyExcelGenerator extends SetUser {
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
try (ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(template).build()) {
try (ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(templateFile).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet(writeSheetAt)
.registerWriteHandler(new FixsMergeStrategy())
.registerWriteHandler(new FormulaCellWriteHandler())
@ -150,8 +150,13 @@ public class EasyExcelGenerator extends SetUser {
* @return
*/
protected File getTargetFile() {
return RebuildConfiguration.getFileOfTemp(String.format("RBREPORT-%d.%s",
System.currentTimeMillis(), template.getName().endsWith(".xlsx") ? "xlsx" : "xls"));
String suffix = "xls";
if (templateFile.getName().endsWith(".xlsx")) suffix = "xlsx";
if (templateFile.getName().endsWith(".html")) suffix = "html";
if (templateFile.getName().endsWith(".docx")) suffix = "docx";
if (templateFile.getName().endsWith(".doc")) suffix = "doc";
return RebuildConfiguration.getFileOfTemp(String.format("RBREPORT-%d.%s", System.currentTimeMillis(), suffix));
}
/**
@ -160,9 +165,9 @@ public class EasyExcelGenerator extends SetUser {
* @return 第一个为主记录若有
*/
protected List<Map<String, Object>> buildData() {
Entity entity = MetadataHelper.getEntity(this.recordId.getEntityCode());
Entity entity = MetadataHelper.getEntity(recordId.getEntityCode());
TemplateExtractor templateExtractor = new TemplateExtractor(this.template);
TemplateExtractor templateExtractor = new TemplateExtractor(templateFile);
Map<String, String> varsMap = templateExtractor.transformVars(entity);
Map<String, String> varsMapOfMain = new HashMap<>();
@ -193,7 +198,7 @@ public class EasyExcelGenerator extends SetUser {
}
// 无效字段
else if (validField == null) {
log.warn("Invalid field `{}` in template : {}", e.getKey(), this.template);
log.warn("Invalid field `{}` in template : {}", e.getKey(), templateFile);
continue;
}
@ -219,9 +224,9 @@ public class EasyExcelGenerator extends SetUser {
entity.getPrimaryField().getName(), entity.getName(), entity.getPrimaryField().getName());
Record record = Application.createQuery(sql, this.getUser())
.setParameter(1, this.recordId)
.setParameter(1, recordId)
.record();
Assert.notNull(record, "No record found : " + this.recordId);
Assert.notNull(record, "No record found : " + recordId);
datas.add(buildData(record, varsMapOfMain));
this.hasMain = true;
@ -236,7 +241,7 @@ public class EasyExcelGenerator extends SetUser {
MetadataHelper.getDetailToMainField(entity.getDetailEntity()).getName());
List<Record> list = Application.createQuery(sql, this.getUser())
.setParameter(1, this.recordId)
.setParameter(1, recordId)
.list();
phNumber = 1;
@ -253,7 +258,7 @@ public class EasyExcelGenerator extends SetUser {
StringUtils.join(fieldsOfApproval, ","));
List<Record> list = Application.createQueryNoFilter(sql)
.setParameter(1, this.recordId)
.setParameter(1, recordId)
.list();
phNumber = 1;
@ -273,6 +278,7 @@ public class EasyExcelGenerator extends SetUser {
*/
protected Map<String, Object> buildData(Record record, Map<String, String> varsMap) {
final Entity entity = record.getEntity();
final boolean isApproval = entity.getEntityCode() == EntityHelper.RobotApprovalStep;
final String invalidFieldTip = Language.L("[无效字段]");
final String unsupportFieldTip = Language.L("[暂不支持]");
@ -347,8 +353,12 @@ public class EasyExcelGenerator extends SetUser {
fieldValue = FieldValueHelper.wrapFieldValue(fieldValue, easyField, Boolean.TRUE);
}
if (record.getEntity().getEntityCode() == EntityHelper.RobotApprovalStep && "state".equalsIgnoreCase(fieldName)) {
fieldValue = Language.L(ApprovalState.valueOf(ObjectUtils.toInt(fieldValue)));
if (isApproval && "state".equalsIgnoreCase(fieldName)) {
int state = ObjectUtils.toInt(fieldValue);
if (state < 1) fieldValue = Language.L("提交");
else if (state == ApprovalState.DRAFT.getState()) fieldValue = Language.L("待审批");
else fieldValue = Language.L(ApprovalState.valueOf(state));
} else if (FieldValueHelper.isUseDesensitized(easyField, this.getUser())) {
fieldValue = FieldValueHelper.desensitized(easyField, fieldValue);
}
@ -371,6 +381,7 @@ public class EasyExcelGenerator extends SetUser {
}
}
}
data.put(varName, fieldValue);
}
}
@ -519,7 +530,8 @@ public class EasyExcelGenerator extends SetUser {
if (recordIds.size() == 1) {
return create(reportId, recordId);
} else {
TemplateFile tt = DataReportManager.instance.getTemplateFile(MetadataHelper.getEntity(recordId.getEntityCode()), reportId);
TemplateFile tt = DataReportManager.instance
.getTemplateFile(MetadataHelper.getEntity(recordId.getEntityCode()), reportId);
return new EasyExcelGenerator33(tt.templateFile, recordIds);
}
}
@ -530,7 +542,8 @@ public class EasyExcelGenerator extends SetUser {
* @return
*/
public static EasyExcelGenerator create(ID reportId, ID recordId) {
TemplateFile tt = DataReportManager.instance.getTemplateFile(MetadataHelper.getEntity(recordId.getEntityCode()), reportId);
TemplateFile tt = DataReportManager.instance
.getTemplateFile(MetadataHelper.getEntity(recordId.getEntityCode()), reportId);
return create(tt.templateFile, recordId, tt.isV33);
}

View file

@ -7,16 +7,21 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.datareport;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.approval.ApprovalHelper;
import com.rebuild.core.support.general.RecordBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.ss.usermodel.PrintSetup;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
@ -28,6 +33,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -45,23 +51,23 @@ import static com.rebuild.core.service.datareport.TemplateExtractor33.DETAIL_PRE
@Slf4j
public class EasyExcelGenerator33 extends EasyExcelGenerator {
final private List<ID> recordIds;
final private List<ID> recordIdMultiple;
protected EasyExcelGenerator33(File template, ID recordId) {
super(template, recordId);
this.recordIds = null;
protected EasyExcelGenerator33(File templateFile, ID recordId) {
super(templateFile, recordId);
this.recordIdMultiple = null;
}
protected EasyExcelGenerator33(File template, List<ID> recordIds) {
super(template, recordIds.get(0));
this.recordIds = recordIds;
this.recordIdMultiple = recordIds;
}
@Override
protected List<Map<String, Object>> buildData() {
final Entity entity = MetadataHelper.getEntity(recordId.getEntityCode());
final TemplateExtractor33 templateExtractor33 = new TemplateExtractor33(template);
final TemplateExtractor33 templateExtractor33 = this.buildTemplateExtractor33();
final Map<String, String> varsMap = templateExtractor33.transformVars(entity);
// 变量
@ -102,7 +108,7 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
if (TemplateExtractor33.isPlaceholder(varName)) continue;
// 无效字段
if (fieldName == null) {
log.warn("Invalid field `{}` in template : {}", e.getKey(), template);
log.warn("Invalid field `{}` in template : {}", e.getKey(), templateFile);
continue;
}
@ -144,7 +150,7 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
if (isApproval) {
querySql += " and isWaiting = 'F' and isCanceled = 'F' order by createdOn";
querySql = String.format(querySql, StringUtils.join(e.getValue(), ","),
"stepId", "RobotApprovalStep", "recordId");
"createdOn,recordId,state,stepId", "RobotApprovalStep", "recordId");
} else if (refName.startsWith(DETAIL_PREFIX)) {
Entity de = entity.getDetailEntity();
@ -172,8 +178,30 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
.setParameter(1, recordId)
.list();
// 补充提交节点
if (isApproval && !list.isEmpty()) {
Record firstNode = list.get(0);
Record submit = RecordBuilder.builder(EntityHelper.RobotApprovalStep)
.add("approver", ApprovalHelper.getSubmitter(firstNode.getID("recordId")))
.add("approvedTime", CalendarUtils.getUTCDateTimeFormat().format(firstNode.getDate("createdOn")))
.add("state", 0)
.build(UserService.SYSTEM_USER);
List<Record> list2 = new ArrayList<>();
list2.add(submit);
list2.addAll(list);
list = list2;
}
phNumber = 1;
for (Record c : list) {
// 特殊处理
if (isApproval) {
int state = c.getInt("state");
Date approvedTime = c.getDate("approvedTime");
if (approvedTime == null && state > 1) c.setDate("approvedTime", c.getDate("createdOn"));
}
datas.add(buildData(c, varsMapOfRefs.get(refName)));
phNumber++;
}
@ -182,21 +210,29 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
return datas;
}
// -- V34 多个
/**
* @return
*/
protected TemplateExtractor33 buildTemplateExtractor33() {
return new TemplateExtractor33(templateFile);
}
// -- V34 支持多记录导出
@Override
public File generate() {
if (recordIds == null) return super.generate();
if (recordIdMultiple == null) return super.generate();
// init
File targetFile = super.getTargetFile();
try {
FileUtils.copyFile(this.template, targetFile);
FileUtils.copyFile(templateFile, targetFile);
} catch (IOException e) {
throw new ReportsException(e);
}
for (ID recordId : this.recordIds) {
PrintSetup copyPrintSetup = null;
for (ID recordId : recordIdMultiple) {
int newSheetAt;
try (Workbook wb = WorkbookFactory.create(Files.newInputStream(targetFile.toPath()))) {
// 1.复制模板
@ -210,7 +246,15 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
wb.setSheetName(newSheetAt, newSheetName);
}
// 2.保存模板
// 2.复制打印设置POI 不会自己复制???
// https://www.bilibili.com/read/cv15053559/
if (copyPrintSetup == null) {
copyPrintSetup = wb.getSheetAt(0).getPrintSetup();
}
newSheet.getPrintSetup().setLandscape(copyPrintSetup.getLandscape());
newSheet.getPrintSetup().setPaperSize(copyPrintSetup.getPaperSize());
// 3.保存模板
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
wb.write(fos);
}
@ -220,7 +264,7 @@ public class EasyExcelGenerator33 extends EasyExcelGenerator {
}
// 生成报表
this.template = targetFile;
this.templateFile = targetFile;
this.writeSheetAt = newSheetAt;
this.recordId = recordId;
this.hasMain = false;

View file

@ -53,7 +53,7 @@ public class EasyExcelListGenerator extends EasyExcelGenerator {
@Override
protected List<Map<String, Object>> buildData() {
Entity entity = MetadataHelper.getEntity(queryData.getString("entity"));
TemplateExtractor varsExtractor = new TemplateExtractor(this.template, Boolean.TRUE);
TemplateExtractor varsExtractor = new TemplateExtractor(templateFile, Boolean.TRUE);
Map<String, String> varsMap = varsExtractor.transformVars(entity);
List<String> validFields = new ArrayList<>();
@ -68,7 +68,7 @@ public class EasyExcelListGenerator extends EasyExcelGenerator {
if (validField != null && e.getKey().startsWith(NROW_PREFIX)) {
validFields.add(validField);
} else {
log.warn("Invalid field `{}` in template : {}", e.getKey(), this.template);
log.warn("Invalid field `{}` in template : {}", e.getKey(), templateFile);
}
}

View file

@ -55,11 +55,11 @@ public class TemplateExtractor {
// 当前日期时间
protected static final String PH__CURRENTDATETIME = PLACEHOLDER + "CURRENTDATETIME";
// v2:{xxx} v1:${xxx}
// 变量匹配 v2:{xxx} v1:${xxx}
protected static final Pattern PATT_V2 = Pattern.compile("\\{(.*?)}");
final protected File template;
final private boolean isList;
final protected File templateFile;
final private boolean isListType;
/**
* @param template
@ -69,12 +69,12 @@ public class TemplateExtractor {
}
/**
* @param template
* @param templateFile
* @param isList 列表模板
*/
public TemplateExtractor(File template, boolean isList) {
this.template = template;
this.isList = isList;
public TemplateExtractor(File templateFile, boolean isList) {
this.templateFile = templateFile;
this.isListType = isList;
}
/**
@ -86,7 +86,7 @@ public class TemplateExtractor {
public Map<String, String> transformVars(Entity entity) {
final Set<String> vars = extractVars();
Entity detailEntity = this.isList ? null : entity.getDetailEntity();
Entity detailEntity = this.isListType ? null : entity.getDetailEntity();
Entity approvalEntity = MetadataHelper.hasApprovalField(entity)
? MetadataHelper.getEntity(EntityHelper.RobotApprovalStep) : null;
@ -135,13 +135,16 @@ public class TemplateExtractor {
* @return
*/
protected Set<String> extractVars() {
List<Cell[]> rows = ExcelUtils.readExcel(this.template);
List<Cell[]> rows = ExcelUtils.readExcel(templateFile);
Set<String> vars = new HashSet<>();
for (Cell[] row : rows) {
for (Cell cell : row) {
if (cell.isEmpty()) continue;
// 变量套变量无法支持
// {.__KEEP:(=IF(ISBLANK({.LimitedCredit}), "", "{.LimitedCredit}天付款"))}
String cellText = cell.asString();
Matcher matcher = PATT_V2.matcher(cellText);
while (matcher.find()) {

View file

@ -34,10 +34,10 @@ public class TemplateExtractor33 extends TemplateExtractor {
private Map<String, String> sortFields = new HashMap<>();
/**
* @param template
* @param templateFile
*/
public TemplateExtractor33(File template) {
super(template, Boolean.FALSE);
public TemplateExtractor33(File templateFile) {
super(templateFile, Boolean.FALSE);
}
/**
@ -57,7 +57,7 @@ public class TemplateExtractor33 extends TemplateExtractor {
for (final String varName : vars) {
// 列表型字段
if (varName.startsWith(NROW_PREFIX)) {
final String listField = varName.substring(1);
final String listField = varName.substring(1).replace("$", ".");
if (isPlaceholder(listField)) {
map.put(varName, null);

View file

@ -18,14 +18,25 @@ import java.io.File;
public class TemplateFile {
final public File templateFile;
final public String templateContent;
final public Entity entity;
final public boolean isList;
final public int type;
final public boolean isV33;
public TemplateFile(File templateFile, Entity entity, boolean isList, boolean isV33) {
public TemplateFile(File templateFile, Entity entity, int type, boolean isV33) {
this.templateFile = templateFile;
this.entity = entity;
this.isList = isList;
this.type = type;
this.isV33 = isV33;
this.templateContent = null;
}
// HTML5
public TemplateFile(String templateContent, Entity entity) {
this.templateContent = templateContent;
this.entity = entity;
this.type = DataReportManager.TYPE_HTML5;
this.isV33 = true;
this.templateFile = null;
}
}

View file

@ -14,7 +14,6 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.service.trigger.impl.FieldAggregation;
import com.rebuild.core.support.general.BatchOperatorQuery;
import lombok.extern.slf4j.Slf4j;
@ -27,18 +26,18 @@ import org.apache.commons.lang.StringUtils;
* @since 2019/12/2
*/
@Slf4j
public class BulkBacthUpdate extends BulkOperator {
public class BulkBatchUpdate extends BulkOperator {
/**
* 修改为
*/
public static final String OP_SET = "SET";
protected static final String OP_SET = "SET";
/**
* 置空
*/
public static final String OP_NULL = "NULL";
protected static final String OP_NULL = "NULL";
public BulkBacthUpdate(BulkContext context, GeneralEntityService ges) {
public BulkBatchUpdate(BulkContext context, GeneralEntityService ges) {
super(context, ges);
}
@ -84,7 +83,7 @@ public class BulkBacthUpdate extends BulkOperator {
ges.createOrUpdate(record);
this.addSucceeded();
} catch (DataSpecificationException ex) {
} catch (Exception ex) {
log.warn("Cannot update `{}` because : {}", id, ex.getLocalizedMessage());
} finally {
@ -109,6 +108,6 @@ public class BulkBacthUpdate extends BulkOperator {
JSONObject customData = (JSONObject) context.getExtraParams().get("customData");
int dataRange = customData.getIntValue("_dataRange");
BatchOperatorQuery query = new BatchOperatorQuery(dataRange, customData.getJSONObject("queryData"));
return query.getQueryedRecords();
return query.getQueryedRecordIds();
}
}

View file

@ -7,7 +7,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.general;
import cn.devezhao.bizz.privileges.Permission;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
@ -25,63 +24,56 @@ import java.util.List;
*/
public interface EntityService extends ServiceSpec {
/**
* 取消共享跟随共享权限
*
* @see BizzPermission
*/
Permission UNSHARE = new BizzPermission("UNSHARE", 1 << 6, true);
/**
* 删除带级联
*
* @param record
* @param recordId
* @param cascades 需要级联删除的实体
* @return
*/
int delete(ID record, String[] cascades);
int delete(ID recordId, String[] cascades);
/**
* 分配
*
* @param record
* @param recordId
* @param to
* @param cascades 需要级联分配的实体
* @return
*/
int assign(ID record, ID to, String[] cascades);
int assign(ID recordId, ID to, String[] cascades);
/**
* 共享
*
* @param record
* @param recordId
* @param to
* @param cascades
* @return
*/
default int share(ID record, ID to, String[] cascades) {
return share(record, to, cascades, BizzPermission.READ.getMask());
default int share(ID recordId, ID to, String[] cascades) {
return share(recordId, to, cascades, BizzPermission.READ.getMask());
}
/**
* 共享
*
* @param record
* @param recordId
* @param to
* @param cascades 需要级联分配的实体
* @param rights 共享权限
* @return
*/
int share(ID record, ID to, String[] cascades, int rights);
int share(ID recordId, ID to, String[] cascades, int rights);
/**
* 取消共享
*
* @param record 主记录
* @param recordId 主记录
* @param accessId 共享的 AccessID
* @return
*/
int unshare(ID record, ID accessId);
int unshare(ID recordId, ID accessId);
/**
* 批量操作
@ -104,7 +96,7 @@ public interface EntityService extends ServiceSpec {
* 检查并获取如有重复记录
*
* @param checkRecord
* @param limit
* @param limit 最大查重返回数量
* @return
*/
List<Record> getAndCheckRepeated(Record checkRecord, int limit);
@ -112,11 +104,11 @@ public interface EntityService extends ServiceSpec {
/**
* 审批
*
* @param record
* @param recordId
* @param state 只接受通过或撤销
* @param approvalUser 审批人
* @see com.rebuild.core.service.approval.ApprovalStepService
* @see com.rebuild.core.service.approval.ApprovalProcessor
*/
void approve(ID record, ApprovalState state, ID approvalUser);
void approve(ID recordId, ApprovalState state, ID approvalUser);
}

View file

@ -9,6 +9,7 @@ package com.rebuild.core.service.general;
import cn.devezhao.bizz.privileges.Permission;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import cn.devezhao.commons.ReflectUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Filter;
@ -24,7 +25,6 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.MetadataSorter;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyEntityConfigProps;
import com.rebuild.core.privileges.UserService;
@ -52,6 +52,7 @@ import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@ -62,6 +63,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observer;
import java.util.Set;
import java.util.TreeMap;
@ -73,10 +75,8 @@ import java.util.TreeMap;
*
* <p>如有需要其他实体可根据自身业务继承并复写</p>
*
* FIXME 删除主记录时会关联删除明细记录持久层实现但明细记录不会触发业务规则
*
* @author Zixin (RB)
* @since 11/06/2017
* @since 11/06/2019
*/
@Slf4j
@Service
@ -87,11 +87,18 @@ public class GeneralEntityService extends ObservableService implements EntitySer
protected GeneralEntityService(PersistManagerFactory aPMFactory) {
super(aPMFactory);
}
// 通知
addObserver(new NotificationObserver());
// 触发器
addObserver(new RobotTriggerObserver());
@Override
protected Observer[] getOrderObservers() {
Observer[] obs = new Observer[] {
// 触发器
new RobotTriggerObserver(),
// 通知
new NotificationObserver(),
};
obs = ArrayUtils.addAll(obs, super.getOrderObservers());
return obs;
}
@Override
@ -119,49 +126,68 @@ public class GeneralEntityService extends ObservableService implements EntitySer
// 含明细
final boolean hasDetails = details != null && !details.isEmpty();
boolean lazyAutoApprovalForDetails = false;
boolean lazyAutoTransformForDetails = false;
// 延迟执行触发器因为明细尚未保存好
boolean lazyAutoApproval4Details = false;
boolean lazyAutoTransform4Details = false;
boolean lazyHookUrl4Details = false;
if (hasDetails) {
// fix: v3.2.2
// 自动审批 fix: v3.2.2
TriggerAction[] hasAutoApprovalTriggers = getSpecTriggers(
record.getEntity(), ActionType.AUTOAPPROVAL, TriggerWhen.CREATE, TriggerWhen.UPDATE);
lazyAutoApprovalForDetails = hasAutoApprovalTriggers.length > 0;
lazyAutoApproval4Details = hasAutoApprovalTriggers.length > 0;
// FIXME 此判断可能无意义待进一步测试后确定是否保留
if (!lazyAutoApprovalForDetails) {
if (!lazyAutoApproval4Details) {
TriggerAction[] hasOnApprovedTriggers = getSpecTriggers(
record.getEntity().getDetailEntity(), null, TriggerWhen.APPROVED);
lazyAutoApprovalForDetails = hasOnApprovedTriggers.length > 0;
lazyAutoApproval4Details = hasOnApprovedTriggers.length > 0;
}
// 自动审批延迟执行因为明细尚未保存好
if (lazyAutoApprovalForDetails) AutoApproval.setLazyAutoApproval();
if (lazyAutoApproval4Details) AutoApproval.setLazy();
// 自动转换
TriggerAction[] hasAutoTransformTriggers = getSpecTriggers(
record.getEntity(), ActionType.AUTOTRANSFORM, TriggerWhen.CREATE, TriggerWhen.UPDATE);
lazyAutoTransformForDetails = hasAutoTransformTriggers.length > 0;
lazyAutoTransform4Details = hasAutoTransformTriggers.length > 0;
// 记录转换延迟执行因为明细尚未保存好
if (lazyAutoTransformForDetails) CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.AutoTransform#setLazyAutoTransform");
if (lazyAutoTransform4Details) CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.AutoTransform#setLazy");
// URL 回调 v3.5
TriggerAction[] hasHookUrlTriggers = getSpecTriggers(
record.getEntity(), ActionType.HOOKURL, TriggerWhen.CREATE, TriggerWhen.UPDATE, TriggerWhen.DELETE);
lazyHookUrl4Details = hasHookUrlTriggers.length > 0;
// 对于全量推送明细尚未保存好
if (lazyHookUrl4Details) CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.HookUrl#setLazy");
}
// 保证顺序
// 保证执行顺序
Map<Integer, ID> detaileds = new TreeMap<>();
try {
record = record.getPrimary() == null ? create(record) : update(record);
if (!hasDetails) return record;
// 主记录+明细记录处理
// 明细记录处理
final String dtfField = MetadataHelper.getDetailToMainField(record.getEntity().getDetailEntity()).getName();
final Entity detailEntity = record.getEntity().getDetailEntity();
final String dtfField = MetadataHelper.getDetailToMainField(detailEntity).getName();
final ID mainid = record.getPrimary();
final boolean checkDetailsRepeated = rcm == GeneralEntityServiceContextHolder.RCM_CHECK_DETAILS
|| rcm == GeneralEntityServiceContextHolder.RCM_CHECK_ALL;
// 明细可能有自己的 Service
EntityService des = Application.getEntityService(detailEntity.getEntityCode());
if (des.getEntityCode() == 0) des = this;
// 先删除
for (int i = 0; i < details.size(); i++) {
Record d = details.get(i);
if (d instanceof DeleteRecord) {
delete(d.getPrimary());
des.delete(d.getPrimary());
detaileds.put(i, d.getPrimary());
}
}
@ -174,7 +200,7 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (checkDetailsRepeated) {
d.setID(dtfField, mainid); // for check
List<Record> repeated = getAndCheckRepeated(d, 20);
List<Record> repeated = des.getAndCheckRepeated(d, 20);
if (!repeated.isEmpty()) {
throw new RepeatedRecordsException(repeated);
}
@ -182,9 +208,9 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (d.getPrimary() == null) {
d.setID(dtfField, mainid);
create(d);
des.create(d);
} else {
update(d);
des.update(d);
}
detaileds.put(i, d.getPrimary());
}
@ -193,12 +219,9 @@ public class GeneralEntityService extends ObservableService implements EntitySer
return record;
} finally {
if (lazyAutoApprovalForDetails) {
AutoApproval.executeLazyAutoApproval();
}
if (lazyAutoTransformForDetails) {
CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.AutoTransform#executeLazyAutoTransform");
}
if (lazyAutoApproval4Details) AutoApproval.executeLazy();
if (lazyAutoTransform4Details) CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.AutoTransform#executeLazy");
if (lazyHookUrl4Details) CommonsUtils.invokeMethod("com.rebuild.rbv.trigger.HookUrl#executeLazy");
}
}
@ -454,7 +477,7 @@ public class GeneralEntityService extends ObservableService implements EntitySer
if (countObservers() > 0) {
setChanged();
notifyObservers(OperatingContext.create(currentUser, UNSHARE, unsharedBefore, null));
notifyObservers(OperatingContext.create(currentUser, InternalPermission.UNSHARE, unsharedBefore, null));
}
return 1;
}
@ -550,11 +573,15 @@ public class GeneralEntityService extends ObservableService implements EntitySer
return new BulkAssign(context, this);
} else if (context.getAction() == BizzPermission.SHARE) {
return new BulkShare(context, this);
} else if (context.getAction() == UNSHARE) {
} else if (context.getAction() == InternalPermission.UNSHARE) {
return new BulkUnshare(context, this);
} else if (context.getAction() == BizzPermission.UPDATE) {
return new BulkBacthUpdate(context, this);
return new BulkBatchUpdate(context, this);
} else if (context.getAction() == InternalPermission.APPROVAL) {
return (BulkOperator) ReflectUtils.newObject(
"com.rebuild.rbv.approval.BulkBatchApprove", context, this);
}
throw new UnsupportedOperationException("Unsupported bulk action : " + context.getAction());
}
@ -673,10 +700,7 @@ public class GeneralEntityService extends ObservableService implements EntitySer
continue;
}
EasyField easyField = EasyMetaFactory.valueOf(field);
if (easyField.getDisplayType() == DisplayType.SERIES) continue;
Object defaultValue = easyField.exprDefaultValue();
Object defaultValue = EasyMetaFactory.valueOf(field).exprDefaultValue();
if (defaultValue != null) {
recordOfNew.setObjectValue(field.getName(), defaultValue);
}
@ -831,4 +855,9 @@ public class GeneralEntityService extends ObservableService implements EntitySer
}
return specTriggers.toArray(new TriggerAction[0]);
}
@Override
public String toString() {
return getEntityCode() + "#" + super.toString();
}
}

View file

@ -45,15 +45,21 @@ public abstract class ObservableService extends Observable implements ServiceSpe
protected ObservableService(PersistManagerFactory aPMFactory) {
this.delegateService = new BaseService(aPMFactory);
// 默认监听者
addObserver(new RevisionHistoryObserver());
addObserver(new AttachmentAwareObserver());
for (Observer o : getOrderObservers()) {
log.info("Add observer : {} for [ {} ] ", o, getEntityCode());
addObserver(o);
}
}
@Override
public synchronized void addObserver(Observer o) {
super.addObserver(o);
log.info("Add observer : {} for [ {} ] ", o, getEntityCode());
/**
* @return
*/
protected Observer[] getOrderObservers() {
// 默认监听者
return new Observer[] {
new RevisionHistoryObserver(), // 2
new AttachmentAwareObserver(), // 1
};
}
@Override
@ -87,7 +93,8 @@ public abstract class ObservableService extends Observable implements ServiceSpe
@Override
public int delete(ID recordId) {
final ID currentUser = UserContextHolder.getUser();
ID currentUser = UserContextHolder.getRestoreUser();
if (currentUser == null) currentUser = UserContextHolder.getUser();
Record deleted = null;
if (countObservers() > 0) {

View file

@ -11,6 +11,8 @@ import cn.devezhao.bizz.privileges.Permission;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.service.trigger.ActionContext;
import org.springframework.util.Assert;
/**
@ -47,7 +49,7 @@ public class OperatingContext {
this.action = action;
this.beforeRecord = beforeRecord;
this.afterRecord = afterRecord;
this.affected = affected == null ? new ID[]{getAnyRecord().getPrimary()} : affected;
this.affected = affected == null ? new ID[]{ getFixedRecordId() } : affected;
this.operationIp = operationIp;
}
@ -80,12 +82,31 @@ public class OperatingContext {
}
/**
* NOTE!!! 请注意当共享时得到的是共享实体 Record
* 如果是为了获取源纪录 ID 推荐使用 {@link #getFixedRecordId()}
*
* @return
*/
public Record getAnyRecord() {
return getAfterRecord() != null ? getAfterRecord() : getBeforeRecord();
}
/**
* v35 获取实际影响记录ID
* 例如在共享时传入的 Record ShareAccess而实际影响的是其中的 recordId 记录
*
* @return
* @see ActionContext#getSourceRecord()
*/
public ID getFixedRecordId() {
ID recordId = getAnyRecord().getPrimary();
if (recordId.getEntityCode() == EntityHelper.ShareAccess) {
recordId = getAnyRecord().getID("recordId");
Assert.notNull(recordId, "[recordId] in ShareAccess cannot be null");
}
return recordId;
}
/**
* 一次操作可能涉及多条记录
*

View file

@ -61,7 +61,7 @@ public abstract class OperatingObserver implements Observer {
onAssign(ctx);
} else if (ctx.getAction() == BizzPermission.SHARE) {
onShare(ctx);
} else if (ctx.getAction() == EntityService.UNSHARE) {
} else if (ctx.getAction() == InternalPermission.UNSHARE) {
onUnshare(ctx);
}
}

View file

@ -7,7 +7,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.general;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
@ -19,11 +18,11 @@ import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.RebuildException;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.collections4.map.CaseInsensitiveMap;
import java.util.Map;
import java.util.Objects;
/**
* 两个 Record 的不同
@ -52,6 +51,13 @@ public class RecordDifference {
return diffMerge(after, false);
}
/**
* 获取不同
*
* @param after
* @param diffCommons
* @return
*/
protected JSON diffMerge(Record after, boolean diffCommons) {
if (record == null && after == null) {
throw new RebuildException("Both records cannot be null");
@ -67,26 +73,28 @@ public class RecordDifference {
if (record != null) {
JSONObject recordSerialize = (JSONObject) record.serialize();
for (Map.Entry<String, Object> e : recordSerialize.entrySet()) {
String field = e.getKey();
if (!diffCommons && isIgnoreField(entity.getField(field))) continue;
String fieldName = e.getKey();
if (!entity.containsField(fieldName)) continue;
if (!diffCommons && isIgnoreField(entity.getField(fieldName))) continue;
Object beforeVal = e.getValue();
if (NullValue.is(beforeVal)) beforeVal = null;
merged.put(field, new Object[]{beforeVal, null});
merged.put(fieldName, new Object[]{beforeVal, null});
}
}
if (after != null) {
JSONObject afterSerialize = (JSONObject) after.serialize();
for (Map.Entry<String, Object> e : afterSerialize.entrySet()) {
String field = e.getKey();
if (!diffCommons && isIgnoreField(entity.getField(field))) continue;
String fieldName = e.getKey();
if (!entity.containsField(fieldName)) continue;
if (!diffCommons && isIgnoreField(entity.getField(fieldName))) continue;
Object afterVal = e.getValue();
if (NullValue.is(afterVal)) continue;
Object[] mergedValue = merged.computeIfAbsent(field, k -> new Object[]{null, null});
Object[] mergedValue = merged.computeIfAbsent(fieldName, k -> new Object[]{null, null});
mergedValue[1] = afterVal;
}
}
@ -96,7 +104,7 @@ public class RecordDifference {
for (Map.Entry<String, Object[]> e : merged.entrySet()) {
Object[] vals = e.getValue();
if (vals[0] == null && vals[1] == null) continue;
if (isEquals(vals[0], vals[1])) continue;
if (CommonsUtils.isSame(vals[0], vals[1])) continue;
JSON item = JSONUtils.toJSONObject(
new String[]{"field", "before", "after"},
@ -107,10 +115,10 @@ public class RecordDifference {
}
/**
* 是否
* 是否
*
* @param diff
* @param diffCommons
* @param diffCommons 是否比较系统共用字段
* @return
* @see #diffMerge(Record)
*/
@ -135,21 +143,4 @@ public class RecordDifference {
|| field.getType() == FieldType.PRIMARY
|| MetadataHelper.isApprovalField(fieldName);
}
/**
* 相等
*
* @param v1
* @param v2
* @return
*/
private boolean isEquals(Object v1, Object v2) {
boolean e = Objects.equals(v1, v2);
if (!e) {
if (v1 instanceof Number && v2 instanceof Number) {
e = ObjectUtils.toDouble(v1) == ObjectUtils.toDouble(v2);
}
}
return e;
}
}

View file

@ -47,7 +47,7 @@ public class RevisionHistoryObserver extends OperatingObserver {
}
private boolean isIgnore(OperatingContext ctx) {
int entity = ctx.getAnyRecord().getEntity().getEntityCode();
int entity = ctx.getFixedRecordId().getEntityCode();
return entity == EntityHelper.FeedsComment || entity == EntityHelper.ProjectTaskComment;
}
@ -130,6 +130,8 @@ public class RevisionHistoryObserver extends OperatingObserver {
TriggerSource triggerSource = RobotTriggerObserver.getTriggerSource();
if (triggerSource != null) {
record.setID("channelWith", triggerSource.getOriginRecord());
// v35 系统用户
record.setID("revisionBy", UserService.SYSTEM_USER);
}
if (context.getOperationIp() != null) {

View file

@ -16,6 +16,7 @@ import com.rebuild.core.support.ConfigurationItem;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.distributed.DistributedJobLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NamedThreadLocal;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@ -109,19 +110,52 @@ public class RecycleBinCleanerJob extends DistributedJobLock {
Application.getSqlExecutor().execute(delSql, 60 * 3);
}
// --
private static final ThreadLocal<Boolean> SKIP_RECYCLEBIN = new NamedThreadLocal<>("Skip recycle-bin");
private static final ThreadLocal<Boolean> SKIP_REVISIONHISTORY = new NamedThreadLocal<>("Skip revision history");
/**
* 回收站激活
* 回收站是否激活
* @return
* @see #setSkipRecyclebinOnce()
*/
public static boolean isEnableRecycleBin() {
return RebuildConfiguration.getInt(ConfigurationItem.RecycleBinKeepingDays) > 0;
boolean enable = RebuildConfiguration.getInt(ConfigurationItem.RecycleBinKeepingDays) > 0;
if (enable) {
Boolean skip = SKIP_RECYCLEBIN.get();
SKIP_RECYCLEBIN.remove();
if (skip != null && skip) return false;
}
return enable;
}
/**
* 修改历史激活
* 变更历史是否激活
* @return
* @see #setSkipRevisionHistoryOnce()
*/
public static boolean isEnableRevisionHistory() {
return RebuildConfiguration.getInt(ConfigurationItem.RevisionHistoryKeepingDays) > 0;
boolean enable = RebuildConfiguration.getInt(ConfigurationItem.RevisionHistoryKeepingDays) > 0;
if (enable) {
Boolean skip = SKIP_REVISIONHISTORY.get();
SKIP_REVISIONHISTORY.remove();
if (skip != null && skip) return false;
}
return enable;
}
/**
* 跳过一次回收站
*/
public static void setSkipRecyclebinOnce() {
SKIP_RECYCLEBIN.set(true);
}
/**
* 跳过一次变更历史
*/
public static void setSkipRevisionHistoryOnce() {
SKIP_REVISIONHISTORY.set(true);
}
}

View file

@ -19,6 +19,7 @@ import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.configuration.general.TransformManager;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.EntityRecordCreator;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
@ -28,7 +29,7 @@ import com.rebuild.core.service.general.GeneralEntityService;
import com.rebuild.core.service.general.GeneralEntityServiceContextHolder;
import com.rebuild.core.service.query.FilterRecordChecker;
import com.rebuild.core.support.SetUser;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@ -95,11 +96,11 @@ public class RecordTransfomer extends SetUser {
/**
* @param sourceRecordId
* @param mainId
* @param specMainId 转换明细时需指定主记录 ID
* @return
* @see #checkFilter(ID)
*/
public ID transform(ID sourceRecordId, ID mainId) {
public ID transform(ID sourceRecordId, ID specMainId) {
// 检查配置
Entity sourceEntity = MetadataHelper.getEntity(sourceRecordId.getEntityCode());
Entity sourceDetailEntity = null;
@ -132,13 +133,22 @@ public class RecordTransfomer extends SetUser {
}
Map<String, Object> dvMap = null;
if (mainId != null) {
if (specMainId != null) {
Field targetDtf = MetadataHelper.getDetailToMainField(targetEntity);
dvMap = Collections.singletonMap(targetDtf.getName(), mainId);
dvMap = Collections.singletonMap(targetDtf.getName(), specMainId);
}
Record main = transformRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, dvMap, false);
ID newId;
// v3.5 此配置未开放
// 在之前的版本中虽然文档写明非空字段无值会转换失败但是从来没有做过非空检查
// 为保持兼容性此选项不启用即入参保持为 false如有需要可指定为 true
final boolean checkNullable = transConfig.getBooleanValue("checkNullable35");
Record main = transformRecord(sourceEntity, targetEntity, fieldsMapping, sourceRecordId, dvMap, false, false, checkNullable);
ID theNewId;
// v3.5 需要先回填
// 因为可能以回填字段作为条件进行转换一次判断
final boolean fillbackFix = fillback(sourceRecordId, EntityHelper.newUnsavedId(main.getEntity().getEntityCode()));
// 有多条+明细
if (sourceDetails != null && sourceDetails.length > 0) {
@ -146,18 +156,18 @@ public class RecordTransfomer extends SetUser {
List<Record> detailsList = new ArrayList<>();
for (Object[] d : sourceDetails) {
detailsList.add(
transformRecord(sourceDetailEntity, targetDetailEntity, fieldsMappingDetail, (ID) d[0], null, false));
transformRecord(sourceDetailEntity, targetDetailEntity, fieldsMappingDetail, (ID) d[0], null, false, false, checkNullable));
}
newId = saveRecord(main, detailsList);
theNewId = saveRecord(main, detailsList);
} else {
newId = saveRecord(main, null);
theNewId = saveRecord(main, null);
}
// 回填
fillback(sourceRecordId, newId);
// 回填修正
if (fillbackFix) fillback(sourceRecordId, theNewId);
return newId;
return theNewId;
}
private ID saveRecord(Record record, List<Record> detailsList) {
@ -198,7 +208,7 @@ public class RecordTransfomer extends SetUser {
// 此配置未开放
int fillbackMode = transConfig.getIntValue("fillbackMode");
if (fillbackMode == 2) {
if (fillbackMode == 2 && !EntityHelper.isUnsavedId(newId)) {
GeneralEntityServiceContextHolder.setAllowForceUpdate(updateSource.getPrimary());
try {
Application.getEntityService(sourceEntity.getEntityCode()).update(updateSource);
@ -206,7 +216,7 @@ public class RecordTransfomer extends SetUser {
GeneralEntityServiceContextHolder.isAllowForceUpdateOnce();
}
} else {
// FIXME 回填仅更新无业务规则
// 无传播更新
Application.getCommonsService().update(updateSource, false);
}
@ -214,19 +224,21 @@ public class RecordTransfomer extends SetUser {
}
/**
* 转换
* 转换 Record
*
* @param sourceEntity
* @param targetEntity
* @param fieldsMapping
* @param sourceRecordId
* @param defaultValue
* @param ignoreUncreateable
* @param ignoreUncreateable 忽略不可新建字段
* @param forceNullValue v3.5 强制设定空字段值更新记录时
* @param checkNullable v3.5 检查不允许为空的字段是否都有值
* @return
*/
protected Record transformRecord(
Entity sourceEntity, Entity targetEntity, JSONObject fieldsMapping,
ID sourceRecordId, Map<String, Object> defaultValue, boolean ignoreUncreateable) {
ID sourceRecordId, Map<String, Object> defaultValue, boolean ignoreUncreateable, boolean forceNullValue, boolean checkNullable) {
Record target = EntityHelper.forNew(targetEntity.getEntityCode(), getUser());
@ -249,9 +261,13 @@ public class RecordTransfomer extends SetUser {
ID specOwningUser = null;
for (Map.Entry<String, Object> e : fieldsMapping.entrySet()) {
if (e.getValue() == null) continue;
final String targetField = e.getKey();
if (e.getValue() == null) {
if (forceNullValue) target.setNull(targetField);
continue;
}
String targetField = e.getKey();
EasyField targetFieldEasy = EasyMetaFactory.valueOf(targetEntity.getField(targetField));
if (ignoreUncreateable && !targetFieldEasy.isCreatable()) continue;
@ -271,6 +287,9 @@ public class RecordTransfomer extends SetUser {
Object targetValue = sourceFieldEasy.convertCompatibleValue(sourceValue, targetFieldEasy);
target.setObjectValue(targetField, targetValue);
} else if (forceNullValue) {
target.setNull(targetField);
}
}
@ -284,6 +303,10 @@ public class RecordTransfomer extends SetUser {
(ID) Application.getUserStore().getUser(specOwningUser).getOwningDept().getIdentity());
}
if (checkNullable) {
new EntityRecordCreator(targetEntity, JSONUtils.EMPTY_OBJECT, getUser()).verify(target);
}
return target;
}

View file

@ -96,7 +96,7 @@ public class TransformerPreview {
try {
for (ID did : ids) {
Record targetRecord = transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, did, null, true);
sourceEntity, targetEntity, fieldsMapping, did, null, true, false, false);
fillLabelOfReference(targetRecord);
@ -122,7 +122,7 @@ public class TransformerPreview {
}
Record targetRecord = transfomer.transformRecord(
sourceEntity, targetEntity, fieldsMapping, sourceId, null, true);
sourceEntity, targetEntity, fieldsMapping, sourceId, null, true, false, false);
fillLabelOfReference(targetRecord);
// 转为明细

View file

@ -22,6 +22,7 @@ import java.util.regex.Pattern;
*
* @author devezhao zhaofang123@gmail.com
* @since 2019/07/12
* @see com.rebuild.core.support.general.ContentWithFieldVars
*/
public class MessageBuilder {

View file

@ -27,6 +27,7 @@ import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.bizz.Department;
import com.rebuild.core.support.License;
import com.rebuild.core.support.SetUser;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.CommonsUtils;
@ -108,12 +109,12 @@ public class AdvFilterParser extends SetUser {
/**
* @param filterExpr
* @param varRecord 条件中包含字段变量将从此记录中提取实际值注意如果包括字段变量但是字段无值则过滤项会被忽略应在配置条件时考虑此问题设置不允许为空
* @param varRecord 条件中包含字段变量将从该记录中提取实际值替换
*/
public AdvFilterParser(JSONObject filterExpr, ID varRecord) {
this.filterExpr = filterExpr;
this.rootEntity = MetadataHelper.getEntity(varRecord.getEntityCode());
this.varRecord = varRecord;
this.varRecord = License.isRbvAttached() ? varRecord : null;
}
/**
@ -702,6 +703,7 @@ public class AdvFilterParser extends SetUser {
private static final String PATT_FIELDVAR = "\\{@([\\w.]+)}";
// `当前`变量当前日期时间用户
private static final String CURRENT_ANY = "CURRENT";
private static final String CURRENT_DATE = "NOW";
private String useValueOfVarRecord(String value, Field queryField) {
if (StringUtils.isBlank(value) || !value.matches(PATT_FIELDVAR)) return value;
@ -712,7 +714,7 @@ public class AdvFilterParser extends SetUser {
Object useValue = null;
// {@CURRENT} DATE
if (CURRENT_ANY.equals(fieldName)) {
if (CURRENT_ANY.equals(fieldName) || CURRENT_DATE.equals(fieldName)) {
DisplayType dt = EasyMetaFactory.getDisplayType(queryField);
if (dt == DisplayType.DATE || dt == DisplayType.DATETIME || dt == DisplayType.TIME) {
useValue = CalendarUtils.now();

View file

@ -229,6 +229,9 @@ public class ParseHelper {
|| dt == DisplayType.LOCATION) {
return StringUtils.defaultIfEmpty(fieldPath, field.getName());
} else if (dt == DisplayType.ANYREFERENCE) {
return fieldPath;
} else {
return null;
}

View file

@ -30,15 +30,15 @@ import java.util.List;
* 查询服务
*
* @author Zixin (RB)
* @since 05/21/2017
* @since 05/21/2019
* @see RoleBaseQueryFilter
* @see QueryHelper
*/
@Service
public class QueryFactory {
private static final int QUERY_TIMEOUT = 10 * 1000;
private static final int SLOW_LOGGER_TIME = 1000;
private static final int QUERY_TIMEOUT = 15; // s
private static final int SLOW_LOGGER_TIME = 3 * 1000; // ms
private final PersistManagerFactory aPMFactory;

View file

@ -23,7 +23,6 @@ import org.springframework.util.Assert;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@ -108,7 +107,7 @@ public class QueryHelper {
}
/**
* 获取明细完整记录
* 获取明细列表记录
*
* @param mainId
* @return
@ -129,7 +128,7 @@ public class QueryHelper {
}
/**
* 获取明细 ID
* 获取明细列表 ID
*
* @param mainId
* @return
@ -217,12 +216,9 @@ public class QueryHelper {
final ID primaryId = base.getPrimary();
Assert.notNull(primaryId, "Record primary cannot be null");
Set<String> fields = new HashSet<>();
for (Iterator<String> iter = base.getAvailableFieldIterator(); iter.hasNext(); ) {
fields.add(iter.next());
}
Set<String> fields = new HashSet<>(base.getAvailableFields());
fields.add(base.getEntity().getPrimaryField().getName());
Record snap = Application.getQueryFactory().recordNoFilter(primaryId, fields.toArray(new String[0]));
if (snap == null) throw new NoRecordFoundException(primaryId);

View file

@ -0,0 +1,23 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.service.trigger;
/**
* 部分触发器执行需要等到明细记录处理完成才能执行
*
* @author devezhao
* @since 2023/11/11
*/
public interface LazyWaitDetailsFinished {
String FLAG_LAZY = "lazy";
// setLazy
// isLazy
// executeLazy
}

View file

@ -42,12 +42,12 @@ public class RobotTriggerManager implements ConfigManager {
}
/**
* @param record
* @param recordId
* @param when
* @return
*/
public TriggerAction[] getActions(ID record, TriggerWhen... when) {
return filterActions(MetadataHelper.getEntity(record.getEntityCode()), record, when);
public TriggerAction[] getActions(ID recordId, TriggerWhen... when) {
return filterActions(MetadataHelper.getEntity(recordId.getEntityCode()), recordId, when);
}
/**
@ -61,21 +61,21 @@ public class RobotTriggerManager implements ConfigManager {
private static final ThreadLocal<List<String>> TRIGGERS_CHAIN_4DEBUG = ThreadLocal.withInitial(ArrayList::new);
/**
* @param record
* @param recordId
* @param entity
* @param when
* @return
*/
private TriggerAction[] filterActions(Entity entity, ID record, TriggerWhen... when) {
private TriggerAction[] filterActions(Entity entity, ID recordId, TriggerWhen... when) {
List<TriggerAction> actions = new ArrayList<>();
for (ConfigBean cb : getConfig(entity)) {
// 发生动作
if (allowedWhen(cb, when)) {
// 附加过滤条件
if (record == null
|| QueryHelper.isMatchAdvFilter(record, (JSONObject) cb.getJSON("whenFilter"), Boolean.TRUE)) {
if (recordId == null
|| QueryHelper.isMatchAdvFilter(recordId, (JSONObject) cb.getJSON("whenFilter"), true)) {
ActionContext ctx = new ActionContext(record, entity, cb.getJSON("actionContent"), cb.getID("id"));
ActionContext ctx = new ActionContext(recordId, entity, cb.getJSON("actionContent"), cb.getID("id"));
TriggerAction o = ActionFactory.createAction(cb.getString("actionType"), ctx);
if (o.getClass().getName().contains("NoRbv")) {
log.warn("Trigger action {} is RBV", cb.getString("actionType"));
@ -86,7 +86,7 @@ public class RobotTriggerManager implements ConfigManager {
if (log.isDebugEnabled()) {
TRIGGERS_CHAIN_4DEBUG.get().add(
String.format("%s (%s) on %s %s (%s)", o.getType(), ctx.getConfigId(), when[0], entity.getName(), record));
String.format("%s (%s) on %s %s (%s)", o.getType(), ctx.getConfigId(), when[0], entity.getName(), recordId));
}
}
}
@ -94,7 +94,7 @@ public class RobotTriggerManager implements ConfigManager {
if (!TRIGGERS_CHAIN_4DEBUG.get().isEmpty()) {
log.info("Record ({}) triggers chain : \n > {}",
record, StringUtils.join(TRIGGERS_CHAIN_4DEBUG.get(), "\n > "));
recordId, StringUtils.join(TRIGGERS_CHAIN_4DEBUG.get(), "\n > "));
}
return actions.toArray(new TriggerAction[0]);

View file

@ -8,7 +8,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger;
import cn.devezhao.persist4j.engine.ID;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.general.OperatingObserver;
import com.rebuild.core.service.general.RepeatedRecordsException;
@ -16,6 +15,7 @@ import com.rebuild.core.service.trigger.impl.FieldAggregation;
import com.rebuild.core.support.CommonsLog;
import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.web.KnownExceptionConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.NamedThreadLocal;
@ -76,7 +76,7 @@ public class RobotTriggerObserver extends OperatingObserver {
@Override
protected void onDeleteBefore(OperatingContext context) {
final ID primary = context.getAnyRecord().getPrimary();
final ID primary = context.getFixedRecordId();
TriggerAction[] deleteActions = RobotTriggerManager.instance.getActions(primary, TriggerWhen.DELETE);
for (TriggerAction action : deleteActions) {
@ -94,7 +94,7 @@ public class RobotTriggerObserver extends OperatingObserver {
@Override
protected void onDelete(OperatingContext context) {
final ID primary = context.getAnyRecord().getPrimary();
final ID primary = context.getFixedRecordId();
try {
execAction(context, TriggerWhen.DELETE);
} finally {
@ -109,11 +109,11 @@ public class RobotTriggerObserver extends OperatingObserver {
* @param when
*/
protected void execAction(OperatingContext context, TriggerWhen when) {
final ID primaryId = context.getAnyRecord().getPrimary();
final ID primaryId = context.getFixedRecordId();
TriggerAction[] beExecuted = when == TriggerWhen.DELETE
? DELETE_BEFORE_HOLD.get(primaryId)
: RobotTriggerManager.instance.getActions(getRealRecordId(context), when);
: RobotTriggerManager.instance.getActions(context.getFixedRecordId(), when);
if (beExecuted == null || beExecuted.length == 0) return;
TriggerSource triggerSource = getTriggerSource();
@ -186,7 +186,8 @@ public class RobotTriggerObserver extends OperatingObserver {
if (ex instanceof TriggerException) {
throw (TriggerException) ex;
} else {
String errMsg = ex.getLocalizedMessage();
String errMsg = KnownExceptionConverter.convert2ErrorMsg(ex);
if (errMsg == null) errMsg = ex.getLocalizedMessage();
if (ex instanceof RepeatedRecordsException) errMsg = Language.L("存在重复记录");
if (StringUtils.isBlank(errMsg)) errMsg = ex.getClass().getSimpleName().toUpperCase();
@ -217,20 +218,6 @@ public class RobotTriggerObserver extends OperatingObserver {
}
}
/**
* 获取实际影响的记录
* 例如在共享时传入的 Record ShareAccess而实际影响的是其中的 recordId 记录
*
* @return
*/
private ID getRealRecordId(OperatingContext context) {
ID recordId = context.getAnyRecord().getPrimary();
if (recordId.getEntityCode() == EntityHelper.ShareAccess) {
recordId = context.getAnyRecord().getID("recordId");
}
return recordId;
}
// --
/**

View file

@ -25,6 +25,10 @@ public abstract class TriggerAction {
* 自己
*/
public static final String SOURCE_SELF = "$PRIMARY$";
/**
* 任意实体字段匹配
*/
public static final String TARGET_ANY = "$";
final protected ActionContext actionContext;

View file

@ -75,6 +75,15 @@ public class TriggerResult implements JSONAware {
return new TriggerResult(1, message, null);
}
/**
* @param message
* @param affected
* @return
*/
public static TriggerResult success(String message, Collection<ID> affected) {
return new TriggerResult(1, message, affected);
}
/**
* @param message
* @return
@ -144,4 +153,13 @@ public class TriggerResult implements JSONAware {
public static TriggerResult noUpdateFields() {
return wran("No update fields");
}
/**
* 无效配置
*
* @return
*/
public static TriggerResult badConfig() {
return wran("Bad config");
}
}

View file

@ -57,7 +57,7 @@ public class TriggerSource {
}
public ID getOriginRecord() {
return getOrigin().getAnyRecord().getPrimary();
return getOrigin().getFixedRecordId();
}
public OperatingContext getLast() {
@ -66,7 +66,7 @@ public class TriggerSource {
public String getLastSourceKey() {
Object[] last = sources.get(sources.size() - 1);
return ((OperatingContext) last[0]).getAnyRecord().getPrimary() + ":" + ((TriggerWhen) last[1]).name().charAt(0);
return ((OperatingContext) last[0]).getFixedRecordId() + ":" + ((TriggerWhen) last[1]).name().charAt(0);
}
public void setSkipOnce() {

View file

@ -8,7 +8,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger;
import cn.devezhao.bizz.privileges.impl.BizzPermission;
import com.rebuild.core.service.general.EntityService;
import com.rebuild.core.privileges.bizz.InternalPermission;
/**
* 触发动作定义
@ -46,7 +46,7 @@ public enum TriggerWhen {
/**
* 取消共享时
*/
UNSHARE(EntityService.UNSHARE.getMask()),
UNSHARE(InternalPermission.UNSHARE.getMask()),
/**
* 审批通过
*/

View file

@ -22,12 +22,12 @@ import java.util.Map;
public class AviatorDate extends AviatorObject {
private static final long serialVersionUID = 2930549924386648595L;
protected static final String DU_YEAR = "Y";
protected static final String DU_MONTH = "M";
protected static final String DU_DAY = "D";
protected static final String DU_HOUR = "H";
protected static final String DU_MINUTE = "I";
protected static final String DU_SECOND = "S";
public static final String DU_YEAR = "Y";
public static final String DU_MONTH = "M";
public static final String DU_DAY = "D";
public static final String DU_HOUR = "H";
public static final String DU_MINUTE = "I";
public static final String DU_SECOND = "S";
final private Date dateValue;

View file

@ -51,6 +51,7 @@ public class AviatorUtils {
addCustomFunction(new CurrentDateFunction());
addCustomFunction(new ChineseYuanFunction());
addCustomFunction(new TextFunction());
addCustomFunction(new IsNullFunction());
}
/**

View file

@ -0,0 +1,58 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.service.trigger.aviator;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.engine.NullValue;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorBoolean;
import com.googlecode.aviator.runtime.type.AviatorObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.Map;
/**
* Usage: ISNULL($any)
* Return: Boolean
*
* @author RB
* @since 2023/10/18
*/
@Slf4j
public class IsNullFunction extends AbstractFunction {
private static final long serialVersionUID = -7849948179882904490L;
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
final Object $any = arg1.getValue(env);
if (NullValue.isNull($any)) return AviatorBoolean.TRUE;
if ($any instanceof Number) {
if (ObjectUtils.toDouble($any) == 0d) return AviatorBoolean.TRUE;
if (ObjectUtils.toLong($any) == 0L) return AviatorBoolean.TRUE;
}
if ($any instanceof Object[]) {
return ((Object[]) $any).length == 0 ? AviatorBoolean.TRUE : AviatorBoolean.FALSE;
}
if ($any instanceof Collection) {
return ((Collection<?>) $any).isEmpty() ? AviatorBoolean.TRUE : AviatorBoolean.FALSE;
}
return StringUtils.isEmpty($any.toString()) ? AviatorBoolean.TRUE : AviatorBoolean.FALSE;
}
@Override
public String getName() {
return "ISNULL";
}
}

View file

@ -7,22 +7,30 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.service.trigger.aviator;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.engine.ID;
import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.type.AviatorJavaType;
import com.googlecode.aviator.runtime.type.AviatorNil;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorString;
import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.utils.CommonsUtils;
import com.rebuild.web.commons.MetadataGetting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Usage: TEXT($id|$id[], [$defaultValue], [$separator])
* Usage: TEXT($id|$id[], [$defaultValue], [$separator], [$fieldName])
* Return: String
*
* @author RB
@ -37,37 +45,62 @@ public class TextFunction extends AbstractFunction {
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
return call(env, arg1, BLANK, SEPARATOR);
return call(env, arg1, BLANK, SEPARATOR, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
return call(env, arg1, BLANK, SEPARATOR);
return call(env, arg1, arg2, SEPARATOR, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3) {
return call(env, arg1, arg2, arg3, AviatorNil.NIL);
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2, AviatorObject arg3, AviatorObject arg4) {
final Object $id = arg1.getValue(env);
final Object $defaultValue = arg2.getValue(env);
final String sep = ObjectUtils.defaultIfNull(arg3.getValue(env), ", ").toString();
final String fieldName = arg4.getValue(env) == null ? null : arg4.getValue(env).toString();
// 引用 ID
if (ID.isId($id)) {
ID anyid = $id instanceof ID ? (ID) $id : ID.valueOf($id.toString());
String text = FieldValueHelper.getLabel(anyid, null);
return text == null ? arg2 : new AviatorString(text);
String text = getLabel(anyid, fieldName);
if (text == null && $defaultValue != null) text = $defaultValue.toString();
return new AviatorString(text);
}
// 多引用 ID[]
if ($id instanceof ID[]) {
List<String> texts = new ArrayList<>();
for (ID anyid : (ID[]) $id) {
String t = FieldValueHelper.getLabel(anyid, null);
if (t != null) texts.add(t);
Object idArray = $id;
if ($id instanceof Collection) {
List<ID> list = null;
for (Object o : (Collection<?>) $id) {
if (o instanceof ID) {
if (list == null) list = new ArrayList<>();
list.add((ID) o);
}
}
if (texts.isEmpty()) return arg2;
if (list != null) idArray = list.toArray(new ID[0]);
}
String sep = ObjectUtils.defaultIfNull(arg3.getValue(env), ", ").toString();
return new AviatorString(StringUtils.join(texts, sep));
if (idArray instanceof ID[]) {
List<String> text = new ArrayList<>();
for (ID anyid : (ID[]) idArray) {
String item = getLabel(anyid, fieldName);
if (item == null && $defaultValue != null) item = $defaultValue.toString();
if (item != null) text.add(item);
}
if (text.isEmpty()) return arg2;
return new AviatorString(StringUtils.join(text, sep));
}
if (arg1 instanceof AviatorJavaType) {
@ -81,6 +114,20 @@ public class TextFunction extends AbstractFunction {
return arg2;
}
// 获取字段内容
private String getLabel(ID id, String fieldName) {
if (fieldName == null) return FieldValueHelper.getLabelNotry(id);
Entity entity = MetadataHelper.getEntity(id.getEntityCode());
Field field = MetadataHelper.getLastJoinField(entity, fieldName);
Object[] o = Application.getQueryFactory().uniqueNoFilter(id, fieldName);
if (o == null || o[0] == null) return null;
Object label = FieldValueHelper.wrapFieldValue(o[0], field, true);
return label == null ? null : label.toString();
}
@Override
public String getName() {
return "TEXT";

View file

@ -28,9 +28,11 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -69,9 +71,11 @@ public class AggregationEvaluator {
if ("FORMULA".equalsIgnoreCase(calcMode)) {
return evalFormula();
}
// ONLY FieldAggregation
else if ("RBJOIN".equalsIgnoreCase(calcMode)) {
return evalRbJoin();
// for FieldAggregation/GroupAggregation
else if ("RBJOIN".equalsIgnoreCase(calcMode)
|| "RBJOIN2".equalsIgnoreCase(calcMode) || "RBJOIN3".equalsIgnoreCase(calcMode)) {
int mode = "RBJOIN2".equalsIgnoreCase(calcMode) ? 2 : ("RBJOIN3".equalsIgnoreCase(calcMode) ? 3 : 1);
return evalRbJoin(mode);
}
String sourceField = item.getString("sourceField");
@ -193,9 +197,10 @@ public class AggregationEvaluator {
/**
* 智能连接
*
* @param mode 去重模式
* @return
*/
private Object evalRbJoin() {
private Object evalRbJoin(int mode) {
String sourceField = item.getString("sourceField");
Field field;
if ((field = MetadataHelper.getLastJoinField(sourceEntity, sourceField)) == null) {
@ -205,9 +210,19 @@ public class AggregationEvaluator {
String ql = String.format("select %s,%s from %s where %s",
sourceField, sourceEntity.getPrimaryField().getName(), sourceEntity.getName(), filterSql);
Object[][] array = Application.createQueryNoFilter(ql).array();
if (array.length == 0) return new Object[0];
EasyField easyField = EasyMetaFactory.valueOf(field);
List<Object> nvList = new ArrayList<>();
Collection<Object> nvList;
Map<Object, Integer> countList = null;
if (mode == 2 || mode == 3) {
nvList = new LinkedHashSet<>();
if (mode == 3) countList = new HashMap<>(); // 针对文本
} else {
nvList = new ArrayList<>();
}
for (Object[] o : array) {
Object n = o[0];
if (n == null) continue;
@ -216,21 +231,54 @@ public class AggregationEvaluator {
if (n instanceof ID[]) {
CollectionUtils.addAll(nvList, (ID[]) n);
} else if (n instanceof ID) {
nvList.add(n);
if (field.getType() == FieldType.PRIMARY) {
nvList.add(n.toString()); // 保持主键
} else {
nvList.add(n);
}
} else {
Object v = easyField.wrapValue(n);
if (v == null) continue;
if (easyField.getDisplayType() == DisplayType.MULTISELECT) {
DisplayType dt = easyField.getDisplayType();
if (dt == DisplayType.MULTISELECT) {
JSONArray a = ((JSONObject) v).getJSONArray("text");
CollectionUtils.addAll(nvList, a);
// TEXT*N
if (countList != null) {
for (Object item : a) {
Integer c = countList.get(item);
if (c == null) c = 0;
countList.put(item, ++c);
}
}
} else if (dt == DisplayType.FILE || dt == DisplayType.IMAGE) {
nvList.addAll((JSONArray) v);
} else {
// TEXT
nvList.add(v);
// TEXT*N
if (countList != null) {
Integer c = countList.get(v);
if (c == null) c = 0;
countList.put(v, ++c);
}
}
}
}
if (countList == null || countList.isEmpty()) {
// Use array
return nvList.toArray(new Object[0]);
}
Collection<Object> nvList2 = new LinkedHashSet<>();
for (Object v : nvList) {
nvList2.add(v + "*" + countList.getOrDefault(v, 1));
}
// Use array
return nvList.toArray(new Object[0]);
return nvList2.toArray(new Object[0]);
}
}

View file

@ -17,6 +17,7 @@ import com.rebuild.core.service.approval.ApprovalStepService;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.service.trigger.ActionType;
import com.rebuild.core.service.trigger.LazyWaitDetailsFinished;
import com.rebuild.core.service.trigger.TriggerAction;
import com.rebuild.core.service.trigger.TriggerException;
import com.rebuild.core.service.trigger.TriggerResult;
@ -37,10 +38,11 @@ import static com.rebuild.core.support.CommonsLog.TYPE_TRIGGER;
* @since 2020/7/31
*/
@Slf4j
public class AutoApproval extends TriggerAction {
public class AutoApproval extends TriggerAction implements LazyWaitDetailsFinished {
// 延迟执行
private static final ThreadLocal<List<AutoApproval>> LAZY_AUTOAPPROVAL = new NamedThreadLocal<>("Lazy AutoApproval");
private OperatingContext operatingContext;
private OperatingContext keepOperatingContext;
public AutoApproval(ActionContext context) {
super(context);
@ -58,19 +60,19 @@ public class AutoApproval extends TriggerAction {
@Override
public Object execute(OperatingContext operatingContext) throws TriggerException {
this.operatingContext = operatingContext;
this.keepOperatingContext = operatingContext;
List<AutoApproval> lazyed;
if ((lazyed = isLazyAutoApproval(Boolean.FALSE)) != null) {
if ((lazyed = isLazy(false)) != null) {
lazyed.add(this);
log.info("Lazy AutoApproval : {}", lazyed);
return "lazy";
return FLAG_LAZY;
}
ID recordId = operatingContext.getAnyRecord().getPrimary();
ID recordId = operatingContext.getFixedRecordId();
String useApproval = ((JSONObject) actionContext.getActionContent()).getString("useApproval");
// 优先使用当前用户
ID approver = ObjectUtils.defaultIfNull(UserContextHolder.getUser(Boolean.TRUE), UserService.SYSTEM_USER);
ID approver = ObjectUtils.defaultIfNull(UserContextHolder.getUser(true), UserService.SYSTEM_USER);
ID approvalId = ID.isId(useApproval) ? ID.valueOf(useApproval) : null;
// v2.10
@ -89,24 +91,23 @@ public class AutoApproval extends TriggerAction {
@Override
public String toString() {
String s = super.toString();
if (operatingContext != null) s += "#OperatingContext:" + operatingContext;
if (keepOperatingContext != null) s += "#OperatingContext:" + keepOperatingContext;
return s;
}
// --
/**
* 跳过自动审批
* @see #isLazyAutoApproval(boolean)
*/
public static void setLazyAutoApproval() {
public static void setLazy() {
LAZY_AUTOAPPROVAL.set(new ArrayList<>());
}
/**
* @param once
* @return
*/
public static List<AutoApproval> isLazyAutoApproval(boolean once) {
public static List<AutoApproval> isLazy(boolean once) {
List<AutoApproval> lazyed = LAZY_AUTOAPPROVAL.get();
if (lazyed != null && once) LAZY_AUTOAPPROVAL.remove();
return lazyed;
@ -115,12 +116,12 @@ public class AutoApproval extends TriggerAction {
/**
* @return
*/
public static int executeLazyAutoApproval() {
List<AutoApproval> lazyed = isLazyAutoApproval(Boolean.TRUE);
public static int executeLazy() {
List<AutoApproval> lazyed = isLazy(true);
if (lazyed != null) {
for (AutoApproval a : lazyed) {
log.info("Lazy AutoApproval execute : {}", a);
Object res = a.execute(a.operatingContext);
Object res = a.execute(a.keepOperatingContext);
CommonsLog.createLog(TYPE_TRIGGER,
UserService.SYSTEM_USER, a.getActionContext().getConfigId(), res.toString());

View file

@ -56,7 +56,7 @@ public class AutoAssign extends TriggerAction {
@Override
public Object execute(OperatingContext operatingContext) throws TriggerException {
final JSONObject content = (JSONObject) actionContext.getActionContent();
final ID recordId = operatingContext.getAnyRecord().getPrimary();
final ID recordId = operatingContext.getFixedRecordId();
JSONArray assignTo = content.getJSONArray("assignTo");
Set<ID> toUsers = UserHelper.parseUsers(assignTo, recordId, true);

View file

@ -14,6 +14,7 @@ import com.rebuild.core.Application;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.privileges.bizz.InternalPermission;
import com.rebuild.core.service.NoRecordFoundException;
import com.rebuild.core.service.approval.ApprovalHelper;
import com.rebuild.core.service.approval.ApprovalState;
import com.rebuild.core.service.general.OperatingContext;
@ -46,7 +47,7 @@ public abstract class AutoHoldTriggerAction extends TriggerAction {
protected void prepare(OperatingContext operatingContext) throws TriggerException {
if (operatingContext.getAction() == InternalPermission.DELETE_BEFORE) {
willRecords = getRelatedRecords(
actionContext, operatingContext.getAnyRecord().getPrimary());
actionContext, operatingContext.getFixedRecordId());
}
}
@ -59,7 +60,7 @@ public abstract class AutoHoldTriggerAction extends TriggerAction {
protected Set<ID> getWillRecords(OperatingContext operatingContext) {
if (willRecords == null) {
willRecords = getRelatedRecords(
actionContext, operatingContext.getAnyRecord().getPrimary());
actionContext, operatingContext.getFixedRecordId());
}
return willRecords;
}
@ -122,7 +123,7 @@ public abstract class AutoHoldTriggerAction extends TriggerAction {
}
for (Field refto : fieldsRefto) {
Entity ownEntity = refto.getOwnEntity();
final Entity ownEntity = refto.getOwnEntity();
String sql = String.format("select %s from %s where ",
ownEntity.getPrimaryField().getName(), ownEntity.getName());
@ -153,8 +154,13 @@ public abstract class AutoHoldTriggerAction extends TriggerAction {
* @see ApprovalHelper#getApprovalState(ID)
*/
protected static ApprovalState getApprovalState(ID recordId) {
Entity entity = MetadataHelper.getEntity(recordId.getEntityCode());
if (MetadataHelper.hasApprovalField(entity)) return ApprovalHelper.getApprovalState(recordId);
else return null;
if (MetadataHelper.hasApprovalField(MetadataHelper.getEntity(recordId.getEntityCode()))) {
try {
return ApprovalHelper.getApprovalState(recordId);
} catch (NoRecordFoundException ignored) {
return null;
}
}
return null;
}
}

View file

@ -56,7 +56,7 @@ public class AutoShare extends TriggerAction {
@Override
public Object execute(OperatingContext operatingContext) throws TriggerException {
final JSONObject content = (JSONObject) actionContext.getActionContent();
final ID recordId = operatingContext.getAnyRecord().getPrimary();
final ID recordId = operatingContext.getFixedRecordId();
JSONArray shareTo = content.getJSONArray("shareTo");
Set<ID> toUsers = UserHelper.parseUsers(shareTo, recordId, true);

View file

@ -14,6 +14,7 @@ import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.metadata.MissingMetaExcetion;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
@ -43,7 +44,9 @@ import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 字段聚合场景 N>1
@ -139,7 +142,7 @@ public class FieldAggregation extends TriggerAction {
@Override
public Object execute(OperatingContext operatingContext) throws TriggerException {
final String chainName = String.format("%s:%s:%s", actionContext.getConfigId(),
operatingContext.getAnyRecord().getPrimary(), operatingContext.getAction().getName());
operatingContext.getFixedRecordId(), operatingContext.getAction().getName());
final List<String> tschain = checkTriggerChain(chainName);
if (tschain == null) return TriggerResult.triggerOnce();
@ -159,7 +162,7 @@ public class FieldAggregation extends TriggerAction {
JSONObject dataFilter = ((JSONObject) actionContext.getActionContent()).getJSONObject("dataFilter");
String dataFilterSql = null;
if (dataFilter != null && !dataFilter.isEmpty()) {
dataFilterSql = new AdvFilterParser(dataFilter).toSqlWhere();
dataFilterSql = new AdvFilterParser(dataFilter, operatingContext.getFixedRecordId()).toSqlWhere();
}
String filterSql = followSourceWhere;
@ -190,13 +193,13 @@ public class FieldAggregation extends TriggerAction {
if (evalValue instanceof Date) targetRecord.setDate(targetField, (Date) evalValue);
else targetRecord.setNull(targetField);
} else if (dt == DisplayType.NTEXT || dt == DisplayType.N2NREFERENCE) {
} else if (dt == DisplayType.NTEXT || dt == DisplayType.N2NREFERENCE || dt == DisplayType.FILE) {
Object[] oArray = (Object[]) evalValue;
if (oArray.length == 0) {
targetRecord.setNull(targetField);
} else if (dt == DisplayType.NTEXT) {
// 使用文本
// ID 使用文本
if (oArray[0] instanceof ID) {
List<String> labelList = new ArrayList<>();
for (Object id : oArray) {
@ -207,10 +210,16 @@ public class FieldAggregation extends TriggerAction {
String join = StringUtils.join(oArray, ", ");
targetRecord.setString(targetField, join);
} else if (dt == DisplayType.N2NREFERENCE) {
// 强制去重
Set<ID> idSet = new LinkedHashSet<>();
for (Object id : oArray) idSet.add((ID) id);
targetRecord.setIDArray(targetField, idSet.toArray(new ID[0]));
} else {
List<ID> idList = new ArrayList<>();
for (Object id : oArray) idList.add((ID) id);
targetRecord.setIDArray(targetField, idList.toArray(new ID[0]));
String join = JSON.toJSONString(oArray);
targetRecord.setString(targetField, join);
}
} else {
@ -293,6 +302,13 @@ public class FieldAggregation extends TriggerAction {
targetEntity = MetadataHelper.getEntity(targetFieldEntity[1]);
String followSourceField = targetFieldEntity[0];
if (TARGET_ANY.equals(followSourceField)) {
TargetWithMatchFields targetWithMatchFields = new TargetWithMatchFields();
targetRecordId = targetWithMatchFields.match(actionContext);
followSourceWhere = StringUtils.join(targetWithMatchFields.getQFieldsFollow().iterator(), " and ");
return;
}
if (!sourceEntity.containsField(followSourceField)) {
throw new MissingMetaExcetion(followSourceField, sourceEntity.getName());
}
@ -328,8 +344,7 @@ public class FieldAggregation extends TriggerAction {
protected boolean isCurrentSame(Record record) {
if (!ignoreSame) return false;
Record c = Application.getQueryFactory().recordNoFilter(
record.getPrimary(), record.getAvailableFields().toArray(new String[0]));
Record c = QueryHelper.querySnap(record);
return new RecordDifference(record).isSame(c, false);
}

View file

@ -15,6 +15,7 @@ import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.general.OperatingContext;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.service.trigger.TriggerAction;
import lombok.extern.slf4j.Slf4j;
/**
@ -36,9 +37,7 @@ public class FieldAggregationRefresh {
/**
*/
public void refresh() {
if (operatingContext.getBeforeRecord() == null || operatingContext.getAfterRecord() == null) {
return;
}
if (operatingContext.getBeforeRecord() == null || operatingContext.getAfterRecord() == null) return;
ID triggerUser = UserService.SYSTEM_USER;
ActionContext parentAc = parent.getActionContext();
@ -47,6 +46,11 @@ public class FieldAggregationRefresh {
String[] targetFieldEntity = ((JSONObject) parentAc.getActionContent()).getString("targetEntity").split("\\.");
String followSourceField = targetFieldEntity[0];
if (TriggerAction.TARGET_ANY.equals(followSourceField)) {
log.debug("Use match-fields does not support refresh");
return;
}
final ID beforeValue = operatingContext.getBeforeRecord().getID(followSourceField);
final ID afterValue = operatingContext.getAfterRecord().getID(followSourceField);

View file

@ -42,6 +42,7 @@ import com.rebuild.core.support.general.ContentWithFieldVars;
import com.rebuild.core.support.general.N2NReferenceSupport;
import com.rebuild.utils.CommonsUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
@ -105,7 +106,7 @@ public class FieldWriteback extends FieldAggregation {
@Override
public Object execute(OperatingContext operatingContext) throws TriggerException {
final String chainName = String.format("%s:%s:%s", actionContext.getConfigId(),
operatingContext.getAnyRecord().getPrimary(), operatingContext.getAction().getName());
operatingContext.getFixedRecordId(), operatingContext.getAction().getName());
final List<String> tschain = checkTriggerChain(chainName);
if (tschain == null) return TriggerResult.triggerOnce();
@ -130,7 +131,7 @@ public class FieldWriteback extends FieldAggregation {
for (ID targetRecordId : targetRecordIds) {
// 删除时无需更新自己
if (operatingContext.getAction() == BizzPermission.DELETE
&& targetRecordId.equals(operatingContext.getAnyRecord().getPrimary())) {
&& targetRecordId.equals(operatingContext.getFixedRecordId())) {
continue;
}
@ -200,8 +201,14 @@ public class FieldWriteback extends FieldAggregation {
targetRecordIds = new HashSet<>();
if (SOURCE_SELF.equalsIgnoreCase(targetFieldEntity[0])) {
// 自己更新自己
// v35
if (TARGET_ANY.equals(targetFieldEntity[0])) {
TargetWithMatchFields targetWithMatchFields = new TargetWithMatchFields();
ID[] ids = targetWithMatchFields.matchMulti(actionContext);
CollectionUtils.addAll(targetRecordIds, ids);
}
// 自己更新自己
else if (SOURCE_SELF.equalsIgnoreCase(targetFieldEntity[0])) {
targetRecordIds.add(actionContext.getSourceRecord());
}
// 1:1
@ -247,13 +254,13 @@ public class FieldWriteback extends FieldAggregation {
// N:N v3.1
Field targetField = targetEntity.getField(targetFieldEntity[0]);
if (targetField.getType() == FieldType.REFERENCE_LIST) {
Set<ID> set = N2NReferenceSupport.findReferences(targetField, operatingContext.getAnyRecord().getPrimary());
Set<ID> set = N2NReferenceSupport.findReferences(targetField, operatingContext.getFixedRecordId());
targetRecordIds.addAll(set);
} else {
String sql = String.format("select %s from %s where %s = ?",
targetEntity.getPrimaryField().getName(), targetFieldEntity[1], targetFieldEntity[0]);
Object[][] array = Application.createQueryNoFilter(sql)
.setParameter(1, operatingContext.getAnyRecord().getPrimary())
.setParameter(1, operatingContext.getFixedRecordId())
.array();
for (Object[] o : array) {
@ -461,7 +468,13 @@ public class FieldWriteback extends FieldAggregation {
} else if (dt == DisplayType.DECIMAL) {
targetRecord.setDouble(targetField, ObjectUtils.toDouble(newValue));
} else if (dt == DisplayType.DATE || dt == DisplayType.DATETIME) {
targetRecord.setDate(targetField, (Date) newValue);
if (newValue instanceof Date) {
targetRecord.setDate(targetField, (Date) newValue);
} else {
Date newValueCast = CalendarUtils.parse(newValue.toString());
if (newValueCast == null) log.warn("Cannot cast string to date : {}", newValue);
else targetRecord.setDate(targetField, newValueCast);
}
} else {
newValue = checkoutFieldValue(newValue, targetFieldEasy);
if (newValue != null) {

View file

@ -33,7 +33,11 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import java.util.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 分组聚合场景 N[+N]>1
@ -137,6 +141,7 @@ public class GroupAggregation extends FieldAggregation {
? null : operatingContext.getBeforeRecord().getObjectValue(sourceField);
Object afterValue = operatingContext.getAfterRecord().getObjectValue(sourceField);
if (NullValue.isNull(beforeValue) && NullValue.isNull(afterValue)) {
// All null
} else {
valueChanged.put(sourceField, new Object[] { beforeValue, afterValue });
}
@ -205,7 +210,7 @@ public class GroupAggregation extends FieldAggregation {
// 需要匹配等级的值
if (sourceFieldLevel != targetFieldLevel) {
ID parent = getItemWithLevel((ID) val, targetFieldLevel);
ID parent = TargetWithMatchFields.getItemWithLevel((ID) val, targetFieldLevel);
Assert.isTrue(parent != null, Language.L("分类字段等级不兼容"));
val = parent;
@ -231,7 +236,7 @@ public class GroupAggregation extends FieldAggregation {
if (!valueChanged.isEmpty()) {
this.groupAggregationRefresh = new GroupAggregationRefresh(this, qFieldsRefresh);
} else {
log.warn("All group-fields are null, ignored");
log.warn("All values of group-fields are null, ignored");
}
return;
}
@ -283,21 +288,4 @@ public class GroupAggregation extends FieldAggregation {
targetRecordId = newTargetRecord.getPrimary();
}
private ID getItemWithLevel(ID itemId, int specLevel) {
ID current = itemId;
for (int i = 0; i < 4; i++) {
Object[] o = Application.createQueryNoFilter(
"select level,parent from ClassificationData where itemId = ?")
.setParameter(1, current)
.unique();
if (o == null) break;
if ((int) o[0] < specLevel) break;
if ((int) o[0] == specLevel) return current;
else current = (ID) o[1];
}
return null;
}
}

View file

@ -52,6 +52,7 @@ public class SendNotification extends TriggerAction {
private static final int MTYPE_DINGTALK = 5; // 钉钉群
private static final int UTYPE_USER = 1; // 内部用户
private static final int UTYPE_ACCOUNT = 2; // 外部人员
private static final int UTYPE_ACCOUNT20 = 20; // 外部人员-输入
private static final int UTYPE_WXWORK = 4; // 企微群
private static final int UTYPE_DINGTALK = 5; // 钉钉群
@ -87,8 +88,8 @@ public class SendNotification extends TriggerAction {
}
Set<Object> s;
if (userType == UTYPE_ACCOUNT) {
s = sendToAccounts(operatingContext);
if (userType == UTYPE_ACCOUNT || userType == UTYPE_ACCOUNT20) {
s = sendToAccounts(operatingContext, userType);
} else if (userType == UTYPE_WXWORK) {
s = sendToWxwork(operatingContext);
} else if (userType == UTYPE_DINGTALK) {
@ -115,62 +116,68 @@ public class SendNotification extends TriggerAction {
Set<Object> send = new HashSet<>();
for (ID user : toUsers) {
if (send.contains(user)) continue;
if (msgType == MTYPE_MAIL) {
String emailAddr = Application.getUserStore().getUser(user).getEmail();
if (RegexUtils.isEMail(emailAddr) && !send.contains(emailAddr)) {
SMSender.sendMailAsync(emailAddr, message[1], message[0]);
if (RegexUtils.isEMail(emailAddr)) {
String mdHtml = MarkdownUtils.render(message[0]);
SMSender.sendMailAsync(emailAddr, message[1], mdHtml);
send.add(emailAddr);
}
}
} else if (msgType == MTYPE_SMS) {
if (msgType == MTYPE_SMS) {
String mobileAddr = Application.getUserStore().getUser(user).getWorkphone();
if (RegexUtils.isCNMobile(mobileAddr) && !send.contains(mobileAddr)) {
if (RegexUtils.isCNMobile(mobileAddr)) {
SMSender.sendSMSAsync(mobileAddr, message[0]);
send.add(mobileAddr);
}
}
} else {
// TYPE_NOTIFICATION
if (!send.contains(user)) {
Message m = MessageBuilder.createMessage(user, message[0], Message.TYPE_DEFAULT, actionContext.getSourceRecord());
Application.getNotifications().send(m);
send.add(user);
}
if (msgType == MTYPE_NOTIFICATION) {
Message m = MessageBuilder.createMessage(user, message[0], Message.TYPE_DEFAULT, actionContext.getSourceRecord());
Application.getNotifications().send(m);
send.add(user);
}
}
return send;
}
private Set<Object> sendToAccounts(OperatingContext operatingContext) {
private Set<Object> sendToAccounts(OperatingContext operatingContext, int userType) {
final JSONObject content = (JSONObject) actionContext.getActionContent();
final int msgType = content.getIntValue("type");
JSONArray fieldsDef = content.getJSONArray("sendTo");
if (fieldsDef == null || fieldsDef.isEmpty()) return null;
List<String> validFields = new ArrayList<>();
for (Object field : fieldsDef) {
if (MetadataHelper.getLastJoinField(actionContext.getSourceEntity(), field.toString()) != null) {
validFields.add(field.toString());
}
}
if (validFields.isEmpty()) return null;
Object[] to = null;
// v3.4 删除就尝试从快照中取
if (operatingContext.getAction() == BizzPermission.DELETE) {
Record beforeRecord = operatingContext.getBeforeRecord();
if (beforeRecord != null) {
List<String> toList = new ArrayList<>();
for (String s : validFields) {
Object v;
if ((v = beforeRecord.getObjectValue(s)) != null) toList.add(v.toString());
}
to = toList.toArray(new String[0]);
}
if (userType == UTYPE_ACCOUNT20) {
to = content.getString("sendTo").split("[,;]");
} else {
to = Application.getQueryFactory().uniqueNoFilter(
actionContext.getSourceRecord(), validFields.toArray(new String[0]));
JSONArray fieldsDef = content.getJSONArray("sendTo");
if (fieldsDef == null || fieldsDef.isEmpty()) return null;
List<String> validFields = new ArrayList<>();
for (Object field : fieldsDef) {
if (MetadataHelper.getLastJoinField(actionContext.getSourceEntity(), field.toString()) != null) {
validFields.add(field.toString());
}
}
if (validFields.isEmpty()) return null;
// v3.4 删除就尝试从快照中取
if (operatingContext.getAction() == BizzPermission.DELETE) {
Record beforeRecord = operatingContext.getBeforeRecord();
if (beforeRecord != null) {
List<String> toList = new ArrayList<>();
for (String s : validFields) {
Object v;
if ((v = beforeRecord.getObjectValue(s)) != null) toList.add(v.toString());
}
to = toList.toArray(new String[0]);
}
} else {
to = Application.getQueryFactory().uniqueNoFilter(
actionContext.getSourceRecord(), validFields.toArray(new String[0]));
}
}
if (to == null) return null;
@ -180,7 +187,7 @@ public class SendNotification extends TriggerAction {
for (Object item : to) {
if (item == null) continue;
String mobileOrEmail = item.toString();
String mobileOrEmail = item.toString().trim();
if (send.contains(mobileOrEmail)) continue;
if (msgType == MTYPE_SMS && RegexUtils.isCNMobile(mobileOrEmail)) {

View file

@ -0,0 +1,243 @@
/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.service.trigger.impl;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.metadata.MissingMetaExcetion;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.general.ClassificationManager;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyFieldConfigProps;
import com.rebuild.core.service.trigger.ActionContext;
import com.rebuild.core.support.i18n.Language;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 寻找目标实体记录
*
* @author devezhao
* @since 2023/10/25
*/
@Slf4j
public class TargetWithMatchFields {
private Entity sourceEntity;
@Getter
private List<String> qFieldsFollow;
@Getter
private Object targetRecordId;
protected TargetWithMatchFields() {
super();
}
/**
* @param actionContext
* @return
*/
public ID match(ActionContext actionContext) {
return (ID) match(actionContext, false);
}
/**
* @param actionContext
* @return
*/
public ID[] matchMulti(ActionContext actionContext) {
Object o = match(actionContext, true);
return o == null ? new ID[0] : (ID[]) o;
}
private Object match(ActionContext actionContext, boolean m) {
if (sourceEntity != null) return targetRecordId; // 已做匹配
final JSONObject actionContent = (JSONObject) actionContext.getActionContent();
sourceEntity = actionContext.getSourceEntity();
Entity targetEntity = MetadataHelper.getEntity(actionContent.getString("targetEntity").split("\\.")[1]);
// 0.字段关联 <Source, Target>
Map<String, String> matchFieldsMapping = new HashMap<>();
for (Object o : actionContent.getJSONArray("targetEntityMatchFields")) {
JSONObject item = (JSONObject) o;
String sourceField = item.getString("sourceField");
String targetField = item.getString("targetField");
if (MetadataHelper.getLastJoinField(sourceEntity, sourceField) == null) {
throw new MissingMetaExcetion(sourceField, sourceEntity.getName());
}
if (!targetEntity.containsField(targetField)) {
throw new MissingMetaExcetion(targetField, targetEntity.getName());
}
matchFieldsMapping.put(sourceField, targetField);
}
if (matchFieldsMapping.isEmpty()) {
log.warn("No match-fields specified");
return null;
}
// 1.源记录数据
String aSql = String.format("select %s from %s where %s = ?",
StringUtils.join(matchFieldsMapping.keySet().iterator(), ","),
sourceEntity.getName(), sourceEntity.getPrimaryField().getName());
final Record sourceRecord = Application.createQueryNoFilter(aSql)
.setParameter(1, actionContext.getSourceRecord())
.record();
// 2.找到目标记录
boolean allNull = true;
List<String> qFields = new ArrayList<>();
qFieldsFollow = new ArrayList<>();
for (Map.Entry<String, String> e : matchFieldsMapping.entrySet()) {
String sourceField = e.getKey();
String targetField = e.getValue();
Object val = sourceRecord.getObjectValue(sourceField);
if (val == null) {
qFields.add(String.format("%s is null", targetField));
qFieldsFollow.add(String.format("%s is null", sourceField));
} else {
//noinspection ConstantConditions
EasyField sourceFieldEasy = EasyMetaFactory.valueOf(MetadataHelper.getLastJoinField(sourceEntity, sourceField));
//noinspection ConstantConditions
EasyField targetFieldEasy = EasyMetaFactory.valueOf(MetadataHelper.getLastJoinField(targetEntity, targetField));
// @see Dimension#getSqlName
// 日期分组
if (sourceFieldEasy.getDisplayType() == DisplayType.DATE
|| sourceFieldEasy.getDisplayType() == DisplayType.DATETIME) {
String formatKey = sourceFieldEasy.getDisplayType() == DisplayType.DATE
? EasyFieldConfigProps.DATE_FORMAT
: EasyFieldConfigProps.DATETIME_FORMAT;
int sourceFieldLength = StringUtils.defaultIfBlank(
sourceFieldEasy.getExtraAttr(formatKey), sourceFieldEasy.getDisplayType().getDefaultFormat()).length();
// 目标字段仅使用日期
int targetFieldLength = StringUtils.defaultIfBlank(
targetFieldEasy.getExtraAttr(EasyFieldConfigProps.DATE_FORMAT), targetFieldEasy.getDisplayType().getDefaultFormat()).length();
// 目标格式长度必须小于等于源格式
Assert.isTrue(targetFieldLength <= sourceFieldLength,
Language.L("日期字段格式不兼容") + String.format(" (%d,%d)", targetFieldLength, sourceFieldLength));
if (targetFieldLength == 4) { // 'Y'
targetField = String.format("DATE_FORMAT(%s,'%s')", targetField, "%Y");
val = CalendarUtils.format("yyyy", (Date) val);
} else if (targetFieldLength == 7) { // 'M'
targetField = String.format("DATE_FORMAT(%s,'%s')", targetField, "%Y-%m");
val = CalendarUtils.format("yyyy-MM", (Date) val);
} else { // 'D' is default
targetField = String.format("DATE_FORMAT(%s,'%s')", targetField, "%Y-%m-%d");
val = CalendarUtils.format("yyyy-MM-dd", (Date) val);
}
}
// 分类分组
else if (sourceFieldEasy.getDisplayType() == DisplayType.CLASSIFICATION) {
int sourceFieldLevel = ClassificationManager.instance.getOpenLevel(
MetadataHelper.getLastJoinField(sourceEntity, sourceField));
int targetFieldLevel = ClassificationManager.instance.getOpenLevel(
MetadataHelper.getLastJoinField(targetEntity, targetField));
// 目标等级必须小于等于源等级
Assert.isTrue(targetFieldLevel <= sourceFieldLevel,
Language.L("分类字段等级不兼容") + String.format(" (%d,%d)", targetFieldLevel, sourceFieldLevel));
// 需要匹配等级的值
if (sourceFieldLevel != targetFieldLevel) {
ID parent = getItemWithLevel((ID) val, targetFieldLevel);
Assert.isTrue(parent != null, Language.L("分类字段等级不兼容"));
val = parent;
sourceRecord.setID(sourceField, (ID) val);
for (int i = 0; i < sourceFieldLevel - targetFieldLevel; i++) {
//noinspection StringConcatenationInLoop
sourceField += ".parent";
}
}
}
qFields.add(String.format("%s = '%s'", targetField, val));
qFieldsFollow.add(String.format("%s = '%s'", sourceField, val));
allNull = false;
}
}
if (allNull) {
log.warn("All values of match-fields are null, ignored");
return null;
}
aSql = String.format("select %s from %s where ( %s )",
targetEntity.getPrimaryField().getName(), targetEntity.getName(),
StringUtils.join(qFields.iterator(), " and "));
// 多个 1:N
if (m) {
Object[][] array = Application.createQueryNoFilter(aSql).array();
List<ID> targetRecordIds = new ArrayList<>();
for (Object[] o : array) targetRecordIds.add((ID) o[0]);
targetRecordId = targetRecordIds.toArray(new ID[0]);
return targetRecordId;
}
// 单个
Object[] targetRecord = Application.createQueryNoFilter(aSql).unique();
targetRecordId = targetRecord == null ? null : (ID) targetRecord[0];
return targetRecordId;
}
/**
* 分类字段级别
*
* @param itemId
* @param specLevel
* @return
*/
static ID getItemWithLevel(ID itemId, int specLevel) {
ID current = itemId;
for (int i = 0; i < 4; i++) {
Object[] o = Application.createQueryNoFilter(
"select level,parent from ClassificationData where itemId = ?")
.setParameter(1, current)
.unique();
if (o == null) break;
if ((int) o[0] < specLevel) break;
if ((int) o[0] == specLevel) return current;
else current = (ID) o[1];
}
return null;
}
}

View file

@ -108,6 +108,8 @@ public enum ConfigurationItem {
PortalBaiduMapAk,
PortalOfficePreviewUrl,
PortalUploadMaxSize(200),
MobileNavStyle(34),
PageMourningMode(false),
// !!! 命令行适用
DataDirectory, // 数据目录
@ -118,6 +120,7 @@ public enum ConfigurationItem {
SecurityEnhanced(false), // 安全增强
TrustedAllUrl(false), // 可信外部地址
LibreofficeBin, // Libreoffice 命令
UnsafeImgAccess(false), // 不安全图片访问
;
@ -136,6 +139,7 @@ public enum ConfigurationItem {
|| SecurityEnhanced.name().equalsIgnoreCase(name)
|| TrustedAllUrl.name().equalsIgnoreCase(name)
|| LibreofficeBin.name().equalsIgnoreCase(name)
|| UnsafeImgAccess.name().equals(name)
|| SN.name().equalsIgnoreCase(name);
}

View file

@ -23,10 +23,8 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* !!!! 请勿修改或删除本文件
* !!!! 请严格遵守REBUILD 用户服务协议https://getrebuild.com/legal/service-terms
*/
// !!!! 请勿对本文件做任何改动否则导致的一切损失由您自行承担
// !!!! 请严格遵守REBUILD 用户服务协议https://getrebuild.com/legal/service-terms
@Slf4j
public final class License {

View file

@ -10,7 +10,7 @@ package com.rebuild.core.support;
import cn.devezhao.commons.CodecUtils;
import com.rebuild.core.Application;
import com.rebuild.core.cache.CommonsCache;
import org.apache.commons.lang3.RandomUtils;
import com.rebuild.utils.CommonsUtils;
/**
* 验证码助手
@ -44,7 +44,7 @@ public class VerfiyCode {
} else if (level == 2) {
vcode = CodecUtils.randomCode(8);
} else {
vcode = RandomUtils.nextInt(100000, 999999) + "";
vcode = CommonsUtils.randomInt(100000, 999999) + "";
}
// 缓存 10 分钟

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.support.general;
import cn.devezhao.commons.web.ServletUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
@ -15,7 +16,9 @@ import com.rebuild.core.Application;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.service.query.ParseHelper;
import com.rebuild.core.support.SetUser;
import org.apache.commons.lang.math.NumberUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;
@ -66,12 +69,14 @@ public class BatchOperatorQuery extends SetUser {
public JSONObject wrapQueryData(int maxRows, boolean clearFields) {
if (this.dataRange != DR_PAGED) {
queryData.put("pageNo", 1);
queryData.put("pageSize", maxRows); // Max
queryData.put("pageSize", maxRows);
}
if (this.dataRange == DR_SELECTED || this.dataRange == DR_ALL) {
queryData.remove("filter");
queryData.remove("advFilter");
}
if (this.dataRange == DR_SELECTED) {
JSONObject idsItem = new JSONObject();
idsItem.put("op", ParseHelper.IN);
@ -88,6 +93,7 @@ public class BatchOperatorQuery extends SetUser {
if (clearFields) {
queryData.put("fields", new String[]{getEntity().getPrimaryField().getName()});
}
queryData.put("reload", Boolean.FALSE);
return queryData;
}
@ -109,7 +115,7 @@ public class BatchOperatorQuery extends SetUser {
*
* @return
*/
public ID[] getQueryedRecords() {
public ID[] getQueryedRecordIds() {
if (this.dataRange == DR_SELECTED) {
String selected = queryData.getString("_selected");
@ -122,14 +128,13 @@ public class BatchOperatorQuery extends SetUser {
return ids.toArray(new ID[0]);
}
String sql = String.format("select %s %s",
getEntity().getPrimaryField().getName(), getFromClauseSql());
String sql = String.format("select %s %s", getEntity().getPrimaryField().getName(), getFromClauseSql());
int pageNo = queryData.getIntValue("pageNo");
int pageSize = queryData.getIntValue("pageSize");
Object[][] array = Application.createQuery(sql, getUser())
.setLimit(pageSize, pageNo * pageSize - pageSize)
.setTimeout(60)
.setTimeout(180)
.array();
Set<ID> ids = new HashSet<>();
@ -140,7 +145,20 @@ public class BatchOperatorQuery extends SetUser {
}
private Entity getEntity() {
String entityName = queryData.getString("entity");
return MetadataHelper.getEntity(entityName);
return MetadataHelper.getEntity(queryData.getString("entity"));
}
// --
/**
* @param request
* @return
*/
public static BatchOperatorQuery create(HttpServletRequest request, String entity) {
JSONObject requestData = (JSONObject) ServletUtils.getRequestJson(request);
requestData.put("entity", entity);
int dr = NumberUtils.toInt(request.getParameter("dr"), DR_PAGED);
return new BatchOperatorQuery(dr, requestData);
}
}

Some files were not shown because too many files have changed in this diff Show more