diff --git a/.gitignore b/.gitignore
index efeb494..2f4543c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,3 +89,6 @@ ry-vue2/ # 旧 vue 控制台
# ===== OCR 模型 (大二进制, 不进版本库, 部署时放 jar 同目录 models/) =====
ry-api/models/
ry-ocr-java/src/main/resources/models/
+
+# ===== OA 单点登录临时笔记 (含 OA 管理员密码, 不入库) =====
+oalogin/
diff --git a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
index 43bb07a..abbe8db 100644
--- a/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
+++ b/ry-api/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -25,6 +25,7 @@ import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.business.domain.BizExpert;
+import com.ruoyi.business.service.ComplianceLoginService;
import com.ruoyi.business.service.IBizExpertService;
/**
@@ -56,6 +57,9 @@ public class SysLoginController
@Autowired
private IBizExpertService expertService;
+ @Autowired
+ private ComplianceLoginService complianceLoginService;
+
/**
* 登录方法
*
@@ -70,6 +74,12 @@ public class SysLoginController
{
return AjaxResult.error("您的信息正在审核中");
}
+ // 合规人员/项目负责人门禁: 校验其仍存在于 ecology 视图, 角色变更则软删重建
+ String complianceGate = complianceGate(loginBody.getUsername());
+ if (complianceGate != null)
+ {
+ return AjaxResult.error(complianceGate);
+ }
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
@@ -101,6 +111,33 @@ public class SysLoginController
return expert != null && "1".equals(expert.getAuditStatus());
}
+ /**
+ * 密码登录门禁: 用户名命中 manager/leader 时校验其仍存在于 ecology 视图.
+ * 返回 null=放行, 非 null=错误提示 (直接报错, 不走密码登录).
+ */
+ private String complianceGate(String username)
+ {
+ if (username == null || username.isEmpty())
+ {
+ return null;
+ }
+ SysUser probe = userService.selectUserByUserName(username);
+ if (probe == null && username.contains("@"))
+ {
+ probe = userService.selectUserByEmail(username);
+ }
+ if (probe == null || probe.getUserId() == null)
+ {
+ return null;
+ }
+ String role = probe.getRoleType();
+ if (!"manager".equals(role) && !"leader".equals(role))
+ {
+ return null;
+ }
+ return complianceLoginService.gateForPasswordLogin(probe);
+ }
+
/**
* 获取用户信息
*
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-prod.yml b/ry-api/ruoyi-admin/src/main/resources/application-prod.yml
index 125515b..6c3343a 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application-prod.yml
@@ -10,6 +10,12 @@ spring:
url: jdbc:mysql://47.95.12.80:3306/hegui?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: hegui
password: C55zwt5L2TeB4kLB
+ # 合规(泛微 ecology)外部只读库, 登录校验合规人员/项目负责人用
+ ecology:
+ url: jdbc:mysql://oagm228.guojustar.com:3306/ecology?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
+ username: user_compliance
+ password: 'ComplexPass@2026'
+ driver-class-name: com.mysql.cj.jdbc.Driver
# 生产环境外部接口地址
ruoyi:
@@ -20,15 +26,15 @@ ruoyi:
base-url: https://hegui.bahim.org.cn/camera/
supplier-account-api:
-# base-url: https://zbsupplier.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
- base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
+ base-url: https://zbsupplier.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
+# base-url: https://zbsuppliertest.guojustar.com/supplier-api/bidding/supplier/openapi/accounts
supplier-account-api-aes:
key-id: supplier-api-key
key: 'kY0+oIO/laeaaaYDd+9TCYbCQR0b/vqMeJ9sUnSVd9U='
bidding-api:
-# notice-list-url: https://zbpublic.guojustar.com/portal-api/bidding/public/notice/list
-# detail-url: https://zbpublic.guojustar.com/#/detail/
- notice-list-url: https://zbpublictest.guojustar.com/portal-api/bidding/public/notice/list
- detail-url: https://zbpublictest.guojustar.com/#/detail/
+ notice-list-url: https://zbpublic.guojustar.com/portal-api/bidding/public/notice/list
+ detail-url: https://zbpublic.guojustar.com/#/detail/
+# notice-list-url: https://zbpublictest.guojustar.com/portal-api/bidding/public/notice/list
+# detail-url: https://zbpublictest.guojustar.com/#/detail/
diff --git a/ry-api/ruoyi-admin/src/main/resources/application-test.yml b/ry-api/ruoyi-admin/src/main/resources/application-test.yml
index 10211c1..9e936a2 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application-test.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application-test.yml
@@ -6,6 +6,12 @@ spring:
url: jdbc:mysql://mmos.uvwcloud.com:3306/guoju0808?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: Qljl1rh_
+ # 合规(泛微 ecology)外部只读库, 登录校验合规人员/项目负责人用
+ ecology:
+ url: jdbc:mysql://101.254.99.156:11120/ecology?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
+ username: user_compliance
+ password: 'ComplexPass@2026'
+ driver-class-name: com.mysql.cj.jdbc.Driver
# 测试环境外部接口地址
ruoyi:
diff --git a/ry-api/ruoyi-admin/src/main/resources/application.yml b/ry-api/ruoyi-admin/src/main/resources/application.yml
index 60135b7..257c176 100644
--- a/ry-api/ruoyi-admin/src/main/resources/application.yml
+++ b/ry-api/ruoyi-admin/src/main/resources/application.yml
@@ -97,7 +97,7 @@ spring:
basename: i18n/messages
profiles:
# 默认 prod 环境; 本地测试用 --spring.profiles.active=test (group 自动展开为 druid,test)
- active: prod
+ active: test
group:
test: druid,test
prod: druid,prod
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/EcologyDataSourceConfig.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/EcologyDataSourceConfig.java
new file mode 100644
index 0000000..0ee1ffb
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/config/EcologyDataSourceConfig.java
@@ -0,0 +1,42 @@
+package com.ruoyi.business.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * 合规(泛微 ecology)外部只读库数据源.
+ *
+ * 用于登录时校验合规人员(manager) / 项目负责人(leader) 是否仍存在于
+ * {@code v_compliance_project_manager} 视图 (按 mobile 手机号匹配).
+ *
+ * 独立 JdbcTemplate (bean 名 ecologyJdbcTemplate), 不接入主库事务 / MyBatis,
+ * 避免与 DruidConfig 的主从动态数据源 (dynamicDataSource @Primary) 产生二义性.
+ */
+@Configuration
+public class EcologyDataSourceConfig
+{
+ @Bean(name = "ecologyJdbcTemplate")
+ public JdbcTemplate ecologyJdbcTemplate(Environment env)
+ {
+ String url = env.getProperty("spring.datasource.ecology.url");
+ String username = env.getProperty("spring.datasource.ecology.username");
+ String password = env.getProperty("spring.datasource.ecology.password");
+ String driver = env.getProperty("spring.datasource.ecology.driver-class-name", "com.mysql.cj.jdbc.Driver");
+
+ DruidDataSource ds = new DruidDataSource();
+ ds.setUrl(url);
+ ds.setUsername(username);
+ ds.setPassword(password);
+ ds.setDriverClassName(driver);
+ // 只读小连接池, 登录校验用
+ ds.setInitialSize(1);
+ ds.setMinIdle(1);
+ ds.setMaxActive(3);
+ ds.setMaxWait(3000);
+ ds.setValidationQuery("SELECT 1");
+ return new JdbcTemplate(ds);
+ }
+}
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
index 0d74d1c..9cb6489 100644
--- a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/controller/BizAuthController.java
@@ -19,7 +19,9 @@ import com.ruoyi.business.dto.SmsValidForm;
import com.ruoyi.business.mapper.BizPersonMapper;
import com.ruoyi.business.service.IBizExpertService;
import com.ruoyi.business.service.IBizOrgService;
+import com.ruoyi.business.service.ComplianceLoginService;
import com.ruoyi.business.service.SysSmsService;
+import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.id.SnowflakeId;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.controller.BaseController;
@@ -69,6 +71,9 @@ public class BizAuthController extends BaseController {
@Autowired
private IBizExpertService expertService;
+ @Autowired
+ private ComplianceLoginService complianceLoginService;
+
@Autowired
private BizPersonMapper bizPersonMapper;
@@ -101,8 +106,8 @@ public class BizAuthController extends BaseController {
if (phone == null || !phone.matches("^1\\d{10}$")) {
return error("请输入正确的手机号");
}
- // 登录反义: 手机号未注册则不发
- if (!userService.isPhoneRegistered(phone)) {
+ // 登录反义: 手机号未注册则不发; 但合规人员/项目负责人可从 ecology 视图自动建号, 放行发码
+ if (!userService.isPhoneRegistered(phone) && !complianceLoginService.existsInEcology(phone)) {
return error("该手机号未注册, 请先注册账号");
}
String uuid = smsService.sendCode(phone);
@@ -138,15 +143,15 @@ public class BizAuthController extends BaseController {
return error("验证码错误或已过期: " + e.getMessage());
}
- // 3. 查用户 (checkPhoneUnique 只 select user_id+phonenumber, 缺 role_type/user_name/status,
- // 需再按 userId 查完整 user, 否则 LoginUser 里 roleType=null 导致前端拿不到角色而跳回登录页)
- SysUser probe = userMapper.checkPhoneUnique(phone);
- if (probe == null || probe.getUserId() == null) {
- return error("该手机号未注册");
+ // 3. 查用户 + 合规人员/项目负责人身份校验 (可能从 ecology 视图自动建号)
+ SysUser user;
+ try {
+ user = complianceLoginService.reconcileForLogin(phone);
+ } catch (ServiceException e) {
+ return error(e.getMessage());
}
- SysUser user = userMapper.selectUserById(probe.getUserId());
if (user == null) {
- return error("该手机号未注册");
+ return error("用户不存在");
}
// 4. 校验用户状态
diff --git a/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/ComplianceLoginService.java b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/ComplianceLoginService.java
new file mode 100644
index 0000000..6eba4e2
--- /dev/null
+++ b/ry-api/ruoyi-business/src/main/java/com/ruoyi/business/service/ComplianceLoginService.java
@@ -0,0 +1,200 @@
+package com.ruoyi.business.service;
+
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.business.domain.BizPerson;
+import com.ruoyi.business.mapper.BizPersonMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.id.SnowflakeId;
+import com.ruoyi.system.mapper.SysUserMapper;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 合规人员(manager) / 项目负责人(leader) 登录身份校验 + 自动建号.
+ *
+ * 单一可信源 = 泛微 ecology 的 {@code v_compliance_project_manager} 视图 (按 mobile 手机号):
+ * - type=1 → 合规人员 → role_type=manager
+ * - type=2 → 项目负责人 → role_type=leader
+ *
+ * 登录决策 (手机号 vs 视图 vs sys_user):
+ * - 已注册且非 manager/leader → 不查视图, 直接放行 (走原逻辑)
+ * - 视图里没有 → 报"用户不存在"
+ * - 视图里有 + sys_user 没有 → 入库(sys_user + biz_person, 登录名=手机号, 密码留空) 后放行
+ * - 视图里有 + sys_user 有 + 角色匹配 → 放行
+ * - 视图里有 + sys_user 有 + 角色不匹配 → 软删旧 + 建新 后放行
+ */
+@Service
+public class ComplianceLoginService
+{
+ private static final Logger log = LoggerFactory.getLogger(ComplianceLoginService.class);
+
+ private static final String ECOLOGY_LOOKUP_SQL =
+ "SELECT lastname, type FROM v_compliance_project_manager WHERE mobile = ? LIMIT 1";
+
+ @Autowired
+ @Qualifier("ecologyJdbcTemplate")
+ private JdbcTemplate ecologyJdbcTemplate;
+
+ @Autowired
+ private SysUserMapper userMapper;
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private BizPersonMapper bizPersonMapper;
+
+ /**
+ * 短信登录主路径: 查用户 + 合规/负责人校验, 返回最终要登录的 SysUser.
+ * 无资格时抛 {@link ServiceException}("用户不存在").
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public SysUser reconcileForLogin(String phone)
+ {
+ SysUser user = findUserByPhone(phone);
+ String role = user == null ? null : user.getRoleType();
+ boolean needEcology = user == null || "manager".equals(role) || "leader".equals(role);
+ if (!needEcology)
+ {
+ return user;
+ }
+
+ EcologyPerson p = queryEcology(phone);
+ if (p == null)
+ {
+ throw new ServiceException("用户不存在");
+ }
+ String expectedRole = p.type == 1 ? "manager" : "leader";
+
+ if (user == null)
+ {
+ // sys_user 没有 → 自动入库
+ return provision(p, phone, expectedRole);
+ }
+ if (expectedRole.equals(role))
+ {
+ // 角色匹配 → 直接放行
+ return user;
+ }
+ // 角色不匹配 → 软删旧 + 建新
+ userService.deleteUserByIds(new Long[] { user.getUserId() });
+ return provision(p, phone, expectedRole);
+ }
+
+ /**
+ * 密码登录门禁: 已解析出的 manager/leader 校验其是否仍在视图、角色是否一致.
+ * 返回 null=放行; 非 null=错误提示 (此时调用方应直接报错, 不再走密码登录).
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public String gateForPasswordLogin(SysUser user)
+ {
+ String phone = user.getPhonenumber();
+ EcologyPerson p = queryEcology(phone == null ? "" : phone);
+ if (p == null)
+ {
+ return "用户不存在";
+ }
+ String expectedRole = p.type == 1 ? "manager" : "leader";
+ if (expectedRole.equals(user.getRoleType()))
+ {
+ return null;
+ }
+ // 角色不匹配 → 软删旧 + 建新 (新账号密码留空, 只能短信登录)
+ userService.deleteUserByIds(new Long[] { user.getUserId() });
+ provision(p, phone, expectedRole);
+ return "账号角色已更新, 请使用手机验证码登录";
+ }
+
+ /**
+ * 手机号是否存在于 ecology 视图 (短信发码前放行合规/负责人未注册手机号用).
+ */
+ public boolean existsInEcology(String phone)
+ {
+ return queryEcology(phone) != null;
+ }
+
+ private SysUser findUserByPhone(String phone)
+ {
+ SysUser probe = userMapper.checkPhoneUnique(phone);
+ if (probe == null || probe.getUserId() == null)
+ {
+ return null;
+ }
+ return userMapper.selectUserById(probe.getUserId());
+ }
+
+ private EcologyPerson queryEcology(String phone)
+ {
+ if (phone == null || phone.isEmpty())
+ {
+ return null;
+ }
+ try
+ {
+ List