diff --git a/deploy/userbase.nacos.yaml b/deploy/userbase.nacos.yaml
index 41698d5..f217d6b 100644
--- a/deploy/userbase.nacos.yaml
+++ b/deploy/userbase.nacos.yaml
@@ -83,12 +83,6 @@ yh:
mobi-api-pattern: /mobiapi/*
app-auth:
enabled: true
- # 本应用自身身份(客户端签名用,若 UserBase 需调用其他带签名服务)
- app-key: 100616
- app-secret: 1febb9502d2744b6a235d3e4cfde4a2d
- # 受信应用(服务端验签用):appKey -> appSecret
- trusted-apps:
- 100616: 1febb9502d2744b6a235d3e4cfde4a2d
ADMIN_TAG_ID: 290
diff --git a/docker/application-prod.yml b/docker/application-prod.yml
index 6252aed..b88f43f 100644
--- a/docker/application-prod.yml
+++ b/docker/application-prod.yml
@@ -69,12 +69,6 @@ yh:
mobi-api-pattern: /mobiapi/*
app-auth:
enabled: true
- # 本应用自身身份(客户端签名用,若 UserBase 需调用其他带签名服务)
- app-key: 100616
- app-secret: 1febb9502d2744b6a235d3e4cfde4a2d
- # 受信应用(服务端验签用):appKey -> appSecret
- trusted-apps:
- 100616: 1febb9502d2744b6a235d3e4cfde4a2d
ADMIN_TAG_ID: 290
diff --git a/src/main/java/com/yunhai/userbase/config/AppSecretConfig.java b/src/main/java/com/yunhai/userbase/config/AppSecretConfig.java
index 738087f..08dcd00 100644
--- a/src/main/java/com/yunhai/userbase/config/AppSecretConfig.java
+++ b/src/main/java/com/yunhai/userbase/config/AppSecretConfig.java
@@ -2,6 +2,7 @@ package com.yunhai.userbase.config;
import com.yunhai.platform.appauth.AppAuthProperties;
import com.yunhai.platform.appauth.AppSecretProvider;
+import com.yunhai.platform.appauth.LocalAppIdentityProvider;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -10,7 +11,9 @@ import org.springframework.context.annotation.Configuration;
* 用户中心验签密钥源:UserBase 作为密钥权威源,优先从本库 application_info 解析调用方密钥
* (覆盖全部「已颁发」应用),并以配置式 {@code yh.app-auth.trustedApps} 兜底平台自身默认密钥。
* 显式覆盖 yh-app-auth 默认的 {@code @ConditionalOnMissingBean} 实现,使开放接口签名校验
- * 不依赖 Redis 中 {@code sign:app:*} 的预填充时机,且能验签所有已颁发应用。
+ * 不依赖 Redis 中 {@code sign:app:*} 的预填充时机,且能验签所有已颁发应用。
+ * 同时注册 UserBase 自身作为出站调用方的身份:固定 appKey=100000,密钥落库,不再依赖 yml
+ * 配置 app-key/app-secret。
*/
@Configuration
public class AppSecretConfig {
@@ -20,4 +23,9 @@ public class AppSecretConfig {
AppAuthProperties properties) {
return new UserBaseAppSecretProvider(repository, properties);
}
+
+ @Bean
+ public LocalAppIdentityProvider localAppIdentityProvider(ApplicationInfoRepository repository) {
+ return new UserBaseLocalAppIdentityProvider(repository);
+ }
}
diff --git a/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java
new file mode 100644
index 0000000..b027951
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/config/UserBaseLocalAppIdentityProvider.java
@@ -0,0 +1,63 @@
+package com.yunhai.userbase.config;
+
+import com.yunhai.platform.appauth.LocalAppIdentityProvider;
+import com.yunhai.userbase.entity.ApplicationInfo;
+import com.yunhai.userbase.repository.ApplicationInfoRepository;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * UserBase 自身作为「出站签名调用方」的身份提供方。
+ * 约定:
+ * 1) appKey 固定占用 100000(基石应用段),由 application_bootstrap.sql 初始化脚本幂等插入;
+ * 2) appSecret 从 application_info 表按 appKey=100000 读取,带 60s 本地缓存(密钥重设后 60s 内生效)。
+ * 显式覆盖 yh-app-auth-spring-boot-starter 默认的 PropertyLocalAppIdentityProvider(yml 不再需要
+ * 配置 yh.app-auth.app-key / app-secret)。
+ */
+public class UserBaseLocalAppIdentityProvider implements LocalAppIdentityProvider {
+
+ private static final String USER_CENTER_APP_KEY = "100000";
+ private static final int USER_CENTER_APP_KEY_INT = 100000;
+ private static final long TTL_MS = 60_000L;
+
+ private final ApplicationInfoRepository repository;
+ private final ConcurrentHashMap cache = new ConcurrentHashMap<>();
+
+ public UserBaseLocalAppIdentityProvider(ApplicationInfoRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public String getAppKey() {
+ return USER_CENTER_APP_KEY;
+ }
+
+ @Override
+ public String getAppSecret() {
+ long now = System.currentTimeMillis();
+ CacheEntry hit = cache.get(USER_CENTER_APP_KEY);
+ if (hit != null && now - hit.ts < TTL_MS) {
+ return hit.secret;
+ }
+ String secret = null;
+ try {
+ ApplicationInfo info = repository.findByAppKey(USER_CENTER_APP_KEY_INT);
+ if (info != null && info.getAppSecret() != null && !info.getAppSecret().trim().isEmpty()) {
+ secret = info.getAppSecret();
+ }
+ } catch (Exception ignored) {
+ }
+ cache.put(USER_CENTER_APP_KEY, new CacheEntry(now, secret));
+ return secret;
+ }
+
+ private static final class CacheEntry {
+ final long ts;
+ final String secret;
+
+ CacheEntry(long ts, String secret) {
+ this.ts = ts;
+ this.secret = secret;
+ }
+ }
+}
diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java b/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java
index bb187c9..4abe136 100644
--- a/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java
+++ b/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java
@@ -1,17 +1,29 @@
package com.yunhai.userbase.controller.oapi;
+import com.yunhai.platform.utils.model.BoaGroup;
+import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
+import com.yunhai.userbase.model.groupauthz.UserPermissionAppVo;
+import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo;
+import com.yunhai.userbase.model.groupauthz.UserPermissionModuleVo;
+import com.yunhai.userbase.service.IGroupAuthzService;
import com.yunhai.userbase.service.UserCenterService;
+import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
/**
* 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /open/api/user/...})。
* 路径与基础平台 {@code DefaultUserCenterClient} 硬编码的契约严格一致:
- * {@code GET /open/api/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}。
+ * {@code GET /open/api/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、
+ * {@code /open/api/user/modules?userId=&schId=}。
* 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求,
* 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。
*/
@@ -22,6 +34,9 @@ public class OpenBaseController {
@Autowired
private UserCenterService userCenterService;
+ @Autowired
+ private IGroupAuthzService groupAuthzService;
+
@GetMapping("byId")
public BoaUser byId(@RequestParam("id") Integer id) {
return userCenterService.getUserInfoWithDeptById(id);
@@ -42,4 +57,78 @@ public class OpenBaseController {
}
return userCenterService.getUserInfoWithDeptByPhone(phone);
}
+
+ /**
+ * 查询用户在指定学校下的已授权模块列表。
+ * 若 {@code schId} 为 null 时,自动取用户的主学校;若仍无主学校信息,退化为取用户主归属组(groups 第 0 个)。
+ *
+ * @param userId 用户ID(必填)
+ * @param schId 学校ID/组ID(可选;为 null 时按上述规则推断)
+ * @return 扁平化的已授权模块列表(含基础平台统一 BoaModule 模型);无权限时返回空列表
+ */
+ @GetMapping("modules")
+ public List modules(@RequestParam("userId") Integer userId,
+ @RequestParam(value = "schId", required = false) Integer schId) {
+ if (userId == null) {
+ return Collections.emptyList();
+ }
+ Integer effectiveGroupId = resolveEffectiveGroupId(userId, schId);
+ if (effectiveGroupId == null) {
+ return Collections.emptyList();
+ }
+ List tree = groupAuthzService.findUserPermissionTree(effectiveGroupId, userId);
+ if (tree == null || tree.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List result = new ArrayList<>();
+ for (UserPermissionCategoryVo category : tree) {
+ if (category.getApps() == null) continue;
+ for (UserPermissionAppVo appVo : category.getApps()) {
+ String icon = appVo.getPcIcon();
+ collectModules(appVo.getModules(), icon, result);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 递归收集已授权模块树到扁平列表,并把应用 PC 图标作为模块的 icon 兜底(BoaModule.icon)。
+ */
+ private void collectModules(List modules,
+ String appIcon,
+ List collector) {
+ if (modules == null || modules.isEmpty()) {
+ return;
+ }
+ for (UserPermissionModuleVo m : modules) {
+ BoaModule bm = new BoaModule();
+ BeanUtils.copyProperties(m, bm);
+ bm.setIcon(appIcon);
+ collector.add(bm);
+ collectModules(m.getChildren(), appIcon, collector);
+ }
+ }
+
+ /**
+ * 按「schId 为空时」解析出用户主学校/主组作为模块查询的上下文组ID。
+ */
+ private Integer resolveEffectiveGroupId(Integer userId, Integer schId) {
+ if (schId != null) {
+ return schId;
+ }
+ BoaUser boa = userCenterService.getUserInfoWithDeptById(userId);
+ if (boa == null) {
+ return null;
+ }
+ BoaGroup school = boa.getSchool();
+ if (school != null && school.getId() != null) {
+ return school.getId();
+ }
+ List groups = boa.getGroups();
+ if (groups != null && !groups.isEmpty()) {
+ BoaGroup main = groups.get(0);
+ return main == null ? null : main.getId();
+ }
+ return null;
+ }
}
diff --git a/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java
index c1cef04..8bde07a 100644
--- a/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java
+++ b/src/main/java/com/yunhai/userbase/repository/ApplicationInfoRepository.java
@@ -17,7 +17,8 @@ import java.util.List;
* @description
*/
public interface ApplicationInfoRepository extends JpaRepository, JpaSpecificationExecutor {
- Integer countByCategoryId(Integer id);
+ @Query("SELECT COUNT(a.id) FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000")
+ Integer countByCategoryId(@Param("categoryId") Integer id);
boolean existsByName(String name);
@@ -54,21 +55,23 @@ public interface ApplicationInfoRepository extends JpaRepository
* 使用构造器表达式做 DTO 投影,避免加载整个实体(含 appSecret、各类 URL 等无关大字段)。
+ * 排除用户中心自身的基石应用(appKey=100000)。
*
* @param categoryId 应用类型ID
* @return 该类型下全部应用的排序视图,按 sortNo 升序(未排序排末尾),id 升序兜底
*/
@Query("SELECT new com.yunhai.userbase.model.application.ApplicationInfoSortVo(a.id, a.name, a.sortNo) " +
- "FROM ApplicationInfo a WHERE a.categoryId = :categoryId ORDER BY a.sortNo ASC, a.id ASC")
+ "FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000 ORDER BY a.sortNo ASC, a.id ASC")
List findSortVoByCategoryId(@Param("categoryId") Integer categoryId);
/**
* 应用授权用:查询全部应用的扁平投影(含 categoryId 供按类型分组),仅取 id/name/pcIcon。
* 按 categoryId、sortNo、id 升序,便于分组后同类型内保持排序。
+ * 排除用户中心自身的基石应用(appKey=100000)。
*
* @return 全部应用的授权投影行
*/
@Query("SELECT new com.yunhai.userbase.model.appauthz.AppAuthzAppRow(a.categoryId, a.id, a.name, a.pcIcon) " +
- "FROM ApplicationInfo a ORDER BY a.categoryId ASC, a.sortNo ASC, a.id ASC")
+ "FROM ApplicationInfo a WHERE a.appKey > 100000 ORDER BY a.categoryId ASC, a.sortNo ASC, a.id ASC")
List findAllAuthzRows();
}
diff --git a/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java b/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java
index 807a483..51ad550 100644
--- a/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java
+++ b/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java
@@ -191,6 +191,7 @@ public class ApplicationInfoService implements IApplicationInfoService {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> cq, CriteriaBuilder cb) {
List predicates = new ArrayList();
+ predicates.add(cb.greaterThan(root.get("appKey"), 100000));
if (query != null) {
if (StringUtils.isNotEmpty(query.getName())) {
String keyword = query.getName();
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index ef71b75..f56c5c1 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,80 +1,73 @@
server:
- port: 8060
+ port: 8060
spring:
- mvc:
- date-format: yyyy-MM-dd HH:mm:ss
- servlet:
- multipart:
- max-file-size: 50MB # 单个文件大小
- max-request-size: 200MB # 单次请求总大小(多文件/表单整体上限,避免单文件没超但整体被拒)
- jackson:
- time-zone: GMT+8
- date-format: yyyy-MM-dd HH:mm:ss
- redis:
- host: 127.0.0.1
- port: 6379
- database: 0
- password: 123456
- # 连接/命令超时:让后台探活(不可达时)快速失败,避免阻塞定时器线程;不影响正常请求
- timeout: 500ms
- data:
+ mvc:
+ date-format: yyyy-MM-dd HH:mm:ss
+ servlet:
+ multipart:
+ max-file-size: 50MB # 单个文件大小
+ max-request-size: 200MB # 单次请求总大小(多文件/表单整体上限,避免单文件没超但整体被拒)
+ jackson:
+ time-zone: GMT+8
+ date-format: yyyy-MM-dd HH:mm:ss
redis:
- repositories:
- enabled: false
- http:
- encoding:
- force: true
- charset: utf-8
- enabled: true
+ host: 127.0.0.1
+ port: 6379
+ database: 0
+ password: 123456
+ # 连接/命令超时:让后台探活(不可达时)快速失败,避免阻塞定时器线程;不影响正常请求
+ timeout: 500ms
+ data:
+ redis:
+ repositories:
+ enabled: false
+ http:
+ encoding:
+ force: true
+ charset: utf-8
+ enabled: true
- datasource:
- url: jdbc:mysql://localhost:3306/bsch_userbase?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
- username: root
- password: 123456
- driver-class-name: com.mysql.cj.jdbc.Driver
- min-idle: 30
- max-active: 30
- #jpa
- jpa:
- database: MySQL
- show-sql: true
- hibernate:
- # 生产环境设置成 none,避免程序运行时自动更新数据库结构
- ddl-auto: update
- database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
- properties:
- hibernate.order_by.default_null_ordering: last
- hibernate:
- dialect: org.hibernate.dialect.MySQL5InnoDBDialect
- open-in-view: true
-
- #本地关闭rabbit
- rabbitmq:
- listener:
- direct:
- auto-startup: false
- simple:
- auto-startup: false
+ datasource:
+ url: jdbc:mysql://localhost:3306/bsch_userbase?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
+ username: root
+ password: 123456
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ min-idle: 30
+ max-active: 30
+ #jpa
+ jpa:
+ database: MySQL
+ show-sql: true
+ hibernate:
+ # 生产环境设置成 none,避免程序运行时自动更新数据库结构
+ ddl-auto: update
+ database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
+ properties:
+ hibernate.order_by.default_null_ordering: last
+ hibernate:
+ dialect: org.hibernate.dialect.MySQL5InnoDBDialect
+ open-in-view: true
+ #本地关闭rabbit
+ rabbitmq:
+ listener:
+ direct:
+ auto-startup: false
+ simple:
+ auto-startup: false
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
yh:
- sso:
- server-url-prefix: http://sso.yunhaixingtong.com/cas/
- client-host-url: http://localhost:8060
- allow-headers: all
- allow-origins: all
- url-pattern: /api/*
- mobi-api-pattern: /mobiapi/*
- app-auth:
- enabled: true
- # 本应用自身身份(客户端签名用,若 UserBase 需调用其他带签名服务)
- app-key: 100616
- app-secret: 1febb9502d2744b6a235d3e4cfde4a2d
- # 受信应用(服务端验签用):appKey -> appSecret
- trusted-apps:
- 100616: 1febb9502d2744b6a235d3e4cfde4a2d
+ sso:
+ server-url-prefix: http://sso.yunhaixingtong.com/cas/
+ client-host-url: http://localhost:8060
+ allow-headers: all
+ allow-origins: all
+ url-pattern: /api/*
+ mobi-api-pattern: /mobiapi/*
+ app-auth:
+ enabled: true
ADMIN_TAG_ID: 290
diff --git a/src/main/resources/sql/application_bootstrap.sql b/src/main/resources/sql/application_bootstrap.sql
new file mode 100644
index 0000000..c85223c
--- /dev/null
+++ b/src/main/resources/sql/application_bootstrap.sql
@@ -0,0 +1,37 @@
+-- ============================================================
+-- 用户中心自身应用身份初始化(appKey=100000,后续业务应用从 100001 起)
+-- ------------------------------------------------------------
+-- 适用数据库:MySQL 5.7 / 8.0
+-- 运行方式:
+-- mysql --default-character-set=utf8mb4 -u -p < application_bootstrap.sql
+-- 前置:表结构由 Hibernate(ddl-auto=update)创建,请先启动一次应用生成
+-- application_info 表后再执行本脚本;如已执行请忽略(幂等)。
+-- 说明:
+-- 1. 本脚本按 app_key=100000 做幂等判断,重复执行不会新增重复记录。
+-- 2. 用户中心作为「应用平台基石/默认调用方」,自身也需要一个合法的应用身份,
+-- 用于对外发起 yh-app-auth 签名的应用签名请求;因此固定占用 appKey=100000。
+-- 3. appSecret 每次执行脚本若不存在时会重新生成一个 UUID(32 位十六进制);
+-- 已存在记录不会被修改,生产上如需重置请手动 UPDATE 或在应用管理界面点「重设密钥」。
+-- 4. 业务应用新增时 ApplicationInfoService 会按 MAX(app_key)+1 分配:
+-- 因此本脚本写入 100000 后,下一条业务应用 appKey=100001,与预期一致。
+-- 5. AUTO_INCREMENT=100001 仅设置 application_info 表的主键 id(非 appKey),
+-- 使后续新建行的主键 id 也从 100001 起,与 appKey 段对齐,便于排查。
+-- ============================================================
+
+SET NAMES utf8mb4;
+
+-- 1) 插入用户中心自身身份(appKey=100000),已存在则跳过
+INSERT INTO application_info (app_key, name, app_secret, app_desc, create_user, create_time, update_user, update_time)
+SELECT 100000,
+ '用户中心平台',
+ REPLACE(UUID(), '-', ''),
+ '用户中心自身应用身份(基石应用,不对外展示于应用管理列表)',
+ 0,
+ NOW(),
+ 0,
+ NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM application_info WHERE app_key = 100000);
+
+-- 2) 调整主键 id 自增起点,使其与 appKey 段对齐(100001 起)
+ALTER TABLE application_info AUTO_INCREMENT = 100001;