diff --git a/superpowers/plans/2026-06-07-wechat-alipay-credentials-runtime-plan.md b/superpowers/plans/2026-06-07-wechat-alipay-credentials-runtime-plan.md
new file mode 100644
index 0000000..dd76ffe
--- /dev/null
+++ b/superpowers/plans/2026-06-07-wechat-alipay-credentials-runtime-plan.md
@@ -0,0 +1,398 @@
+# Wechat/Alipay Provider 凭证动态加载改造
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 修复 `WeixinAuthenticationProvider` / `AlipayAuthenticationProvider` 的凭证烧死 bug —— 改为持有 `AppCredentialsCache`、在请求时按 `X-App-Id` 动态解析凭证,使 SysApp CRUD / 缓存过期能立即生效。
+
+**Architecture:** Provider 改造为"工具注入 + 内部解析"模式。每个请求处理时,从请求头读 `X-App-Id` → 调 `AppCredentialsCache.get(appId)` → 拿最新凭证 → 动态构造 `WechatApiClient`/`AlipayApiClient` → 调第三方 API。`OAuth2ServerConfig` 简化为只负责依赖注入。
+
+**Tech Stack:** Java 21, Spring Boot 4.x, Spring Security OAuth2, Fastjson2, MyBatis Plus
+
+---
+
+## 文件结构
+
+### 修改
+- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
+ - 构造参数:`WechatApiClient` → `AppCredentialsCache`
+ - 新增私有方法 `currentRequestAppId()` 读 `X-App-Id`
+ - `buildToken()` 改为按 appId 解析凭证后动态构造 `WechatApiClient`
+
+- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
+ - 构造参数:`AlipayApiClient` → `AppCredentialsCache`
+ - `buildToken()` 同样按 appId 解析凭证后动态构造 `AlipayApiClient`
+
+- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
+ - 删 `resolveCredentials()` / `currentRequestAppId()` 私有方法
+ - 删 `new WechatApiClient(...)` / `new AlipayApiClient(...)` 单例构造
+ - 把 `appCredentialsCache` 直接传给两个 Provider
+ - 清理不再需要的 import
+
+### 验收点
+- [ ] 微信登录请求:第一次请求时 `WechatApiClient` 用 `X-App-Id` 解析的凭证调微信 API
+- [ ] SysApp 增删改后:缓存被 evict,下次请求自动用新凭证(不需重启)
+- [ ] 缓存过期 30min 后:下次请求自动从 DB 重新加载凭证
+- [ ] `X-App-Id` 缺失 / 凭证不存在:抛 `OAuth2AuthenticationException` + `server_error` + 描述含 appId
+- [ ] 编译通过 `rui-common-oauth2` 模块
+- [ ] 不影响 `PasswordAuthenticationProvider` / `SmsAuthenticationProvider`
+
+---
+
+## Task 1: WeixinAuthenticationProvider 改造
+
+**Files:**
+- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
+
+- [ ] **Step 1: 替换字段**
+
+ 把 `private final WechatApiClient wechatApiClient;` 改为:
+ ```java
+ private final AppCredentialsCache appCredentialsCache;
+ ```
+
+- [ ] **Step 2: 修改构造函数**
+
+ 构造参数 `WechatApiClient wechatApiClient` → `AppCredentialsCache appCredentialsCache`:
+ ```java
+ public WeixinAuthenticationProvider(AuthenticationManager authenticationManager,
+ OAuth2AuthorizationService authorizationService,
+ OAuth2TokenGenerator extends OAuth2Token> tokenGenerator,
+ AppCredentialsCache appCredentialsCache,
+ UserAuthFeign userAuthFeign) {
+ super(authenticationManager, authorizationService, tokenGenerator);
+ this.appCredentialsCache = appCredentialsCache;
+ this.userAuthFeign = userAuthFeign;
+ }
+ ```
+
+- [ ] **Step 3: 添加 `X-App-Id` 读取辅助方法**
+
+ ```java
+ /**
+ * 从当前请求上下文读取 X-App-Id 头。
+ *
+ * 微信/支付宝登录必须通过该头传递应用标识,
+ * 以支持多租户/多应用凭证隔离。
+ *
+ * @return appId;未传或读取失败返回 null
+ */
+ private String currentRequestAppId() {
+ try {
+ ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ if (attrs == null) {
+ return null;
+ }
+ HttpServletRequest request = attrs.getRequest();
+ return request.getHeader("X-App-Id");
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ ```
+
+ 需要的 import:
+ ```java
+ import com.rui.common.oauth2.cache.AppCredentialsCache;
+ import com.rui.common.oauth2.cache.AppCredentialsVO;
+ import jakarta.servlet.http.HttpServletRequest;
+ import org.springframework.web.context.request.RequestContextHolder;
+ import org.springframework.web.context.request.ServletRequestAttributes;
+ ```
+
+ 删除的 import:
+ ```java
+ // (如有) import com.rui.common.oauth2.authentication.weixin.WechatApiClient; // 字段类型变了,但本包内仍可访问
+ ```
+
+ 实际上 `WechatApiClient` 仍在 `buildToken` 里 new 出来用,import 不变。
+
+- [ ] **Step 4: 改造 `buildToken` 方法**
+
+ ```java
+ @Override
+ public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) {
+ String code = (String) reqParameters.get("code");
+ String phone = (String) reqParameters.get("phone");
+
+ // 1. 从请求头拿 X-App-Id
+ String appId = currentRequestAppId();
+ if (appId == null || appId.isBlank()) {
+ log.warn("微信登录缺少 X-App-Id 头");
+ throw new OAuth2AuthenticationException(new OAuth2Error(
+ OAuth2ErrorCodes.SERVER_ERROR,
+ "wechat login requires X-App-Id header",
+ ERROR_URI));
+ }
+
+ // 2. 从缓存拿凭证(30min TTL + 空对象防穿透 + 服务降级)
+ AppCredentialsVO creds = appCredentialsCache.get(appId);
+ if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
+ log.warn("微信登录凭证未配置或服务降级: appId={}", appId);
+ throw new OAuth2AuthenticationException(new OAuth2Error(
+ OAuth2ErrorCodes.SERVER_ERROR,
+ "wechat credentials not configured for appId=" + appId,
+ ERROR_URI));
+ }
+
+ // 3. 用最新凭证动态构造 API 客户端(支持 SysApp CRUD / 缓存过期即时生效)
+ WechatApiClient wechatApiClient = new WechatApiClient(creds.getAppId(), creds.getAppSecret());
+
+ // 4. 调用微信 API 换取 openId 和 unionId
+ WechatApiClient.WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code);
+ String openId = wxResponse.getOpenid();
+ String unionId = wxResponse.getUnionid();
+
+ log.info("微信登录: appId={}, openId={}, unionId={}, phone={}", appId, openId, unionId, phone);
+
+ // TODO: 这里需要调用 UserSocialService 查询绑定关系
+ // 暂时使用 openId 作为 principal
+ String principal = openId + "#" + unionId + "#" + (phone != null ? phone : "");
+ return new UsernamePasswordAuthenticationToken(principal, null);
+ }
+ ```
+
+ 需要的常量(类顶部添加):
+ ```java
+ private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+ ```
+
+- [ ] **Step 5: 编译验证**
+
+ ```bash
+ mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
+ ```
+
+ 预期:`BUILD SUCCESS`
+
+- [ ] **Step 6: Commit**
+
+ ```bash
+ git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java
+ git commit -m "refactor(oauth2): WeixinAuthenticationProvider 改为运行时解析凭证
+
+- 构造参数 WechatApiClient → AppCredentialsCache
+- buildToken 内按 X-App-Id 头解析凭证
+- 每次请求动态构造 WechatApiClient,支持 SysApp CRUD / 缓存过期即时生效
+- 凭证缺失抛 server_error(避免 ClassCastException)
+
+依赖 OAuth2ServerConfig 同步改造(Task 3)。"
+ ```
+
+---
+
+## Task 2: AlipayAuthenticationProvider 改造
+
+**Files:**
+- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
+
+- [ ] **Step 1: 替换字段**
+
+ 把 `private final AlipayApiClient alipayApiClient;` 改为:
+ ```java
+ private final AppCredentialsCache appCredentialsCache;
+ ```
+
+- [ ] **Step 2: 修改构造函数**
+
+ 构造参数 `AlipayApiClient alipayApiClient` → `AppCredentialsCache appCredentialsCache`:
+ ```java
+ public AlipayAuthenticationProvider(AuthenticationManager authenticationManager,
+ OAuth2AuthorizationService authorizationService,
+ OAuth2TokenGenerator extends OAuth2Token> tokenGenerator,
+ AppCredentialsCache appCredentialsCache) {
+ super(authenticationManager, authorizationService, tokenGenerator);
+ this.appCredentialsCache = appCredentialsCache;
+ }
+ ```
+
+- [ ] **Step 3: 添加 `X-App-Id` 读取辅助方法**(同 Task 1 Step 3)
+
+- [ ] **Step 4: 改造 `buildToken` 方法**
+
+ ```java
+ @Override
+ public UsernamePasswordAuthenticationToken buildToken(Map reqParameters) {
+ String code = (String) reqParameters.get("code");
+ String phone = (String) reqParameters.get("phone");
+
+ // 1. 从请求头拿 X-App-Id
+ String appId = currentRequestAppId();
+ if (appId == null || appId.isBlank()) {
+ log.warn("支付宝登录缺少 X-App-Id 头");
+ throw new OAuth2AuthenticationException(new OAuth2Error(
+ OAuth2ErrorCodes.SERVER_ERROR,
+ "alipay login requires X-App-Id header",
+ ERROR_URI));
+ }
+
+ // 2. 从缓存拿凭证
+ AppCredentialsVO creds = appCredentialsCache.get(appId);
+ if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
+ log.warn("支付宝登录凭证未配置或服务降级: appId={}", appId);
+ throw new OAuth2AuthenticationException(new OAuth2Error(
+ OAuth2ErrorCodes.SERVER_ERROR,
+ "alipay credentials not configured for appId=" + appId,
+ ERROR_URI));
+ }
+
+ // 3. 用最新凭证动态构造 API 客户端
+ AlipayApiClient alipayApiClient = new AlipayApiClient(
+ creds.getAppId(),
+ creds.getAppKey(),
+ creds.getAesKey() == null ? "" : creds.getAesKey());
+
+ // 4. 调用支付宝 API 获取 userId(按 spec 第 5.4 节:userId 作为唯一标识)
+ AlipayApiClient.AlipayTokenResponse alipayResponse = alipayApiClient.getAccessToken(code);
+ String userId = alipayResponse.getUserId();
+
+ log.info("支付宝登录: appId={}, userId={}, phone={}", appId, userId, phone);
+
+ // TODO: 查找或创建用户
+ String principal = userId + "#" + (phone != null ? phone : "");
+ return new UsernamePasswordAuthenticationToken(principal, null);
+ }
+ ```
+
+ > 注意:当前 `AlipayApiClient(String appId, String privateKey, String publicKey)` 构造签名是 3 参。
+ > `AppCredentialsVO` 暂未确认字段,先按 `getAppKey()` / `getAesKey()` 假设,**实施时如发现字段不匹配,停下来汇报,不要硬猜**。
+
+ 需要的 import(参考 Task 1)。
+
+- [ ] **Step 5: 编译验证**
+
+ ```bash
+ mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
+ ```
+
+- [ ] **Step 6: Commit**
+
+ ```bash
+ git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
+ git commit -m "refactor(oauth2): AlipayAuthenticationProvider 改为运行时解析凭证
+
+- 构造参数 AlipayApiClient → AppCredentialsCache
+- buildToken 内按 X-App-Id 头解析凭证
+- 每次请求动态构造 AlipayApiClient,支持 SysApp CRUD / 缓存过期即时生效
+- 凭证缺失抛 server_error
+
+依赖 OAuth2ServerConfig 同步改造(Task 3)。"
+ ```
+
+---
+
+## Task 3: OAuth2ServerConfig 清理
+
+**Files:**
+- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
+
+- [ ] **Step 1: 替换 Provider 实例化代码**
+
+ 在 `authorizationServerFilterChain` 方法内(约 130-141 行):
+
+ 删除:
+ ```java
+ // 微信:凭证从 X-App-Id 请求头 → AppCredentialsCache 拿
+ AppCredentialsVO wechatCreds = resolveCredentials(appCredentialsCache, "wechat");
+ WechatApiClient wechatApiClient = new WechatApiClient(
+ wechatCreds.getAppId(), wechatCreds.getAppSecret());
+ WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
+ authenticationManager, authorizationService, tokenGenerator, wechatApiClient, userAuthFeign);
+
+ // 支付宝:暂用空凭证(certificates 解析未完成,TODO 接 Alipay SDK 后改造)
+ // 当前 AlipayApiClient 在 buildToken 时会抛 UnsupportedOperationException(按 spec 占位)
+ AlipayApiClient alipayApiClient = new AlipayApiClient("", "", "");
+ AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
+ authenticationManager, authorizationService, tokenGenerator, alipayApiClient);
+ ```
+
+ 替换为:
+ ```java
+ // 微信 / 支付宝:凭证由 Provider 内部按 X-App-Id 头从 AppCredentialsCache 解析
+ WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
+ authenticationManager, authorizationService, tokenGenerator, appCredentialsCache, userAuthFeign);
+ AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
+ authenticationManager, authorizationService, tokenGenerator, appCredentialsCache);
+ ```
+
+- [ ] **Step 2: 删除 `resolveCredentials` / `currentRequestAppId` 私有方法**
+
+ 删除约 151-181 行的两个方法。
+
+- [ ] **Step 3: 清理不再需要的 import**
+
+ 删除:
+ ```java
+ import com.rui.common.oauth2.authentication.weixin.WechatApiClient;
+ import com.rui.common.oauth2.authentication.alipay.AlipayApiClient;
+ import com.rui.common.oauth2.cache.AppCredentialsVO;
+ import jakarta.servlet.http.HttpServletRequest;
+ import org.springframework.web.context.request.RequestContextHolder;
+ import org.springframework.web.context.request.ServletRequestAttributes;
+ ```
+
+- [ ] **Step 4: 编译验证**
+
+ ```bash
+ mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
+ ```
+
+ 预期:`BUILD SUCCESS`
+
+- [ ] **Step 5: Commit**
+
+ ```bash
+ git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java
+ git commit -m "refactor(oauth2): OAuth2ServerConfig 清理凭证启动期构造
+
+- 移除 resolveCredentials / currentRequestAppId 私有方法
+- 移除启动期 new WechatApiClient / new AlipayApiClient
+- Provider 构造改为直接注入 AppCredentialsCache
+- 凭证解析完全下放到 Provider buildToken 请求路径
+
+与 WeixinAuthenticationProvider / AlipayAuthenticationProvider 配合
+实现按 X-App-Id 头的运行时凭证加载。"
+ ```
+
+---
+
+## Task 4: 验证 AlipayApiClient 字段映射
+
+**Files:**
+- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayApiClient.java`
+- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/cache/AppCredentialsVO.java`
+
+- [ ] **Step 1: 核对构造签名**
+
+ 读取两个文件,确认:
+ - `AlipayApiClient` 构造参数类型与 `AppCredentialsVO` 提供的 getter 一一对应
+ - 如字段名不一致(如 `privateKey` vs `appSecret`),调整 Task 2 的代码
+
+- [ ] **Step 2: 必要时提交修复 commit**
+
+ 如发现字段不匹配,单独 commit:
+ ```bash
+ git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
+ git commit -m "fix(oauth2): 修正 AlipayApiClient 构造参数映射"
+ ```
+
+---
+
+## 验收检查清单
+
+| 验收点 | 对应任务 | 状态 |
+|--------|---------|------|
+| 微信登录按 X-App-Id 解析凭证 | Task 1 | [ ] |
+| 支付宝登录按 X-App-Id 解析凭证 | Task 2 | [ ] |
+| AppCredentialsCache 复用(30min TTL + 空对象穿透) | Task 1+2 | [ ] |
+| OAuth2ServerConfig 不再启动期构造 API 客户端 | Task 3 | [ ] |
+| 凭证缺失抛 OAuth2 server_error | Task 1+2 | [ ] |
+| 编译通过 rui-common-oauth2 | Task 1+2+3 Step 5 | [ ] |
+| 不影响 Password / Sms Provider | Task 1+2+3 | [ ] |
+| AlipayApiClient 字段映射正确 | Task 4 | [ ] |
+
+## 实施选项
+
+1. **Subagent-Driven(推荐)** - 我为每个任务分派独立的子代理,任务间审查,快速迭代
+2. **Inline Execution** - 在本会话中执行任务,批量执行并设置检查点
+
+**请选择执行方式?**