This commit is contained in:
haomingming
2026-06-16 10:32:04 +08:00
parent 52fd2e7560
commit e0b089fbf8
53 changed files with 3674 additions and 577 deletions
+5
View File
@@ -62,6 +62,11 @@
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
@@ -12,6 +12,11 @@ import com.writeoff.module.expert.model.ExpertBankCardInfo;
import com.writeoff.module.expert.model.ExpertInfo;
import com.writeoff.module.system.service.DataPermissionService;
import com.writeoff.security.AuthContext;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@@ -114,12 +119,31 @@ public class ExpertService {
StringBuilder whereClause = new StringBuilder(
"WHERE e.tenant_id=" + PLATFORM_TENANT_ID + " AND e.is_deleted=0"
);
List<Object> countArgs = new ArrayList<>();
if (keyword != null && !keyword.trim().isEmpty()) {
String kw = keyword.trim().replace("'", "''");
whereClause.append(" AND (e.expert_name LIKE '%").append(kw).append("%' OR e.id_no LIKE '%").append(kw).append("%')");
}
com.writeoff.module.system.service.DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
if (!scope.isExpertAll()) {
boolean hasIdScope = !scope.getExpertIds().isEmpty();
boolean hasOwnerScope = scope.isExpertOwnerOnly();
if (!hasIdScope && !hasOwnerScope) {
return new PageResult<ExpertInfo>(new ArrayList<>(), 0, safePage, safeSize);
}
whereClause.append(" AND (1=0");
if (hasIdScope) {
whereClause.append(" OR e.id IN (").append(scope.getExpertIds().stream().map(String::valueOf).collect(Collectors.joining(","))).append(")");
}
if (hasOwnerScope) {
Long userId = AuthContext.userId();
if (userId != null) {
whereClause.append(" OR e.created_by=").append(userId);
}
}
whereClause.append(")");
}
Integer total = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM expert e " + whereClause,
Integer.class
@@ -138,7 +162,6 @@ public class ExpertService {
sql.append(whereClause);
sql.append(" ORDER BY e.id DESC LIMIT ").append(safeSize).append(" OFFSET ").append(offset);
List<ExpertInfo> list = jdbcTemplate.query(sql.toString(), EXPERT_ROW_MAPPER);
list = filterByExpertScope(list);
List<ExpertInfo> maskedList = new java.util.ArrayList<ExpertInfo>(list.size());
for (ExpertInfo item : list) {
maskedList.add(maskSensitiveFields(item));
@@ -195,11 +218,13 @@ public class ExpertService {
@Transactional(rollbackFor = Exception.class)
public ExpertInfo create(CreateExpertRequest request) {
String idNo = request.getIdNo() == null ? "" : request.getIdNo().trim().toUpperCase();
request.setIdNo(idNo);
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
Integer.class,
PLATFORM_TENANT_ID,
request.getIdNo()
idNo
);
if (count != null && count > 0) {
throw new BusinessException(10001, "身份证号已存在");
@@ -617,11 +642,76 @@ public class ExpertService {
name
);
if (byName.isEmpty()) {
throw new BusinessException(10001, label + "不在平台字典内,请先在平台字典中维护");
String pinyinCode = getPinyin(name);
if (pinyinCode.length() > 40) {
pinyinCode = pinyinCode.substring(0, 40);
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
Integer.class,
dictType,
pinyinCode
);
if (count != null && count > 0) {
int i = 1;
while(true) {
String newCode = pinyinCode + "_" + i;
if (newCode.length() > 50) {
newCode = newCode.substring(newCode.length() - 50);
}
Integer c = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
Integer.class, dictType, newCode
);
if (c == null || c == 0) {
pinyinCode = newCode;
break;
}
i++;
}
}
Integer maxSort = jdbcTemplate.queryForObject(
"SELECT MAX(sort_no) FROM platform_dictionary_item WHERE dict_type=?",
Integer.class,
dictType
);
int nextSort = (maxSort == null ? 0 : maxSort) + 10;
jdbcTemplate.update(
"INSERT INTO platform_dictionary_item (dict_type, dict_code, dict_name, sort_no, status, remark, created_by, updated_by) " +
"VALUES (?, ?, ?, ?, 'ENABLED', 'Auto-imported', ?, ?)",
dictType, pinyinCode, name, nextSort, safeUserId(), safeUserId()
);
return new DictionaryItem(pinyinCode, name);
}
return byName.get(0);
}
private String getPinyin(String src) {
if (src == null || src.trim().isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
format.setVCharType(HanyuPinyinVCharType.WITH_V);
for (char c : src.trim().toCharArray()) {
if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
try {
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyins != null && pinyins.length > 0) {
result.append(pinyins[0]);
}
} catch (Exception e) {
result.append(c);
}
} else {
result.append(Character.toUpperCase(c));
}
}
return result.toString();
}
private void validateImportExpert(CreateExpertRequest request, Set<String> batchIdNos, Set<String> batchPhones) {
if (request == null) {
throw new BusinessException(10001, "导入行不能为空");
@@ -11,6 +11,11 @@ import com.writeoff.module.file.service.OssService;
import com.writeoff.module.expert.model.ExpertBankCardInfo;
import com.writeoff.module.expert.model.ExpertInfo;
import com.writeoff.security.AuthContext;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@@ -188,11 +193,13 @@ public class PlatformExpertService {
@Transactional(rollbackFor = Exception.class)
public ExpertInfo create(CreateExpertRequest request) {
String idNo = request.getIdNo() == null ? "" : request.getIdNo().trim().toUpperCase();
request.setIdNo(idNo);
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM expert WHERE tenant_id=? AND id_no=? AND is_deleted=0",
Integer.class,
PLATFORM_TENANT_ID,
request.getIdNo()
idNo
);
if (count != null && count > 0) {
throw new BusinessException(10001, "身份证号已存在");
@@ -614,11 +621,76 @@ public class PlatformExpertService {
name
);
if (byName.isEmpty()) {
throw new BusinessException(10001, label + "不在平台字典内,请先在平台字典中维护");
String pinyinCode = getPinyin(name);
if (pinyinCode.length() > 40) {
pinyinCode = pinyinCode.substring(0, 40);
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
Integer.class,
dictType,
pinyinCode
);
if (count != null && count > 0) {
int i = 1;
while(true) {
String newCode = pinyinCode + "_" + i;
if (newCode.length() > 50) {
newCode = newCode.substring(newCode.length() - 50);
}
Integer c = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM platform_dictionary_item WHERE dict_type=? AND dict_code=?",
Integer.class, dictType, newCode
);
if (c == null || c == 0) {
pinyinCode = newCode;
break;
}
i++;
}
}
Integer maxSort = jdbcTemplate.queryForObject(
"SELECT MAX(sort_no) FROM platform_dictionary_item WHERE dict_type=?",
Integer.class,
dictType
);
int nextSort = (maxSort == null ? 0 : maxSort) + 10;
jdbcTemplate.update(
"INSERT INTO platform_dictionary_item (dict_type, dict_code, dict_name, sort_no, status, remark, created_by, updated_by) " +
"VALUES (?, ?, ?, ?, 'ENABLED', 'Auto-imported', ?, ?)",
dictType, pinyinCode, name, nextSort, safeUserId(), safeUserId()
);
return new DictionaryItem(pinyinCode, name);
}
return byName.get(0);
}
private String getPinyin(String src) {
if (src == null || src.trim().isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
format.setVCharType(HanyuPinyinVCharType.WITH_V);
for (char c : src.trim().toCharArray()) {
if (Character.toString(c).matches("[\\u4E00-\\u9FA5]+")) {
try {
String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c, format);
if (pinyins != null && pinyins.length > 0) {
result.append(pinyins[0]);
}
} catch (Exception e) {
result.append(c);
}
} else {
result.append(Character.toUpperCase(c));
}
}
return result.toString();
}
private DictionaryItem resolveOptionalDictionaryItem(String dictType, String dictCode, String dictName, String label) {
boolean hasCode = dictCode != null && !dictCode.trim().isEmpty();
boolean hasName = dictName != null && !dictName.trim().isEmpty();
@@ -172,7 +172,7 @@ public class MeetingController {
}
@PutMapping("/{id}")
@RequirePermission(value = "meeting.create", dataScope = DataScopeType.MEETING, auditAction = "MEETING_UPDATE")
@RequirePermission(value = "meeting.update", dataScope = DataScopeType.MEETING, auditAction = "MEETING_UPDATE")
public ApiResponse<Meeting> update(@PathVariable("id") Long id,
@RequestBody @Valid CreateMeetingRequest request) {
return ApiResponse.success(meetingService.update(id, request));
@@ -2050,6 +2050,8 @@ public class MeetingMaterialService {
requireMeetingInvoiceAttachmentList(map.get("attachments"));
}
}
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException(10001, "资料内容格式错误或缺少必填字段");
}
@@ -3170,7 +3172,7 @@ public class MeetingMaterialService {
}
Collection<?> list = (Collection<?>) value;
if (list.isEmpty()) {
throw new BusinessException(10001, "提交失败,缺少必填字段: invoices");
return;
}
for (Object item : list) {
if (!(item instanceof Map)) {
@@ -230,8 +230,12 @@ public class MeetingService {
double cateringRatio = request.getCateringRatio() == null ? project.getCateringFeeRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
long defaultBudgetCent = calculateDefaultMeetingBudgetCent(project);
if (defaultBudgetCent <= 0L) {
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "默认会议预算必须大于 0");
long initialBudgetCent = request.getBudgetCent() != null ? request.getBudgetCent() : defaultBudgetCent;
if (!project.isAllowMeetingOverBudget() && initialBudgetCent > defaultBudgetCent) {
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "当前项目不允许会议预算超出默认分配额度");
}
if (initialBudgetCent <= 0L) {
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议预算必须大于 0");
}
String now = nowIsoSeconds();
Meeting meeting = new Meeting(
@@ -243,7 +247,7 @@ public class MeetingService {
request.getLocation(),
request.getStartTime(),
request.getEndTime(),
defaultBudgetCent,
initialBudgetCent,
laborRatio,
cateringRatio,
MeetingStatus.NOT_STARTED,
@@ -401,6 +405,15 @@ public class MeetingService {
double laborRatio = request.getLaborRatio() == null ? existing.getLaborRatio() : normalizeRatio(request.getLaborRatio(), "劳务占比");
double cateringRatio = request.getCateringRatio() == null ? existing.getCateringRatio() : normalizeRatio(request.getCateringRatio(), "餐费占比");
assertMeetingRatiosWithinProject(project, laborRatio, cateringRatio);
long newBudgetCent = request.getBudgetCent() != null ? request.getBudgetCent() : existing.getBudgetCent();
if (!project.isAllowMeetingOverBudget() && newBudgetCent > existing.getBudgetCent()) {
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "当前项目不允许调高会议预算");
}
if (newBudgetCent <= 0L) {
throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议预算必须大于 0");
}
Meeting updated = new Meeting(
existing.getId(),
existing.getProjectId(),
@@ -410,7 +423,7 @@ public class MeetingService {
request.getLocation(),
request.getStartTime(),
request.getEndTime(),
request.getBudgetCent(),
newBudgetCent,
laborRatio,
cateringRatio,
existing.getStatus(),
@@ -144,8 +144,8 @@ public class MeetingSummaryExportService {
projectName,
queryTenantName(tenantId)
);
String meetingCategory = stringValue(meeting.get("meeting_category"));
String location = stringValue(meeting.get("location"));
String meetingCategory = resolveDictName("MEETING_CATEGORY", stringValue(meeting.get("meeting_category")));
String location = resolveDictName("MEETING_LOCATION", stringValue(meeting.get("location")));
String startTime = stringValue(meeting.get("start_time"));
String endTime = stringValue(meeting.get("end_time"));
String guestCountText = formatNumber(basicInfo.get("guestCount"));
@@ -240,6 +240,26 @@ public class MeetingSummaryExportService {
}
}
private String resolveDictName(String dictType, String dictCode) {
if (dictCode == null || dictCode.trim().isEmpty()) {
return dictCode;
}
try {
List<String> names = jdbcTemplate.queryForList(
"SELECT dict_name FROM platform_dictionary_item WHERE dict_type=? AND dict_code=? AND is_deleted=0 LIMIT 1",
String.class,
dictType,
dictCode
);
if (!names.isEmpty() && names.get(0) != null) {
return names.get(0);
}
} catch (Exception ex) {
// ignore
}
return dictCode;
}
private List<Long> parseIdList(Object value) {
if (!(value instanceof List)) {
return Collections.emptyList();
@@ -93,9 +93,9 @@ public class UserController {
@PostMapping("/{id}/reset-password")
@RequirePermission(value = "user.password.reset", dataScope = DataScopeType.TENANT, auditAction = "USER_RESET_PASSWORD")
public ApiResponse<String> resetPassword(@PathVariable("id") Long id, @RequestBody @Valid ResetPasswordRequest request) {
systemUserService.resetPassword(id, request);
return ApiResponse.success("OK");
public ApiResponse<String> resetPassword(@PathVariable("id") Long id) {
systemUserService.resetPassword(id);
return ApiResponse.success("密码已重置");
}
@GetMapping("/{id}/role-history")
@@ -22,6 +22,7 @@ import com.writeoff.module.system.model.UserRoleHistory;
import com.writeoff.security.AuthContext;
import com.writeoff.security.PasswordCodecService;
import com.writeoff.security.PasswordPolicyService;
import com.writeoff.security.PasswordSetupService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -55,7 +56,9 @@ public class SystemUserService {
private final PasswordPolicyService passwordPolicyService;
private final PasswordCodecService passwordCodecService;
private final TransactionTemplate transactionTemplate;
private final PasswordSetupService passwordSetupService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final String frontendBaseUrl;
private static final RowMapper<SystemUser> USER_ROW_MAPPER = (rs, n) -> new SystemUser(
rs.getLong("id"),
@@ -97,13 +100,17 @@ public class SystemUserService {
NotificationDispatchService notificationDispatchService,
PasswordPolicyService passwordPolicyService,
PasswordCodecService passwordCodecService,
PlatformTransactionManager transactionManager) {
PlatformTransactionManager transactionManager,
PasswordSetupService passwordSetupService,
@org.springframework.beans.factory.annotation.Value("${app.frontend-base-url:http://localhost:5173}") String frontendBaseUrl) {
this.jdbcTemplate = jdbcTemplate;
this.dataPermissionService = dataPermissionService;
this.notificationDispatchService = notificationDispatchService;
this.passwordPolicyService = passwordPolicyService;
this.passwordCodecService = passwordCodecService;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.passwordSetupService = passwordSetupService;
this.frontendBaseUrl = frontendBaseUrl;
}
public PageResult<SystemUser> listUsers(int pageNo, int pageSize, Boolean includeDeleted) {
@@ -179,10 +186,8 @@ public class SystemUserService {
final String phone = request.getPhone() == null ? "" : request.getPhone().trim();
final String email = request.getEmail() == null ? "" : request.getEmail().trim();
final String rawPassword = request.getPassword() == null ? "" : request.getPassword().trim();
if (rawPassword.isEmpty()) {
throw new BusinessException(10001, "\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a");
}
passwordPolicyService.validate(rawPassword);
final String finalPassword = rawPassword.isEmpty() ? ("Tmp@" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8) + "aA1") : rawPassword;
passwordPolicyService.validate(finalPassword);
final String validFrom = request.getValidFrom() == null || request.getValidFrom().trim().isEmpty()
? LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
: normalizeDateTimeString(request.getValidFrom());
@@ -191,7 +196,7 @@ public class SystemUserService {
: normalizeDateTimeString(request.getValidTo());
return transactionTemplate.execute(status -> {
assertPhoneAvailable(phone, null);
String passwordHash = passwordCodecService.encode(rawPassword);
String passwordHash = passwordCodecService.encode(finalPassword);
String tenantSwitchAccountKey = resolveTenantSwitchAccountKeyByPhone(phone, null);
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
@@ -217,7 +222,8 @@ public class SystemUserService {
}, keyHolder);
Long id = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
autoAssignExecutorRoleWhenCreatorIsProjectExecutor(id);
sendUserCreatedMail(id, userName, phone, email, validFrom, validTo);
String setupLink = passwordSetupService.issueUserSetupLink(tenantId(), id, safeUserId());
sendUserCreatedMail(id, userName, phone, email, finalPassword, validFrom, validTo, setupLink);
return new SystemUser(id, userName, phone, email, "ENABLED", validFrom, validTo, "", "");
});
}
@@ -248,8 +254,8 @@ public class SystemUserService {
ps.setTimestamp(idx++, validTo == null ? Timestamp.valueOf(LocalDateTime.of(2099, 12, 31, 23, 59, 59)) : validTo);
ps.setLong(idx++, operator);
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
passwordPolicyService.validate(request.getPassword());
ps.setString(idx++, passwordCodecService.encode(request.getPassword()));
passwordPolicyService.validate(request.getPassword().trim());
ps.setString(idx++, passwordCodecService.encode(request.getPassword().trim()));
}
ps.setLong(idx++, tenantId());
ps.setLong(idx, userId);
@@ -413,14 +419,54 @@ public class SystemUserService {
);
}
public void resetPassword(Long userId, ResetPasswordRequest request) {
public void resetPassword(Long userId) {
assertUserExists(userId);
passwordPolicyService.validate(request.getNewPassword());
jdbcTemplate.update(
"UPDATE sys_user SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
passwordCodecService.encode(request.getNewPassword()),
SystemUser user = jdbcTemplate.queryForObject(
"SELECT id, user_name, phone, email, status, valid_from, valid_to FROM sys_user WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
(rs, rowNum) -> new SystemUser(
rs.getLong("id"),
rs.getString("user_name"),
rs.getString("phone"),
rs.getString("email"),
rs.getString("status"),
rs.getString("valid_from"),
rs.getString("valid_to")
),
tenantId(),
userId
);
String setupLink = passwordSetupService.issueUserSetupLink(tenantId(), userId, safeUserId());
List<Map<String, Object>> tenants = jdbcTemplate.queryForList(
"SELECT tenant_code, tenant_name FROM tenant WHERE id=? LIMIT 1",
tenantId()
);
String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
Map<String, Object> variables = new LinkedHashMap<String, Object>();
variables.put("userId", userId);
variables.put("targetUserId", userId);
variables.put("userName", user.getUserName());
variables.put("phone", user.getPhone());
variables.put("email", user.getEmail());
variables.put("tenantCode", tenantCode);
variables.put("tenantName", tenantName);
variables.put("setupLink", setupLink);
DispatchNotificationRequest dispatchRequest = new DispatchNotificationRequest();
dispatchRequest.setIdempotencyKey("user-password-reset-" + tenantId() + "-" + userId + "-" + System.currentTimeMillis());
dispatchRequest.setEventCode("USER_PASSWORD_RESET");
dispatchRequest.setBizType("USER");
dispatchRequest.setBizId("user-" + userId);
try {
dispatchRequest.setVariablesJson(objectMapper.writeValueAsString(variables));
} catch (Exception e) {
log.error("Failed to write variables json", e);
}
notificationDispatchService.dispatch(dispatchRequest);
}
public void changeMyPassword(Long userId, String oldPassword, String newPassword) {
@@ -707,8 +753,8 @@ public class SystemUserService {
if (item.getUserName() == null || item.getUserName().trim().isEmpty()) {
throw new BusinessException(10001, "用户名不能为空");
}
if (item.getPassword() == null || item.getPassword().trim().isEmpty()) {
throw new BusinessException(10001, "密码不能为空");
if (item.getPassword() != null && !item.getPassword().trim().isEmpty()) {
passwordPolicyService.validate(item.getPassword().trim());
}
ImportValidationUtils.validatePhone(item.getPhone());
ImportValidationUtils.validateRequiredEmail(item.getEmail());
@@ -717,7 +763,6 @@ public class SystemUserService {
if (!batchPhones.add(phone)) {
throw new BusinessException(10001, "批次内手机号重复");
}
passwordPolicyService.validate(item.getPassword().trim());
String roleCode = ImportValidationUtils.trim(item.getRoleCode());
if (roleCode.isEmpty()) {
return null;
@@ -750,7 +795,7 @@ public class SystemUserService {
: ex.getMessage();
}
private void sendUserCreatedMail(Long userId, String userName, String phone, String email, String validFrom, String validTo) {
private void sendUserCreatedMail(Long userId, String userName, String phone, String email, String rawPassword, String validFrom, String validTo, String setupLink) {
if (userId == null || userId <= 0) {
return;
}
@@ -761,18 +806,26 @@ public class SystemUserService {
);
String tenantCode = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_code"));
String tenantName = tenants.isEmpty() ? "" : String.valueOf(tenants.get(0).get("tenant_name"));
String loginPath = "/" + tenantCode + "/login";
String baseUrl = frontendBaseUrl == null ? "" : frontendBaseUrl;
while (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
String loginPath = baseUrl.isEmpty() ? "/" + tenantCode + "/login" : baseUrl + "/" + tenantCode + "/login";
String displayValidTo = (validTo != null && validTo.contains("2099-12-31")) ? "长期有效" : validTo;
Map<String, Object> variables = new LinkedHashMap<String, Object>();
variables.put("userId", userId);
variables.put("targetUserId", userId);
variables.put("userName", userName);
variables.put("phone", phone);
variables.put("email", email);
variables.put("password", rawPassword);
variables.put("validFrom", validFrom);
variables.put("validTo", validTo);
variables.put("validTo", displayValidTo);
variables.put("tenantCode", tenantCode);
variables.put("tenantName", tenantName);
variables.put("loginPath", loginPath);
variables.put("loginPath", setupLink != null ? setupLink : loginPath);
variables.put("setupLink", setupLink != null ? setupLink : "");
DispatchNotificationRequest request = new DispatchNotificationRequest();
request.setIdempotencyKey("user-created-" + tenantId() + "-" + userId);
request.setEventCode("USER_CREATED");
@@ -270,7 +270,8 @@ public class TenantService {
);
String setupLink = passwordSetupService.issueTenantAdminSetupLink(tenantId, uid, safeUserId());
sendTenantAdminMail(tenantId, request, action, setupLink);
// 移除了发送邮件功能
// sendTenantAdminMail(tenantId, request, action, setupLink);
java.util.Map<String, Object> data = new java.util.LinkedHashMap<String, Object>();
data.put("tenantId", tenantId);
@@ -278,6 +279,7 @@ public class TenantService {
data.put("roleId", roleId);
data.put("roleCode", roleCode);
data.put("action", action);
data.put("setupLink", setupLink);
return data;
}
@@ -39,11 +39,23 @@ public class TemplateController {
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "scopeType", required = false) String scopeType,
@RequestParam(value = "bizScene", required = false) String bizScene,
@RequestParam(value = "watermarkEnabled", required = false) Boolean watermarkEnabled,
@RequestParam(value = "effectiveStatus", required = false) String effectiveStatus,
@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
return ApiResponse.success(templateService.list(templateName, templateType, status, scopeType, bizScene, watermarkEnabled, effectiveStatus, pageNo, pageSize));
return ApiResponse.success(templateService.list(templateName, templateType, status, scopeType, bizScene, effectiveStatus, pageNo, pageSize));
}
@GetMapping("/view-list")
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VIEW_LIST")
public ApiResponse<PageResult<TemplateInfo>> viewList(
@RequestParam(value = "templateName", required = false) String templateName,
@RequestParam(value = "templateType", required = false) String templateType,
@RequestParam(value = "scopeType", required = false) String scopeType,
@RequestParam(value = "bizScene", required = false) String bizScene,
@RequestParam(value = "effectiveStatus", required = false) String effectiveStatus,
@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") int pageSize) {
return ApiResponse.success(templateService.listView(templateName, templateType, scopeType, bizScene, effectiveStatus, pageNo, pageSize));
}
@GetMapping("/published-options")
@@ -98,6 +110,12 @@ public class TemplateController {
return ApiResponse.success(templateService.create(request));
}
@PostMapping("/{id}/update")
@RequirePermission(value = "template.update", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_UPDATE")
public ApiResponse<TemplateInfo> update(@PathVariable("id") Long id, @RequestBody @Valid com.writeoff.module.template.dto.UpdateTemplateRequest request) {
return ApiResponse.success(templateService.update(id, request));
}
@PostMapping("/upload-sign")
@RequirePermission(value = "template.create", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_UPLOAD_SIGN")
public ApiResponse<Map<String, Object>> uploadSign(@RequestBody @Valid TemplateUploadSignRequest request) {
@@ -105,7 +123,7 @@ public class TemplateController {
}
@GetMapping("/{id}/versions")
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSIONS")
@RequirePermission(value = "template.detail.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSIONS")
public ApiResponse<List<TemplateVersionInfo>> versions(@PathVariable("id") Long id) {
return ApiResponse.success(templateService.versions(id));
}
@@ -148,16 +166,10 @@ public class TemplateController {
return ApiResponse.success(templateService.download(id, request.getRemoteAddr(), request.getHeader("User-Agent")));
}
@GetMapping("/{id}/download-watermark")
@RequirePermission(value = "template.download", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_DOWNLOAD_WATERMARK")
public ApiResponse<Map<String, Object>> downloadWatermark(@PathVariable("id") Long id,
@RequestParam(value = "watermarkText", required = false) String watermarkText,
HttpServletRequest request) {
return ApiResponse.success(templateService.downloadWatermark(id, watermarkText, request.getRemoteAddr(), request.getHeader("User-Agent")));
}
@GetMapping("/{id}/versions/diff")
@RequirePermission(value = "template.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSION_DIFF")
@RequirePermission(value = "template.detail.read", dataScope = DataScopeType.TENANT, auditAction = "TEMPLATE_VERSION_DIFF")
public ApiResponse<Map<String, Object>> versionDiff(@PathVariable("id") Long id,
@RequestParam(value = "leftVersionNo", required = false) Integer leftVersionNo,
@RequestParam(value = "rightVersionNo", required = false) Integer rightVersionNo) {
@@ -172,7 +184,6 @@ public class TemplateController {
@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "userKeyword", required = false) String userKeyword,
@RequestParam(value = "versionNo", required = false) Integer versionNo,
@RequestParam(value = "downloadType", required = false) String downloadType,
@RequestParam(value = "ip", required = false) String ip,
@RequestParam(value = "downloadedFrom", required = false) String downloadedFrom,
@RequestParam(value = "downloadedTo", required = false) String downloadedTo,
@@ -184,7 +195,6 @@ public class TemplateController {
userId,
userKeyword,
versionNo,
downloadType,
ip,
downloadedFrom,
downloadedTo,
@@ -18,7 +18,6 @@ public class CreateTemplateRequest {
private String changeLog;
private String effectiveFrom;
private String effectiveTo;
private Boolean watermarkEnabled;
private Integer downloadRateLimitPerHour;
public String getTemplateName() {
@@ -109,13 +108,6 @@ public class CreateTemplateRequest {
this.effectiveTo = effectiveTo;
}
public Boolean getWatermarkEnabled() {
return watermarkEnabled;
}
public void setWatermarkEnabled(Boolean watermarkEnabled) {
this.watermarkEnabled = watermarkEnabled;
}
public Integer getDownloadRateLimitPerHour() {
return downloadRateLimitPerHour;
@@ -0,0 +1,100 @@
package com.writeoff.module.template.dto;
import javax.validation.constraints.NotBlank;
public class UpdateTemplateRequest {
@NotBlank(message = "模板名称不能为空")
private String templateName;
@NotBlank(message = "模板类型不能为空")
private String templateType;
@NotBlank(message = "适用范围不能为空")
private String scopeType;
private Long projectId;
private Long meetingId;
private Long scopeId;
private String bizScene;
private String effectiveFrom;
private String effectiveTo;
private Integer downloadRateLimitPerHour;
public String getTemplateName() {
return templateName;
}
public void setTemplateName(String templateName) {
this.templateName = templateName;
}
public String getTemplateType() {
return templateType;
}
public void setTemplateType(String templateType) {
this.templateType = templateType;
}
public String getScopeType() {
return scopeType;
}
public void setScopeType(String scopeType) {
this.scopeType = scopeType;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public Long getMeetingId() {
return meetingId;
}
public void setMeetingId(Long meetingId) {
this.meetingId = meetingId;
}
public Long getScopeId() {
return scopeId;
}
public void setScopeId(Long scopeId) {
this.scopeId = scopeId;
}
public String getBizScene() {
return bizScene;
}
public void setBizScene(String bizScene) {
this.bizScene = bizScene;
}
public String getEffectiveFrom() {
return effectiveFrom;
}
public void setEffectiveFrom(String effectiveFrom) {
this.effectiveFrom = effectiveFrom;
}
public String getEffectiveTo() {
return effectiveTo;
}
public void setEffectiveTo(String effectiveTo) {
this.effectiveTo = effectiveTo;
}
public Integer getDownloadRateLimitPerHour() {
return downloadRateLimitPerHour;
}
public void setDownloadRateLimitPerHour(Integer downloadRateLimitPerHour) {
this.downloadRateLimitPerHour = downloadRateLimitPerHour;
}
}
@@ -10,7 +10,6 @@ public class TemplateDownloadLogInfo {
private String userPhone;
private String objectKey;
private String downloadType;
private String watermarkText;
private Long projectId;
private Long meetingId;
private String ip;
@@ -18,7 +17,7 @@ public class TemplateDownloadLogInfo {
private String downloadedAt;
public TemplateDownloadLogInfo(Long id, Long templateId, String templateName, Integer versionNo, Long userId, String userName, String userPhone,
String objectKey, String downloadType, String watermarkText, Long projectId, Long meetingId,
String objectKey, String downloadType, Long projectId, Long meetingId,
String ip, String userAgent, String downloadedAt) {
this.id = id;
this.templateId = templateId;
@@ -29,7 +28,6 @@ public class TemplateDownloadLogInfo {
this.userPhone = userPhone;
this.objectKey = objectKey;
this.downloadType = downloadType;
this.watermarkText = watermarkText;
this.projectId = projectId;
this.meetingId = meetingId;
this.ip = ip;
@@ -73,9 +71,7 @@ public class TemplateDownloadLogInfo {
return downloadType;
}
public String getWatermarkText() {
return watermarkText;
}
public Long getProjectId() {
return projectId;
@@ -14,13 +14,12 @@ public class TemplateInfo {
private String currentObjectKey;
private String effectiveFrom;
private String effectiveTo;
private Boolean watermarkEnabled;
private Integer downloadRateLimitPerHour;
private String createdAt;
private String updatedAt;
public TemplateInfo(Long id, String templateName, String templateType, String scopeType, Long projectId, Long meetingId, Long scopeId, String bizScene,
String status, Integer currentVersionNo, String currentObjectKey, String effectiveFrom, String effectiveTo, Boolean watermarkEnabled, Integer downloadRateLimitPerHour,
String status, Integer currentVersionNo, String currentObjectKey, String effectiveFrom, String effectiveTo, Integer downloadRateLimitPerHour,
String createdAt, String updatedAt) {
this.id = id;
this.templateName = templateName;
@@ -35,7 +34,6 @@ public class TemplateInfo {
this.currentObjectKey = currentObjectKey;
this.effectiveFrom = effectiveFrom;
this.effectiveTo = effectiveTo;
this.watermarkEnabled = watermarkEnabled;
this.downloadRateLimitPerHour = downloadRateLimitPerHour;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
@@ -93,9 +91,6 @@ public class TemplateInfo {
return effectiveTo;
}
public Boolean getWatermarkEnabled() {
return watermarkEnabled;
}
public Integer getDownloadRateLimitPerHour() {
return downloadRateLimitPerHour;
@@ -59,7 +59,6 @@ public class TemplateService {
rs.getString("current_object_key"),
rs.getString("effective_from"),
rs.getString("effective_to"),
rs.getInt("watermark_enabled") == 1,
rs.getInt("download_rate_limit_per_hour"),
rs.getString("created_at"),
rs.getString("updated_at")
@@ -86,7 +85,6 @@ public class TemplateService {
rs.getString("user_phone"),
rs.getString("object_key"),
rs.getString("download_type"),
rs.getString("watermark_text"),
rs.getObject("project_id") == null ? null : rs.getLong("project_id"),
rs.getObject("meeting_id") == null ? null : rs.getLong("meeting_id"),
rs.getString("ip"),
@@ -114,7 +112,6 @@ public class TemplateService {
String status,
String scopeType,
String bizScene,
Boolean watermarkEnabled,
String effectiveStatus,
int pageNo,
int pageSize) {
@@ -150,10 +147,7 @@ public class TemplateService {
whereSql.append(" AND t.biz_scene=?");
whereArgs.add(normalizedBizScene);
}
if (watermarkEnabled != null) {
whereSql.append(" AND t.watermark_enabled=?");
whereArgs.add(Boolean.TRUE.equals(watermarkEnabled) ? 1 : 0);
}
appendEffectiveStatusFilter(whereSql, normalizedEffectiveStatus);
Integer total = jdbcTemplate.queryForObject(
@@ -176,6 +170,16 @@ public class TemplateService {
return new PageResult<>(list, totalCount, safePage, safeSize);
}
public PageResult<TemplateInfo> listView(String templateName,
String templateType,
String scopeType,
String bizScene,
String effectiveStatus,
int pageNo,
int pageSize) {
return list(templateName, templateType, "PUBLISHED", scopeType, bizScene, effectiveStatus, pageNo, pageSize);
}
public List<TemplateInfo> listPublishedOptions(String bizScene) {
String normalizedBizScene = normalizeOptionalBizScene(bizScene);
StringBuilder sql = new StringBuilder(templateSelectSql())
@@ -248,8 +252,8 @@ public class TemplateService {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO template (tenant_id, template_name, template_type, scope_type, project_id, meeting_id, scope_id, biz_scene, status, current_version_no, effective_from, effective_to, watermark_enabled, download_rate_limit_per_hour, created_by, updated_by) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'DRAFT', 1, STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), ?, ?, ?, ?)",
"INSERT INTO template (tenant_id, template_name, template_type, scope_type, project_id, meeting_id, scope_id, biz_scene, status, current_version_no, effective_from, effective_to, download_rate_limit_per_hour, created_by, updated_by) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'DRAFT', 1, STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
ps.setLong(1, tenantId());
@@ -262,10 +266,9 @@ public class TemplateService {
ps.setString(8, normalizeBizScene(request.getBizScene()));
ps.setString(9, effectiveFrom);
ps.setString(10, effectiveTo);
ps.setInt(11, Boolean.TRUE.equals(request.getWatermarkEnabled()) ? 1 : 0);
ps.setInt(12, downloadRateLimitPerHour);
ps.setInt(11, downloadRateLimitPerHour);
ps.setLong(12, userId);
ps.setLong(13, userId);
ps.setLong(14, userId);
return ps;
}, keyHolder);
Long templateId = keyHolder.getKey() == null ? null : keyHolder.getKey().longValue();
@@ -282,6 +285,36 @@ public class TemplateService {
return findById(validTemplateId);
}
@Transactional(rollbackFor = Exception.class)
public TemplateInfo update(Long templateId, com.writeoff.module.template.dto.UpdateTemplateRequest request) {
TemplateInfo template = findById(templateId);
assertTemplateEditable(template);
String scopeType = normalizeScope(request.getScopeType());
String templateType = normalizeTemplateType(request.getTemplateType());
String effectiveFrom = normalizeOptionalDateTime(request.getEffectiveFrom(), "effectiveFrom");
String effectiveTo = normalizeOptionalDateTime(request.getEffectiveTo(), "effectiveTo");
Integer downloadRateLimitPerHour = normalizeDownloadRateLimit(request.getDownloadRateLimitPerHour());
assertEffectiveRangeValid(effectiveFrom, effectiveTo);
assertTypeOptionEnabled(templateType);
jdbcTemplate.update(
"UPDATE template SET template_name=?, template_type=?, scope_type=?, project_id=?, meeting_id=?, scope_id=?, biz_scene=?, effective_from=STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), effective_to=STR_TO_DATE(?, '%Y-%m-%d %H:%i:%s'), download_rate_limit_per_hour=?, updated_by=?, updated_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
request.getTemplateName(),
templateType,
scopeType,
request.getProjectId(),
request.getMeetingId(),
request.getScopeId(),
normalizeBizScene(request.getBizScene()),
effectiveFrom,
effectiveTo,
downloadRateLimitPerHour,
safeUserId(),
tenantId(),
templateId
);
return findById(templateId);
}
public List<TemplateVersionInfo> versions(Long templateId) {
assertTemplateExists(templateId);
return jdbcTemplate.query(
@@ -451,15 +484,14 @@ public class TemplateService {
assertTemplateEffectiveNow(template, "下载");
assertDownloadRateLimit(template, userId);
jdbcTemplate.update(
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, project_id, meeting_id, ip, user_agent) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
tenantId(),
template.getId(),
template.getCurrentVersionNo(),
userId,
template.getCurrentObjectKey(),
"NORMAL",
null,
template.getProjectId(),
template.getMeetingId(),
ip,
@@ -473,46 +505,6 @@ public class TemplateService {
return result;
}
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> downloadWatermark(Long templateId, String watermarkText, String ip, String userAgent) {
TemplateInfo template = findById(templateId);
if ("DISABLED".equalsIgnoreCase(template.getStatus()) || "ARCHIVED".equalsIgnoreCase(template.getStatus())) {
throw new BusinessException(10003, "模板已停用,无法下载");
}
if (template.getCurrentObjectKey() == null || template.getCurrentObjectKey().trim().isEmpty()) {
throw new BusinessException(10003, "模板当前版本文件不存在");
}
String signedUrl = ossService.generateDownloadUrl(template.getCurrentObjectKey());
Long userId = safeUserId();
assertTemplateDownloadAllowed(template);
if (!template.getWatermarkEnabled()) {
throw new BusinessException(10003, "模板未启用水印下载");
}
assertTemplateEffectiveNow(template, "水印下载");
assertDownloadRateLimit(template, userId);
jdbcTemplate.update(
"INSERT INTO template_download_log (tenant_id, template_id, version_no, user_id, object_key, download_type, watermark_text, project_id, meeting_id, ip, user_agent) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
tenantId(),
template.getId(),
template.getCurrentVersionNo(),
userId,
template.getCurrentObjectKey(),
"WATERMARK",
watermarkText == null ? null : watermarkText.trim(),
template.getProjectId(),
template.getMeetingId(),
ip,
userAgent
);
Map<String, Object> result = new LinkedHashMap<String, Object>();
result.put("templateId", template.getId());
result.put("versionNo", template.getCurrentVersionNo());
result.put("objectKey", template.getCurrentObjectKey());
result.put("signedUrl", signedUrl);
result.put("watermarkText", watermarkText == null ? "" : watermarkText.trim());
return result;
}
public Map<String, Object> versionDiff(Long templateId, Integer leftVersionNo, Integer rightVersionNo) {
assertTemplateExists(templateId);
@@ -548,7 +540,6 @@ public class TemplateService {
Long userId,
String userKeyword,
Integer versionNo,
String downloadType,
String ip,
String downloadedFrom,
String downloadedTo,
@@ -559,7 +550,6 @@ public class TemplateService {
int offset = (safePage - 1) * safeSize;
String normalizedTemplateName = trimToNull(templateName);
String normalizedUserKeyword = trimToNull(userKeyword);
String normalizedDownloadType = normalizeOptionalDownloadType(downloadType);
String normalizedIp = trimToNull(ip);
String normalizedDownloadedFrom = trimToNull(downloadedFrom);
String normalizedDownloadedTo = trimToNull(downloadedTo);
@@ -598,10 +588,7 @@ public class TemplateService {
whereSql.append(" AND l.version_no=?");
whereArgs.add(versionNo);
}
if (normalizedDownloadType != null) {
whereSql.append(" AND l.download_type=?");
whereArgs.add(normalizedDownloadType);
}
if (normalizedIp != null) {
whereSql.append(" AND l.ip LIKE ?");
whereArgs.add("%" + normalizedIp + "%");
@@ -628,7 +615,7 @@ public class TemplateService {
List<TemplateDownloadLogInfo> list = jdbcTemplate.query(
"SELECT l.id, l.template_id, COALESCE(t.template_name, '') AS template_name, " +
"l.version_no, l.user_id, COALESCE(u.user_name, '') AS user_name, COALESCE(u.phone, '') AS user_phone, " +
"l.object_key, l.download_type, COALESCE(l.watermark_text, '') AS watermark_text, " +
"l.object_key, l.download_type, " +
"l.project_id, l.meeting_id, l.ip, l.user_agent, DATE_FORMAT(l.downloaded_at, '%Y-%m-%d %H:%i:%s') AS downloaded_at" +
whereSql +
" ORDER BY l.downloaded_at DESC, l.id DESC LIMIT ? OFFSET ?",
@@ -709,7 +696,7 @@ public class TemplateService {
"t.current_version_no, tv.object_key AS current_object_key, " +
"DATE_FORMAT(t.effective_from, '%Y-%m-%d %H:%i:%s') AS effective_from, " +
"DATE_FORMAT(t.effective_to, '%Y-%m-%d %H:%i:%s') AS effective_to, " +
"t.watermark_enabled, t.download_rate_limit_per_hour, " +
"t.download_rate_limit_per_hour, " +
"DATE_FORMAT(t.created_at, '%Y-%m-%d %H:%i:%s') AS created_at, " +
"DATE_FORMAT(t.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
"FROM template t " +
@@ -806,17 +793,7 @@ public class TemplateService {
return normalized;
}
private String normalizeOptionalDownloadType(String downloadType) {
String value = trimToNull(downloadType);
if (value == null) {
return null;
}
String normalized = value.toUpperCase();
if (!"NORMAL".equals(normalized) && !"WATERMARK".equals(normalized)) {
throw new BusinessException(10003, "downloadType仅支持NORMAL/WATERMARK");
}
return normalized;
}
private String normalizeContentType(String contentType) {
if (contentType == null || contentType.trim().isEmpty()) {
@@ -936,7 +913,7 @@ public class TemplateService {
private void assertTemplateEditable(TemplateInfo template) {
if ("ARCHIVED".equalsIgnoreCase(template.getStatus())) {
throw new BusinessException(10003, "已归档模板不允许新增版本");
throw new BusinessException(10003, "已归档模板不允许编辑或新增版本");
}
}
@@ -20,6 +20,7 @@ import java.util.Map;
@Service
public class PasswordSetupService {
private static final String SCENARIO_TENANT_ADMIN_SETUP = "TENANT_ADMIN_SETUP";
private static final String SCENARIO_USER_SETUP = "USER_SETUP";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final JdbcTemplate jdbcTemplate;
@@ -71,6 +72,36 @@ public class PasswordSetupService {
return buildSetupLink(tenantCode, rawToken);
}
@Transactional
public String issueUserSetupLink(Long tenantId, Long userId, Long operatorUserId) {
Map<String, Object> user = loadSystemUser(tenantId, userId);
String tenantCode = String.valueOf(user.get("tenant_code"));
jdbcTemplate.update(
"UPDATE auth_password_setup_token " +
"SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
"WHERE tenant_id=? AND user_id=? AND scenario=? AND is_deleted=0 AND used_at IS NULL",
safeOperator(operatorUserId),
tenantId,
userId,
SCENARIO_USER_SETUP
);
String rawToken = generateToken();
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(Math.max(passwordSetupExpireMinutes, 10L));
jdbcTemplate.update(
"INSERT INTO auth_password_setup_token (tenant_id, user_id, scenario, token_hash, expires_at, created_by, updated_by) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
tenantId,
userId,
SCENARIO_USER_SETUP,
hashToken(rawToken),
Timestamp.valueOf(expiresAt),
safeOperator(operatorUserId),
safeOperator(operatorUserId)
);
return buildSetupLink(tenantCode, rawToken);
}
public Map<String, Object> verifyTenantPasswordSetupToken(String tenantCode, String rawToken) {
Map<String, Object> tokenRecord = loadAvailableTokenRecord(tenantCode, rawToken);
Map<String, Object> data = new LinkedHashMap<String, Object>();
@@ -112,11 +143,12 @@ public class PasswordSetupService {
jdbcTemplate.update(
"UPDATE auth_password_setup_token " +
"SET is_deleted=1, updated_by=?, updated_at=CURRENT_TIMESTAMP " +
"WHERE tenant_id=? AND user_id=? AND scenario=? AND id<>? AND is_deleted=0 AND used_at IS NULL",
"WHERE tenant_id=? AND user_id=? AND scenario IN (?, ?) AND id<>? AND is_deleted=0 AND used_at IS NULL",
userId,
tenantId,
userId,
SCENARIO_TENANT_ADMIN_SETUP,
SCENARIO_USER_SETUP,
tokenId
);
@@ -139,11 +171,12 @@ public class PasswordSetupService {
"FROM auth_password_setup_token tkn " +
"JOIN tenant t ON tkn.tenant_id=t.id " +
"JOIN sys_user u ON tkn.user_id=u.id AND tkn.tenant_id=u.tenant_id " +
"WHERE tkn.token_hash=? AND tkn.scenario=? AND tkn.is_deleted=0 " +
"WHERE tkn.token_hash=? AND tkn.scenario IN (?, ?) AND tkn.is_deleted=0 " +
"AND t.is_deleted=0 AND u.is_deleted=0 " +
"LIMIT 1",
hashToken(normalizedToken),
SCENARIO_TENANT_ADMIN_SETUP
SCENARIO_TENANT_ADMIN_SETUP,
SCENARIO_USER_SETUP
);
if (rows.isEmpty()) {
throw new BusinessException(10001, "设置链接无效或已过期");
@@ -181,6 +214,22 @@ public class PasswordSetupService {
return rows.get(0);
}
private Map<String, Object> loadSystemUser(Long tenantId, Long userId) {
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT u.id, u.tenant_id, t.tenant_code " +
"FROM sys_user u " +
"JOIN tenant t ON u.tenant_id=t.id " +
"WHERE u.tenant_id=? AND u.id=? AND u.is_deleted=0 AND t.is_deleted=0 " +
"LIMIT 1",
tenantId,
userId
);
if (rows.isEmpty()) {
throw new BusinessException(10003, "用户不存在");
}
return rows.get(0);
}
private String buildSetupLink(String tenantCode, String rawToken) {
String baseUrl = normalizeBaseUrl(frontendBaseUrl);
String path = "/" + tenantCode + "/setup-password?token=" + urlEncode(rawToken);
@@ -0,0 +1,27 @@
-- 添加模板编辑权限
INSERT INTO permission (id, permission_code, permission_name, module)
SELECT t.next_id, 'template.update', '编辑模板', 'template'
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
WHERE NOT EXISTS (
SELECT 1 FROM permission WHERE permission_code = 'template.update'
);
-- 授予角色编辑模板权限(通常跟随 template.create 一起授权)
-- 找出所有拥有 template.create 权限的角色,给他们也加上 template.update 权限
SET @next_rp_id = (SELECT IFNULL(MAX(id), 0) FROM role_permission);
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
SELECT
(@next_rp_id := @next_rp_id + 1) AS id,
rp.tenant_id,
rp.role_id,
p.id
FROM role_permission rp
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.create'
JOIN permission p ON p.permission_code = 'template.update'
WHERE NOT EXISTS (
SELECT 1 FROM role_permission existing_rp
WHERE existing_rp.tenant_id = rp.tenant_id
AND existing_rp.role_id = rp.role_id
AND existing_rp.permission_id = p.id
);
@@ -0,0 +1,51 @@
-- 添加查看详情和水印下载权限
INSERT INTO permission (id, permission_code, permission_name, module)
SELECT t.next_id, 'template.detail.read', '查看模板详情', 'template'
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
WHERE NOT EXISTS (
SELECT 1 FROM permission WHERE permission_code = 'template.detail.read'
);
INSERT INTO permission (id, permission_code, permission_name, module)
SELECT t.next_id, 'template.watermark.download', '水印下载模板', 'template'
FROM (SELECT IFNULL(MAX(id), 0) + 1 AS next_id FROM permission) t
WHERE NOT EXISTS (
SELECT 1 FROM permission WHERE permission_code = 'template.watermark.download'
);
-- 授予角色新权限(继承现有权限)
SET @next_rp_id = (SELECT IFNULL(MAX(id), 0) FROM role_permission);
-- 给有 template.read 的加上 template.detail.read
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
SELECT
(@next_rp_id := @next_rp_id + 1) AS id,
rp.tenant_id,
rp.role_id,
p.id
FROM role_permission rp
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.read'
JOIN permission p ON p.permission_code = 'template.detail.read'
WHERE NOT EXISTS (
SELECT 1 FROM role_permission existing_rp
WHERE existing_rp.tenant_id = rp.tenant_id
AND existing_rp.role_id = rp.role_id
AND existing_rp.permission_id = p.id
);
-- 给有 template.download 的加上 template.watermark.download
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
SELECT
(@next_rp_id := @next_rp_id + 1) AS id,
rp.tenant_id,
rp.role_id,
p.id
FROM role_permission rp
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'template.download'
JOIN permission p ON p.permission_code = 'template.watermark.download'
WHERE NOT EXISTS (
SELECT 1 FROM role_permission existing_rp
WHERE existing_rp.tenant_id = rp.tenant_id
AND existing_rp.role_id = rp.role_id
AND existing_rp.permission_id = p.id
);
@@ -0,0 +1,7 @@
-- 移除水印相关字段
ALTER TABLE template DROP COLUMN watermark_enabled;
ALTER TABLE template_download_log DROP COLUMN watermark_text;
-- 删除之前新增的 template.watermark.download 权限
DELETE FROM role_permission WHERE permission_id IN (SELECT id FROM permission WHERE permission_code = 'template.watermark.download');
DELETE FROM permission WHERE permission_code = 'template.watermark.download';
@@ -0,0 +1,27 @@
SET @next_permission_id := (SELECT IFNULL(MAX(id), 0) + 1 FROM permission);
INSERT INTO permission (id, permission_code, permission_name, module)
SELECT @next_permission_id, 'meeting.update', '编辑会议', 'meeting'
FROM dual
WHERE NOT EXISTS (
SELECT 1
FROM permission
WHERE permission_code = 'meeting.update'
);
SET @next_role_permission_id := (SELECT IFNULL(MAX(id), 0) FROM role_permission);
INSERT INTO role_permission (id, tenant_id, role_id, permission_id)
SELECT
(@next_role_permission_id := @next_role_permission_id + 1) AS id,
rp.tenant_id,
rp.role_id,
p.id AS permission_id
FROM role_permission rp
JOIN permission source_p ON source_p.id = rp.permission_id AND source_p.permission_code = 'meeting.create'
JOIN permission p ON p.permission_code = 'meeting.update'
WHERE NOT EXISTS (
SELECT 1
FROM role_permission exist_rp
WHERE exist_rp.tenant_id = rp.tenant_id
AND exist_rp.role_id = rp.role_id
AND exist_rp.permission_id = p.id
);