Mm not login (#939)

* Add option to restrict login during maintenance mode

* i18n for entity

* close #929
This commit is contained in:
REBUILD 企业管理系统 2025-07-31 23:32:22 +08:00 committed by GitHub
parent 7963a7fa85
commit 8c97e7d0d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 311 additions and 45 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 164bbdfe55e2f70bc1bcd615f2498ad32fe3a916
Subproject commit 87b722817d9c088fba923da34542d09cbc0f7e91

View file

@ -404,7 +404,7 @@
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
@ -429,12 +429,12 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>

View file

@ -127,8 +127,8 @@ public abstract class BaseEasyMeta<T extends BaseMeta> implements BaseMeta, JSON
* @see com.rebuild.core.support.i18n.Language#L(BaseMeta)
*/
public String getLabel() {
String l = Language.L(getRawMeta());
return CommonsUtils.escapeHtml(l);
String L42 = Language.L(getRawMeta());
return CommonsUtils.escapeHtml(L42);
}
/**

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.support.i18n;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.metadata.BaseMeta;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
@ -192,8 +193,14 @@ public class Language implements Initialization {
}
public static String L(BaseMeta meta) {
String lang = getCurrentBundle().getLang(meta.getDescription());
return lang == null ? meta.getDescription() : lang;
// v4.2
String key = "META.";
if (meta instanceof Field) key += ((Field) meta).getOwnEntity().getName() + "." + meta.getName();
else key += meta.getName();
String L42 = getCurrentBundle().getLang(key.toUpperCase());
if (L42 == null) L42 = getCurrentBundle().getLang(meta.getDescription());
return L42 == null ? meta.getDescription() : L42;
}
public static String L(DisplayType type) {

View file

@ -187,12 +187,15 @@ public class LanguageBundle implements JSONable {
// -- 系统语言
public static final String SYS_LC = "zh_CN";
static final LanguageBundle SYS_BUNDLE = new LanguageBundle() {
private static final long serialVersionUID = -5127621395095384712L;
@Override
public String getLocale() {
return SYS_LC;
}
@Override
public String L(String key, Object... placeholders) {
String lang = getLang(key);

View file

@ -529,7 +529,7 @@ public class ConfigurationController extends BaseController {
*/
public static MaintenanceMode getCurrentMm() {
// 过期
if (CURRENT_MM != null && CURRENT_MM.getStartTime().getTime() - CalendarUtils.now().getTime() <= 0) {
if (CURRENT_MM != null && CURRENT_MM.getEndTime().compareTo(CalendarUtils.now()) <= 0) {
CURRENT_MM = null;
}
return CURRENT_MM;
@ -541,6 +541,7 @@ public class ConfigurationController extends BaseController {
Date startTime;
Date endTime;
String note;
boolean notLogin;
}
@GetMapping("systems/maintenance-mode")

View file

@ -97,6 +97,7 @@ public class ProtectedAdmin {
EXF("/extform", "外部表单"),
PRO("/project", "项目"),
FJS("/frontjs-code", "FrontJS"),
I18("/i18n/editor", "多语言"),
USR("/bizuser/users;bizuser/departments", "部门用户"),
ROL("/bizuser/role-privileges;/bizuser/role", "角色权限"),
TEM("/bizuser/teams", "团队"),

View file

@ -12,11 +12,13 @@ import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Record;
import cn.devezhao.persist4j.engine.ID;
import cn.devezhao.persist4j.metadata.BaseMeta;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.UserContextHolder;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.general.ClassificationManager;
import com.rebuild.core.configuration.general.EasyActionManager;
@ -71,6 +73,8 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import static com.rebuild.web.commons.LanguageController.putLocales;
/**
* @author Zixin (RB)
* @since 08/03/2018
@ -457,4 +461,44 @@ public class MetaEntityController extends EntityController {
res.sort(Comparator.comparing((Object o) -> ((JSONObject) o).getString("label")));
return res;
}
/**
* @param entity
* @return
* @see com.rebuild.web.admin.LanguageAdminController
*/
@GetMapping("entity/{entity}/i18n")
public ModelAndView pageI18n(@PathVariable String entity) {
ModelAndView mv = createModelAndView("/admin/metadata/entity-i18n");
putLocales(mv, UserContextHolder.getLocale());
setEntityBase(mv, entity);
return mv;
}
@GetMapping("entity/{entity}/i18n-list")
public RespBody listI18n(@PathVariable String entity) {
Entity e = MetadataHelper.getEntity(entity);
String key = "META." + e.getName();
Set<String> locales = Application.getLanguage().availableLocales().keySet();
List<Map<String, String>> i18nList = new ArrayList<>();
i18nList.add(buildI18n(e, key, locales));
key += ".";
for (Field field : MetadataSorter.sortFields(e)) {
i18nList.add(buildI18n(field, key + field.getName(), locales));
}
return RespBody.ok(i18nList);
}
private Map<String, String> buildI18n(BaseMeta entityOrField, String key, Set<String> locales) {
key = key.toUpperCase();
Map<String, String> i18n = new HashMap<>();
i18n.put("_key", key);
i18n.put("_def", entityOrField.getDescription());
for (String L : locales) {
i18n.put(L, Application.getLanguage().getBundle(L).getLang(key));
}
return i18n;
}
}

View file

@ -21,7 +21,6 @@ import com.rebuild.core.support.setup.Installer;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import com.rebuild.web.user.signup.LoginController;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
@ -41,6 +40,8 @@ import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import static com.rebuild.web.commons.LanguageController.putLocales;
/**
* @author devezhao
* @since 2019/11/25
@ -65,7 +66,7 @@ public class InstallController extends BaseController implements InstallState {
mv.getModel().put("Version", Application.VER);
// 切换语言
LoginController.putLocales(mv, AppUtils.getReuqestLocale(request));
putLocales(mv, AppUtils.getReuqestLocale(request));
return mv;
}

View file

@ -8,6 +8,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.web.commons;
import cn.devezhao.commons.web.ServletUtils;
import com.rebuild.core.Application;
import com.rebuild.core.support.i18n.LanguageBundle;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.Etag;
@ -15,9 +16,13 @@ import com.rebuild.web.BaseController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 多语言控制
@ -41,4 +46,30 @@ public class LanguageController extends BaseController {
"window._LANGBUNDLE = " + bundle.toJSON().toJSONString());
}
}
// --
/**
* 可用语言
*
* @param into
* @param currentLocale
*/
public static void putLocales(ModelAndView into, String currentLocale) {
String currentLocaleText = null;
List<String[]> alangs = new ArrayList<>();
for (Map.Entry<String, String> L : Application.getLanguage().availableLocales().entrySet()) {
String text = L.getValue();
text = text.split("\\(")[0].trim();
alangs.add(new String[]{L.getKey(), text});
if (L.getKey().equals(currentLocale)) {
currentLocaleText = text;
}
}
into.getModelMap().put("currentLang", currentLocaleText);
into.getModelMap().put("availableLangs", alangs);
}
}

View file

@ -41,11 +41,11 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.rebuild.web.commons.LanguageController.putLocales;
/**
* @author Zixin (RB)
* @since 07/25/2018
@ -170,11 +170,8 @@ public class LoginController extends LoginAction {
// v34 维护计划
if (!UserHelper.isAdmin(loginUser.getId())) {
ConfigurationController.MaintenanceMode mm = ConfigurationController.getCurrentMm();
if (mm != null) {
long left = (mm.getStartTime().getTime() - CalendarUtils.now().getTime()) / 1000;
if (left <= 300) {
return RespBody.errorl("系统即将开始维护,暂时禁止登录");
}
if (mm != null && mm.isNotLogin() && mm.getStartTime().compareTo(CalendarUtils.now()) <= 0) {
return RespBody.errorl("系统即将开始维护,暂时禁止登录");
}
}
@ -302,31 +299,4 @@ public class LoginController extends LoginAction {
}
return v;
}
// --
/**
* 可用语言
*
* @param into
* @param currentLocale
*/
public static void putLocales(ModelAndView into, String currentLocale) {
String currentLocaleText = null;
List<String[]> alangs = new ArrayList<>();
for (Map.Entry<String, String> lc : Application.getLanguage().availableLocales().entrySet()) {
String lcText = lc.getValue();
lcText = lcText.split("\\(")[0].trim();
alangs.add(new String[] { lc.getKey(), lcText });
if (lc.getKey().equals(currentLocale)) {
currentLocaleText = lcText;
}
}
into.getModelMap().put("currentLang", currentLocaleText);
into.getModelMap().put("availableLangs", alangs);
}
}

View file

@ -80,6 +80,9 @@
<li th:class="${active == 'frontjs-code'} ? 'active'" th:if="${T(com.rebuild.web.admin.ProtectedAdmin).allow('FJS', #request)}">
<a th:href="@{/admin/frontjs-code}"><i class="icon mdi mdi-language-javascript"></i><span>FrontJS</span> <sup class="rbv"></sup></a>
</li>
<li th:class="${active == 'i18n-editor'} ? 'active'" th:if="${T(com.rebuild.web.admin.ProtectedAdmin).allow('I18', #request)}" th:classappend="bosskey-show--">
<a th:href="@{/admin/i18n/editor}"><i class="icon mdi mdi-translate-variant"></i><span>多语言</span> <sup class="rbv"></sup></a>
</li>
<li class="divider">[[${bundle.L('用户')}]]</li>
<li th:class="${active == 'users'} ? 'active'" th:if="${T(com.rebuild.web.admin.ProtectedAdmin).allow('USR', #request)}">
<a th:href="@{/admin/bizuser/users}"><i class="icon mdi mdi-account-multiple"></i><span>[[${bundle.L('部门用户')}]]</span></a>

View file

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:replace="~{/_include/header}" />
<meta name="page-help" content="https://getrebuild.com/docs/admin/entity/layout#%E5%88%97%E8%A1%A8%E9%A1%B5%E9%9D%A2" />
<title>[[${bundle.L('多语言')}]]</title>
<style>
#i18nList td {
padding-top: 4px;
padding-bottom: 4px;
}
#i18nList td textarea {
padding-top: 7px;
}
</style>
</head>
<body>
<div class="rb-wrapper rb-fixed-sidebar rb-collapsible-sidebar rb-collapsible-sidebar-hide-logo rb-aside rb-color-header" th:classappend="${sideCollapsedClazz}">
<th:block th:replace="~{/_include/nav-top}" />
<th:block th:replace="~{/_include/nav-left-admin(active='entities')}" />
<div class="rb-content">
<aside class="page-aside">
<div class="rb-scroller-aside rb-scroller">
<div class="aside-content">
<div class="content">
<div class="aside-header">
<button class="navbar-toggle collapsed" type="button"><span class="icon zmdi zmdi-caret-down"></span></button>
<span class="title">[[${entityLabel}]]</span>
<p class="description">[[${comments}]]</p>
</div>
</div>
<th:block th:replace="~{/admin/metadata/subnav-entity(active='i18n')}" />
</div>
</div>
</aside>
<div class="page-head">
<div class="page-head-title">[[${bundle.L('多语言')}]]</div>
</div>
<div class="main-content container-fluid pt-0">
<div class="card card-table">
<div class="card-body">
<div class="dataTables_wrapper container-fluid">
<div class="row rb-datatable-header">
<div class="col-sm-6">
<div class="dataTables_filter">
<div class="input-group input-search invisible">
<input class="form-control" type="text" th:placeholder="${bundle.L('查询')}" />
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"><i class="icon zmdi zmdi-search"></i></button>
</span>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="dataTables_oper">
<span class="J_show-locales">
<button class="btn btn-light w-auto dropdown-toggle ml-2" type="button" data-toggle="dropdown">
<i class="icon mdi mdi-translate-variant"></i>
</button>
<div class="dropdown-menu dropdown-menu-right p-0">
<div class="m-3">
<h5>[[${bundle.L('选择翻译语言')}]]</h5>
<label class="custom-control custom-control-sm custom-checkbox mb-2" th:each="L : ${availableLangs}">
<input class="custom-control-input" type="checkbox" th:value="${L[0]}" checked />
<span class="custom-control-label" th:text="${L[1]}"></span>
</label>
</div>
</div>
</span>
<button class="btn btn-primary J_save ml-2" type="button">[[${bundle.L('保存')}]]</button>
</div>
</div>
</div>
<div class="row rb-datatable-body">
<div class="col-sm-12">
<div class="rb-loading rb-loading-active data-list">
<table class="table table-hover table-fixed" id="i18nList">
<thead>
<tr>
<th data-locale="_def" width="25%">[[${bundle.L('实体')}]]/[[${bundle.L('字段')}]]</th>
<th th:data-locale="${L[0]}" th:text="${L[1]}" th:each="L : ${availableLangs}"></th>
</tr>
</thead>
<tbody>
<tr class="hide">
<td data-locale="_def"></td>
<td th:data-locale="${L[0]}" th:each="L : ${availableLangs}">
<textarea class="form-control form-control-sm row1x" th:placeholder="${bundle.L('默认')}"></textarea>
</td>
</tr>
</tbody>
</table>
<th:block th:replace="~{/_include/spinner}" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<th:block th:replace="~{/_include/footer}" />
<script>
window.__PageConfig = {
id: '[[${entityMetaId}]]',
isSuperAdmin: '[[${isSuperAdmin}]]' === 'true',
entityName: '[[${entityName}]]',
}
</script>
<script th:src="@{/assets/js/metadata/entity-switch.js}" type="text/babel"></script>
<script th:src="@{/assets/js/metadata/entity-i18n.js}" type="text/babel"></script>
</body>
</html>

