批量推送代码
This commit is contained in:
+11
@@ -48,6 +48,17 @@ public class BizExpertController extends BaseController
|
||||
{
|
||||
return success(bizExpertService.getByUserId(SecurityUtils.getUserId()));
|
||||
}
|
||||
/**
|
||||
* 按手机号查专家 (参会人 dialog 手机号放大镜回填用): 命中返回专家档案, 未命中返回 null
|
||||
*/
|
||||
@GetMapping("/byPhone/{phone}")
|
||||
public AjaxResult getByPhone(@PathVariable String phone)
|
||||
{
|
||||
BizExpert q = new BizExpert();
|
||||
q.setPhone(phone);
|
||||
List<BizExpert> list = bizExpertService.selectList(q);
|
||||
return success(list != null && !list.isEmpty() ? list.get(0) : null);
|
||||
}
|
||||
/**
|
||||
* admin 创建专家: 同时创建 sys_user (用户名=手机号, 密码=手机号)
|
||||
* 返回 SysUser (含明文 password 给前端 toast 用)
|
||||
|
||||
+20
-4
@@ -259,6 +259,9 @@ public class BizProjectController extends BaseController
|
||||
@PostMapping("/{projectId}/assigns")
|
||||
public AjaxResult saveAssigns(@PathVariable("projectId") Long projectId, @RequestBody List<BizProjectAssign> assigns)
|
||||
{
|
||||
// 已结题项目不能再分配
|
||||
BizProject project = requireAssignableProject(projectId);
|
||||
|
||||
if (assigns == null) assigns = new ArrayList<>();
|
||||
bizProjectAssignService.validateSum(projectId, assigns);
|
||||
// #3 通知去重: 拉旧数据按 executionUnitId 索引, 同 (executionUnitId, sessions, amount) → 无变化 → 跳过
|
||||
@@ -270,8 +273,7 @@ public class BizProjectController extends BaseController
|
||||
}
|
||||
bizProjectAssignService.deleteByProjectId(projectId);
|
||||
// #3 通知: 查一次项目名, 避免循环里重复查 DB
|
||||
BizProject project = bizProjectService.getById(projectId);
|
||||
String projectName = project != null ? project.getProjectName() : null;
|
||||
String projectName = project.getProjectName();
|
||||
for (BizProjectAssign a : assigns) {
|
||||
a.setProjectId(projectId);
|
||||
if (a.getStatus() == null) a.setStatus("0");
|
||||
@@ -331,6 +333,15 @@ public class BizProjectController extends BaseController
|
||||
try { return Long.parseLong(s); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
|
||||
/** 校验项目存在且未结题 — 已结题项目禁止一切分配操作, 返回项目供取项目名. */
|
||||
private BizProject requireAssignableProject(Long projectId) {
|
||||
if (projectId == null) throw new ServiceException("项目不存在");
|
||||
BizProject p = bizProjectService.getById(projectId);
|
||||
if (p == null) throw new ServiceException("项目不存在");
|
||||
if ("1".equals(p.getIsFinished())) throw new ServiceException("已结题项目不能分配");
|
||||
return p;
|
||||
}
|
||||
|
||||
@Log(title = "项目执行方分配", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{projectId}/assigns")
|
||||
public AjaxResult clearAssigns(@PathVariable("projectId") Long projectId)
|
||||
@@ -406,6 +417,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null) {
|
||||
return error("projectId 必填");
|
||||
}
|
||||
// 已结题项目不能分配
|
||||
BizProject project = requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
java.util.List<Long> mids = body.getMonitorUserIds();
|
||||
if (mids == null || mids.isEmpty()) {
|
||||
// 向后兼容: 单值 monitorUserId
|
||||
@@ -430,8 +443,7 @@ public class BizProjectController extends BaseController
|
||||
|
||||
// 通知被分配的监察员 (新增 / 说明或积分变化才发). projectId 在 sponsor_assign 是 String, 转 Long 查主表
|
||||
Long projectIdLong = parseProjectId(body.getProjectId());
|
||||
BizProject project = projectIdLong != null ? bizProjectService.getById(projectIdLong) : null;
|
||||
String projectName = project != null ? project.getProjectName() : null;
|
||||
String projectName = project.getProjectName();
|
||||
for (Long mid : mids) {
|
||||
if (mid == null) continue;
|
||||
BizProjectSponsorAssign old = oldByMonitor.get(mid);
|
||||
@@ -467,6 +479,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null) {
|
||||
return error("projectId 必填");
|
||||
}
|
||||
// 已结题项目不能分配
|
||||
requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
java.util.List<Long> sids = body.getStaffUserIds();
|
||||
if (sids == null || sids.isEmpty()) {
|
||||
// 向后兼容: 单值 staffUserId
|
||||
@@ -532,6 +546,8 @@ public class BizProjectController extends BaseController
|
||||
if (body.getProjectId() == null || body.getMonitorUserId() == null) {
|
||||
throw new IllegalArgumentException("projectId / monitorUserId 必填");
|
||||
}
|
||||
// 已结题项目不能分配 (抛 ServiceException → 本循环 catch 计入 errors)
|
||||
requireAssignableProject(parseProjectId(body.getProjectId()));
|
||||
body.setCreateBy(loginName);
|
||||
body.setSponsorOrgId(bizOrgService.selectOrgIdByUserId(loginUid));
|
||||
// 通知去重: 拉旧分配, 找同 monitorUserId, 比较 assignDesc/assignPoints 是否变化
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.business.scheduler;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.business.supplier.SupplierAccountApiCodec;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 供应商账号数据拉取调度器: 每分钟拉取最近 5 分钟更新的账号, 解密后打印.
|
||||
* <p>
|
||||
* 数据源: {@code GET /supplier-api/bidding/supplier/openapi/accounts}
|
||||
* 入参 lastUpdatedTime(最后更新时间) / pageNum / pageSize, 按更新时间倒序返回.
|
||||
* 返回 data 字段为 AES-256-GCM 加密串, 用 {@link SupplierAccountApiCodec} 解密.
|
||||
* <p>
|
||||
* 说明: 只打印不落库 (后续需要持久化时再扩展).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SupplierAccountPullScheduler
|
||||
{
|
||||
private static final String KEY = "supplier-account-api-aes.key";
|
||||
private static final String BASE_URL = "supplier-account-api.base-url";
|
||||
private static final String PAGE_SIZE = "supplier-account-api.page-size";
|
||||
private static final String PULL_MINUTES = "supplier-account-api.pull-minutes";
|
||||
|
||||
private static final String DEFAULT_BASE_URL =
|
||||
"https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts";
|
||||
|
||||
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||
public void pullAccounts()
|
||||
{
|
||||
try
|
||||
{
|
||||
String key = env.getProperty(KEY);
|
||||
if (key == null || key.isEmpty())
|
||||
{
|
||||
log.warn("[SupplierAccountPull] 未配置 {} , 跳过", KEY);
|
||||
return;
|
||||
}
|
||||
String baseUrl = env.getProperty(BASE_URL, DEFAULT_BASE_URL);
|
||||
int pageSize = env.getProperty(PAGE_SIZE, Integer.class, 20);
|
||||
int pullMinutes = env.getProperty(PULL_MINUTES, Integer.class, 5*24*60*60);
|
||||
String lastUpdatedTime = LocalDateTime.now().minusMinutes(pullMinutes).format(TIME_FMT);
|
||||
|
||||
int pageNum = 1;
|
||||
int fetched = 0;
|
||||
while (true)
|
||||
{
|
||||
String param = "lastUpdatedTime=" + URLEncoder.encode(lastUpdatedTime, "UTF-8")
|
||||
+ "&pageNum=" + pageNum + "&pageSize=" + pageSize;
|
||||
String resp = HttpUtils.sendGet(baseUrl, param);
|
||||
if (resp == null || resp.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
JsonNode root = objectMapper.readTree(resp);
|
||||
String encrypted = root.path("data").asText("");
|
||||
if (encrypted.isEmpty())
|
||||
{
|
||||
log.info("[SupplierAccountPull] page={} 无 data 字段 (code={}, msg={}), 结束",
|
||||
pageNum, root.path("code").asText(), root.path("msg").asText());
|
||||
break;
|
||||
}
|
||||
|
||||
String plain = SupplierAccountApiCodec.decrypt(encrypted, key);
|
||||
JsonNode inner = objectMapper.readTree(plain);
|
||||
int total = inner.path("total").asInt(0);
|
||||
JsonNode rows = inner.path("rows");
|
||||
int size = rows.isArray() ? rows.size() : 0;
|
||||
fetched += size;
|
||||
|
||||
// 只打印即可: 整页明文 JSON 打出来 (供观察/后续落库)
|
||||
log.info("[SupplierAccountPull] page={} total={} 本页={} 明文: {}", pageNum, total, size, plain);
|
||||
|
||||
if (size == 0 || fetched >= total)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
log.info("[SupplierAccountPull] 本轮完成, 共 {} 条", fetched);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("[SupplierAccountPull] 拉取失败 (跳过, 下分钟再试)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -3,7 +3,9 @@ package com.ruoyi.business.service.impl;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.business.domain.BizMeeting;
|
||||
@@ -67,7 +69,19 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
||||
if (entity.getServiceAuditStage() == null || entity.getServiceAuditStage().isEmpty()) {
|
||||
entity.setServiceAuditStage("NOT_SUBMITTED");
|
||||
}
|
||||
return bizMeetingMapper.insert(entity);
|
||||
try {
|
||||
return bizMeetingMapper.insert(entity);
|
||||
}
|
||||
catch (DuplicateKeyException e) {
|
||||
// 入库时 meeting_id 撞库 (Redis 序列被重置/回退): 自动加 10 + 随机(0~10) 重新取一次, 不直接报错
|
||||
Long oldId = entity.getMeetingId();
|
||||
if (oldId == null) {
|
||||
throw e;
|
||||
}
|
||||
long retryId = oldId + 10L + ThreadLocalRandom.current().nextInt(11);
|
||||
entity.setMeetingId(retryId);
|
||||
return bizMeetingMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-4
@@ -79,9 +79,14 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
}
|
||||
|
||||
// 1. 创建 sys_user 子账号
|
||||
// 继承主账号 role_type, 避免子账号被 sys_user.role_type DEFAULT 'executor' 覆盖
|
||||
// (之前不写 roleType 时, sponsor 主账号的子账号会被 DEFAULT 错位成 executor)
|
||||
// role_type 取 person.unitType (sponsor/executor/doctor), 而非继承创建者角色:
|
||||
// 否则 admin/manager 在 admin/sponsor-people 建人会把子账号错位成 admin/manager (后台管理员).
|
||||
// 仅当 unitType 缺失时才回退到主账号 role_type 兜底.
|
||||
SysUser mainUser = mainUserId == null ? null : sysUserMapper.selectUserById(mainUserId);
|
||||
String roleType = entity.getUnitType();
|
||||
if (roleType == null || roleType.isEmpty()) {
|
||||
roleType = mainUser != null ? mainUser.getRoleType() : null;
|
||||
}
|
||||
SysUser newUser = new SysUser();
|
||||
newUser.setUserName(entity.getLoginUsername());
|
||||
newUser.setNickName(entity.getName());
|
||||
@@ -92,8 +97,8 @@ public class BizPersonServiceImpl implements IBizPersonService
|
||||
newUser.setParentUserId(mainUserId);
|
||||
newUser.setStatus("0");
|
||||
newUser.setDelFlag("0");
|
||||
if (mainUser != null && mainUser.getRoleType() != null) {
|
||||
newUser.setRoleType(mainUser.getRoleType());
|
||||
if (roleType != null) {
|
||||
newUser.setRoleType(roleType);
|
||||
}
|
||||
newUser.setCreateBy(SecurityUtils.getUsername());
|
||||
// mybatis useGeneratedKeys 自动回填 userId
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.ruoyi.business.supplier;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* 供应商账号接口 AES 解密工具 (对应 refer/BiddingSupplierAccountApiCodec).
|
||||
* <p>
|
||||
* 接口返回的 {@code data} 字段格式: {@code BSA.v1.<keyId>.<nonce>.<ciphertext>}
|
||||
* <ul>
|
||||
* <li>BSA — 固定前缀 (Bidding Supplier Account)</li>
|
||||
* <li>v1 — 版本号</li>
|
||||
* <li>keyId — 密钥标识, 对应配置 {@code supplier-account-api-aes.key-id}
|
||||
* (单密钥场景忽略, 密钥旋转时按此取对应 key)</li>
|
||||
* <li>nonce — 12 字节随机 IV, URL-safe Base64 编码 (无填充)</li>
|
||||
* <li>ciphertext — 密文, URL-safe Base64 编码 (无填充, 末尾带 16 字节 GCM 认证标签)</li>
|
||||
* </ul>
|
||||
* 算法: AES-256/GCM/NoPadding (密钥 32 字节, GCM 认证标签 128 bit).
|
||||
*/
|
||||
public class SupplierAccountApiCodec
|
||||
{
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
|
||||
private SupplierAccountApiCodec() {}
|
||||
|
||||
/**
|
||||
* 解密接口返回的 data 字段, 得到明文 JSON 字符串.
|
||||
*
|
||||
* @param data 接口返回的 data 字段 ({@code BSA.v1.keyId.nonce.ciphertext})
|
||||
* @param key AES 密钥 (Base64 编码, 解码后 32 字节)
|
||||
*/
|
||||
public static String decrypt(String data, String key)
|
||||
{
|
||||
if (data == null || data.isEmpty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String[] parts = data.split("\\.");
|
||||
if (parts.length < 5)
|
||||
{
|
||||
throw new IllegalArgumentException("供应商接口 data 格式非法 (期望 BSA.v1.keyId.nonce.ciphertext)");
|
||||
}
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
byte[] nonce = decodeBase64Url(parts[3]);
|
||||
byte[] ciphertext = decodeBase64Url(parts[4]);
|
||||
try
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"),
|
||||
new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IllegalStateException("供应商接口数据解密失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** URL-safe Base64 → bytes (兼容 '-'/'_' 且无填充) */
|
||||
private static byte[] decodeBase64Url(String s)
|
||||
{
|
||||
String b = s.replace('-', '+').replace('_', '/');
|
||||
while (b.length() % 4 != 0)
|
||||
{
|
||||
b += "=";
|
||||
}
|
||||
return Base64.getDecoder().decode(b);
|
||||
}
|
||||
}
|
||||
@@ -49,15 +49,17 @@
|
||||
<include refid="selectFields"/>
|
||||
<where>
|
||||
p.is_deleted = 0
|
||||
<if test="planName != null and planName != ''"> and p.plan_name like concat('%', #{planName}, '%')</if>
|
||||
<if test="planDirectionId != null"> and p.plan_direction_id = #{planDirectionId}</if>
|
||||
<if test="planCategory != null and planCategory != ''"> and p.plan_category = #{planCategory}</if>
|
||||
<if test="projectForm != null and projectForm != ''"> and p.project_form = #{projectForm}</if>
|
||||
<if test="submitterId != null"> and p.submitter_id = #{submitterId}</if>
|
||||
<choose>
|
||||
<!-- 选了具体状态: 等值匹配 -->
|
||||
<when test="status != null and status != ''"> and p.status = #{status}</when>
|
||||
<!-- 未选: 不加 status 过滤, 由前端按角色决定默认值 (经理侧默认查 1/2/3, 医生侧默认查全部含 0) -->
|
||||
</choose>
|
||||
<if test="remark != null and remark != ''"> and p.remark = #{remark}</if>
|
||||
<if test="remark != null and remark != ''"> and p.remark like concat('%', #{remark}, '%')</if>
|
||||
</where>
|
||||
order by p.plan_id desc
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user