From 1fca1a2e2ef34b2ccf4778940c29dc674018540b Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 19 Jan 2026 16:22:21 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E7=8E=AF=E5=A2=83=E7=9A=84secret=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BF=A1=E6=81=AF=E3=80=82=202.=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E5=9C=B0=E5=9D=80=EF=BC=8C=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E6=98=AF=E9=92=89=E9=92=89=E5=BA=94=E7=94=A8=E5=88=99=E7=94=A8?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=9A=84=E5=9B=9E=E8=B0=83=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E3=80=82=203.=20=E9=AB=98=E9=A2=91=E9=94=99=E8=AF=AF=E8=AF=84?= =?UTF-8?q?=E6=9F=A5=E7=82=B9=E6=94=B9=E6=88=90=E6=98=BE=E7=A4=BA=E5=87=BA?= =?UTF-8?q?=E9=94=99=E6=AC=A1=E6=95=B0=E3=80=82=204.=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E7=9A=84=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=97=E8=A1=A8=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E4=BF=AE=E6=94=B9=E7=8A=B6=E6=80=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/login/oauth-client.ts | 26 ++++++- app/components/ui/Switch.tsx | 49 +++++++++++++ app/config/api-config.ts | 28 +++++--- app/config/oauth-secret.server.ts | 33 ++++++++- app/routes/callback.tsx | 31 ++++++++- app/routes/home.tsx | 7 +- app/styles/components/switch.css | 111 ++++++++++++++++++++++++++++++ ecosystem.config.cjs | 3 +- 8 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 app/components/ui/Switch.tsx create mode 100644 app/styles/components/switch.css diff --git a/app/api/login/oauth-client.ts b/app/api/login/oauth-client.ts index 68d8fa4..bdae049 100644 --- a/app/api/login/oauth-client.ts +++ b/app/api/login/oauth-client.ts @@ -43,6 +43,7 @@ interface UserInfoResponse { export class OAuthClient { private config: OAuthConfig; + private runtimeRedirectUri?: string; // 运行时回调地址(用于钉钉/内网动态切换) constructor(config: OAuthConfig) { this.config = { @@ -64,6 +65,28 @@ export class OAuthClient { } } + /** + * 设置运行时回调地址(用于钉钉/内网动态切换) + * @param redirectUri 回调地址 + */ + setRedirectUri(redirectUri: string): void { + this.runtimeRedirectUri = redirectUri; + console.log('🔧 [OAuthClient] 运行时回调地址已设置:', redirectUri); + } + + /** + * 获取当前使用的回调地址 + * 优先使用运行时设置的回调地址,否则使用配置中的默认地址 + * @returns 回调地址 + */ + private getRedirectUri(): string { + const uri = this.runtimeRedirectUri || this.config.redirectUri || ''; + if (this.runtimeRedirectUri) { + console.log('🔧 [OAuthClient] 使用运行时回调地址:', uri); + } + return uri; + } + /** * 生成授权URL * @param state 状态值,建议包含随机字符串和_idp后缀 @@ -96,12 +119,13 @@ export class OAuthClient { */ async getAccessToken(code: string): Promise { const url = `${this.config.serverUrl}/oauth/token`; + const redirectUri = this.getRedirectUri(); // 使用动态回调地址 const data = new URLSearchParams({ grant_type: 'authorization_code', code: code, client_id: this.config.clientId, client_secret: this.config.clientSecret || '', // 提供默认值避免类型错误 - redirect_uri: this.config.redirectUri + redirect_uri: redirectUri }); console.log('🔧 请求Token URL:', url); diff --git a/app/components/ui/Switch.tsx b/app/components/ui/Switch.tsx new file mode 100644 index 0000000..508e985 --- /dev/null +++ b/app/components/ui/Switch.tsx @@ -0,0 +1,49 @@ +/* 开关组件 - 用于切换启用/禁用状态 */ + +interface SwitchProps { + checked: boolean; + onChange?: (checked: boolean) => void; + disabled?: boolean; + loading?: boolean; + className?: string; + id?: string; +} + +export function Switch({ + checked, + onChange, + disabled = false, + loading = false, + className = '', + id +}: SwitchProps) { + const handleClick = () => { + if (!disabled && !loading && onChange) { + onChange(!checked); + } + }; + + const isDisabled = disabled || loading; + + return ( + + ); +} diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 505c7d9..cb635ec 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -23,8 +23,10 @@ interface ApiConfig { clientId?: string; // OAuth2应用Client Secret clientSecret?: string; - // 回调地址 + // 回调地址(内网Web) redirectUri?: string; + // 钉钉Web回调地址(互联网地址) + dingtalkRedirectUri?: string; // 应用ID(用于登出) appId?: string; }; @@ -39,7 +41,7 @@ interface ApiConfig { // 端口特定配置映射 // 根据不同端口提供不同的API配置 -const portConfigs: Record> = { +export const portConfigs: Record> = { // 主要 // 梅州 @@ -65,7 +67,9 @@ const portConfigs: Record> = { appUrl: 'http://10.79.97.17:51703', oauth: { - redirectUri: 'http://10.79.97.17:51703/callback' + redirectUri: 'http://10.79.97.17:51703/callback', + // 钉钉Web回调地址(互联网地址)- 需要根据实际部署修改 + dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51703 || 'https://10-79-97-1751703-b2oaixksdrrisox0t3.ztna-dingtalk.com/callback' } }, @@ -84,7 +88,8 @@ const portConfigs: Record> = { collaboraUrl: 'http://10.79.97.17:9980', appUrl: 'http://10.79.97.17:51704', oauth: { - redirectUri: 'http://10.79.97.17:51704/callback' + redirectUri: 'http://10.79.97.17:51704/callback', + dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51704 || 'https://10-79-97-1751704-xxxxxxxxx.ztna-dingtalk.com/callback' } }, @@ -96,7 +101,8 @@ const portConfigs: Record> = { collaboraUrl: 'http://10.79.97.17:9980', appUrl: 'http://10.79.97.17:51705', oauth: { - redirectUri: 'http://10.79.97.17:51705/callback' + redirectUri: 'http://10.79.97.17:51705/callback', + dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51705 || 'https://10-79-97-1751705-xxxxxxxxx.ztna-dingtalk.com/callback' } }, @@ -108,7 +114,8 @@ const portConfigs: Record> = { collaboraUrl: 'http://10.79.97.17:9980', appUrl: 'http://10.79.97.17:51706', oauth: { - redirectUri: 'http://10.79.97.17:51706/callback' + redirectUri: 'http://10.79.97.17:51706/callback', + dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51706 || 'https://10-79-97-1751706-xxxxxxxxx.ztna-dingtalk.com/callback' } }, @@ -129,7 +136,8 @@ const portConfigs: Record> = { appUrl: 'http://10.79.97.17:51707', oauth: { - redirectUri: 'http://10.79.97.17:51707/callback' + redirectUri: 'http://10.79.97.17:51707/callback', + dingtalkRedirectUri: process.env.DINGTALK_REDIRECT_URI_51707 || 'https://10-79-97-1751707-xxxxxxxxx.ztna-dingtalk.com/callback' } }, @@ -202,9 +210,9 @@ const configs: Record = { collaboraUrl: 'http://10.79.97.17:9980', appUrl: 'http://10.79.97.17:51703', oauth: { - clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', - serverUrl: 'http://10.79.112.85', // IDaaS服务器地址(测试) - // serverUrl: 'http://10.79.97.252', // IDaaS服务器地址(生产) + clientId: '224266374b56ee6254ed3d339014b033kaZy92exUmy', + // serverUrl: 'http://10.79.112.85', // IDaaS服务器地址(测试) + serverUrl: 'http://10.79.97.252', // IDaaS服务器地址(生产) // ⚠️ 安全警告:clientSecret 不应该硬编码在代码中 // 请在生产环境使用环境变量 OAUTH_CLIENT_SECRET clientSecret: 'placeholder', // 占位符,实际值从环境变量获取 diff --git a/app/config/oauth-secret.server.ts b/app/config/oauth-secret.server.ts index 03a9aa3..688795b 100644 --- a/app/config/oauth-secret.server.ts +++ b/app/config/oauth-secret.server.ts @@ -5,7 +5,7 @@ * Remix 会自动排除 .server.ts 文件不打包到客户端 */ -import { OAUTH_CONFIG } from './api-config'; +import { OAUTH_CONFIG, portConfigs } from './api-config'; // 用于控制日志输出(避免重复日志) let hasLoggedSecret = false; @@ -67,3 +67,34 @@ export function getServerOAuthConfigRuntime() { }; } +/** + * 获取端口特定的OAuth配置(包含钉钉回调地址) + * @param port 端口号 + * @returns OAuth配置(包含内网和钉钉回调地址) + */ +export function getPortOAuthConfig(port: string) { + const secret = getOAuthClientSecret(); + const portConfig = portConfigs[port]; + + if (!portConfig?.oauth) { + console.warn(`⚠️ [oauth-secret.server] 端口 ${port} 没有特定OAuth配置,使用默认配置`); + return { + serverUrl: OAUTH_CONFIG.serverUrl!, + clientId: OAUTH_CONFIG.clientId!, + redirectUri: OAUTH_CONFIG.redirectUri!, + appId: OAUTH_CONFIG.appId!, + clientSecret: secret, + dingtalkRedirectUri: undefined + }; + } + + return { + serverUrl: OAUTH_CONFIG.serverUrl!, + clientId: OAUTH_CONFIG.clientId!, + redirectUri: portConfig.oauth.redirectUri || OAUTH_CONFIG.redirectUri!, + appId: OAUTH_CONFIG.appId!, + clientSecret: secret, + dingtalkRedirectUri: portConfig.oauth.dingtalkRedirectUri + }; +} + diff --git a/app/routes/callback.tsx b/app/routes/callback.tsx index 5393650..96e2135 100644 --- a/app/routes/callback.tsx +++ b/app/routes/callback.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { useSearchParams } from "@remix-run/react"; import { createUserSession, sessionStorage } from "~/api/login/auth.server"; import { OAuthClient } from "~/api/login/oauth-client"; -import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server"; +import { getServerOAuthConfigRuntime, getPortOAuthConfig } from "~/config/oauth-secret.server"; import { loginWithOAuth, type LoginRequest } from "~/api/login/login-client"; import { isMobileDevice, MOBILE_CHAT_PATH } from "~/utils/mobile-detect.server"; @@ -106,9 +106,36 @@ export async function loader({ request }: LoaderFunctionArgs) { console.log("✅ OAuth2.0回调参数验证通过"); + // 🔑 判断是否从钉钉登录:检查 remote-user header + const remoteUser = request.headers.get("remote-user"); + const isDingTalkLogin = remoteUser !== null; + + console.log("🔧 [Callback] remote-user 检测:", { + remoteUser: remoteUser, + isDingTalkLogin: isDingTalkLogin, + loginType: isDingTalkLogin ? '钉钉Web登录' : '内网Web登录' + }); + // 声明在 try 外部,以便在 catch 中访问 let tokenResponse = null; - const oauthClient = new OAuthClient(getServerOAuthConfigRuntime()); + + // 获取端口特定的OAuth配置(包含钉钉回调地址) + const portOAuthConfig = getPortOAuthConfig(port); + const oauthClient = new OAuthClient(portOAuthConfig); + + // 🔑 根据登录类型设置回调地址 + if (isDingTalkLogin) { + // 钉钉登录:使用钉钉回调地址 + if (portOAuthConfig.dingtalkRedirectUri) { + oauthClient.setRedirectUri(portOAuthConfig.dingtalkRedirectUri); + console.log("🔧 [Callback] 使用钉钉回调地址:", portOAuthConfig.dingtalkRedirectUri); + } else { + console.warn("⚠️ [Callback] 钉钉登录但未配置钉钉回调地址,使用默认内网地址"); + } + } else { + // 内网登录:使用默认内网回调地址 + console.log("🔧 [Callback] 使用内网回调地址:", portOAuthConfig.redirectUri); + } try { console.log("🔧 开始处理OAuth2.0回调"); diff --git a/app/routes/home.tsx b/app/routes/home.tsx index dc8366d..d6d7531 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -411,7 +411,7 @@ export default function Home() { 排名 评查点名称 - 出错人数 + 出错次数 @@ -430,8 +430,9 @@ export default function Home() { {item.point_name} - - {item.error_user_count} 人 + {/* */} + {/* */} +

{item.error_user_count}

diff --git a/app/styles/components/switch.css b/app/styles/components/switch.css new file mode 100644 index 0000000..afca498 --- /dev/null +++ b/app/styles/components/switch.css @@ -0,0 +1,111 @@ +/** + * Switch 开关组件样式 + */ + +/* Switch 基础样式 */ +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + background-color: #e9ecef; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.3s ease, border-color 0.3s ease; + border: 1px solid #dee2e6; + flex-shrink: 0; + box-sizing: border-box; + padding: 0; +} + +/* 移除 button 默认样式 */ +.switch::-webkit-inner-spin-button, +.switch::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.switch { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +/* Switch 选中状态 - 绿色 */ +.switch-checked { + background-color: #00684a; + border-color: #00684a; +} + +.switch-unchecked { + background-color: #e9ecef; + border-color: #dee2e6; +} + +/* Switch 滑块 */ +.switch-handle { + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.switch-handle-checked { + transform: translateX(20px); +} + +.switch-handle-unchecked { + transform: translateX(0); +} + +/* Switch 禁用状态 */ +.switch-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.switch-disabled.switch-checked { + background-color: rgba(0, 104, 74, 0.5); + border-color: rgba(0, 104, 74, 0.5); +} + +/* Switch 加载状态 */ +.switch-loading { + cursor: wait; +} + +/* 加载动画图标 */ +.switch-loading-icon { + display: block; + width: 10px; + height: 10px; + border: 2px solid #00684a; + border-top-color: transparent; + border-radius: 50%; + animation: switch-spin 0.8s linear infinite; +} + +@keyframes switch-spin { + to { + transform: rotate(360deg); + } +} + +/* Hover 效果(仅非禁用状态) */ +.switch:not(.switch-disabled):hover { + box-shadow: 0 0 0 4px rgba(0, 104, 74, 0.1); +} + +/* Focus 样式 */ +.switch:focus-visible { + outline: 2px solid #00684a; + outline-offset: 2px; +} diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index ccaad9b..04b6938 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -26,7 +26,8 @@ module.exports = { NEXT_PUBLIC_PORT: '51703', NEXT_PUBLIC_CLIENT_ID: 'meizhou', NEXT_PUBLIC_API_PORT_CONFIG: '51703', - OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb' + OAUTH_CLIENT_SECRET: '8LRtfPB6dI6PoRJ53b9pJSagB6UOns2Ss3biO8mTCZ', + DINGTALK_REDIRECT_URI_51703: 'https://10-79-97-1751703-b3qrpmf0jy80t3.ztna-dingtalk.com/callback' }, error_file: './logs/meizhou-err.log', out_file: './logs/meizhou-out.log',