View file

@ -19,6 +19,7 @@
</li>
<li th:class="${active == 'advanced'} ? 'active'"><a th:href="${(urlPrefix?:'') + 'advanced'}">[[${bundle.L('高级配置')}]]</a></li>
<li th:class="${active == 'overview'} ? 'active'" th:classappend="bosskey-show"><a th:href="${(urlPrefix?:'') + 'overview'}">[[${bundle.L('技术全览')}]] (LAB)</a></li>
<li th:class="${active == 'i18n'} ? 'active'" th:classappend="bosskey-show--"><a th:href="${(urlPrefix?:'') + 'i18n'}">[[${bundle.L('多语言')}]]</a></li>
</ul>
</div>
</th:block>

View file

@ -915,6 +915,11 @@ body.view-body {
margin: 0 0 0 5px;
}
textarea.row1x {
height: 37px !important;
resize: none;
}
textarea.row2x {
height: 52px !important;
resize: none;

View file

@ -198,6 +198,12 @@ class DlgMM extends RbAlert {
<label>{$L('弹窗附加内容')}</label>
<textarea className="form-control form-control-sm row2x" ref={(c) => (this._$note = c)} placeholder={$L('维护期间系统将无法使用请及时保存数据')} />
</div>
<div className="form-group">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mb-0">
<input className="custom-control-input" type="checkbox" ref={(c) => (this._$notLogin = c)} />
<span className="custom-control-label">{$L('维护时间内禁止登录')}</span>
</label>
</div>
<div className="form-group mb-2">
<button type="button" className="btn btn-danger" onClick={this._onConfirm}>
{$L('开启')}
@ -279,6 +285,7 @@ class DlgMM extends RbAlert {
startTime: $val(this._$startTime),
endTime: $val(this._$endTime),
note: $val(this._$note),
notLogin: $val(this._$notLogin),
}
}

View file

@ -0,0 +1,78 @@
/*!
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.
*/
const wpc = window.__PageConfig
$(document).ready(() => {
$unhideDropdown('.J_show-locales')
$('.J_show-locales input').on('click', () => {
const $table = $('#i18nList')
$table.find('[data-locale]').removeClass('hide')
let hide = []
$('.J_show-locales input:not(:checked)').each(function () {
hide.push($(this).val())
})
hide.forEach((locale) => {
$table.find(`[data-locale=${locale}]`).addClass('hide')
})
$storage.set('i18n-hide', hide.join(';'))
})
$.get(`./i18n-list?entity=${wpc.entityName}`, (res) => i18nList(res.data || []))
$('.J_save').on('click', () => {
const post = buildI18nList()
$.post('/admin/i18n/editor-post', JSON.stringify(post), (res) => {
if (res.error_code === 0) {
location.reload()
} else {
RbHighbar.error(res.error_msg)
}
})
})
})
function i18nList(data) {
const $tbody = $('#i18nList tbody')
const $tmpl = $('#i18nList tr.hide')
data.forEach((item) => {
const $tr = $tmpl.clone().removeClass('hide').appendTo($tbody)
for (let k in item) {
const $td = $tr.find(`td[data-locale=${k}]`)
if (k === '_def') $td.text(item[k]).attr('data-key', item._key)
else $td.find('textarea').val(item[k])
}
})
$('#i18nList').parent().removeClass('rb-loading-active')
$tmpl.remove()
// 记住隐藏
const hide = $storage.get('i18n-hide')
if (hide) {
hide.split(';').forEach((locale) => {
$(`.J_show-locales input[value=${locale}]`)[0].click()
})
}
}
function buildI18nList() {
let data = []
$('#i18nList tbody tr').each(function () {
const $tr = $(this)
const key = $tr.find('td[data-key]').data('key')
$tr.find('textarea').each(function () {
const text = $(this).val()
const locale = $(this).parent().data('locale')
data.push([locale, key, text || null])
})
})
return data
}