883540bd7e
上一版做成了网页授权(OAuth 允许页),方向错了。改成用户要的流程: 扫码 → 弹公众号关注页 → 关注即登录,服务号顺带涨粉。 流程:PC 建票 → 后端用 access_token 调「带参数二维码」接口(scene=ticket) → 展示微信二维码图 → 用户扫码关注 → 微信推 subscribe/SCAN 事件到 /wx/mp/callback → 按 openid 找/建用户 → ticket 置 authorized → PC 轮询拿 JWT。 明文模式(消息加解密):回调只验签名 sha1(sort(token,ts,nonce)),不做 AES。 关键实现点: - access_token 缓存进 Redis(跨实例共享,避免重复拉取互相失效)+ 进程内锁双检; - 事件同时处理 subscribe(未关注,EventKey 带 qrscene_ 前缀)与 SCAN(已关注,不带); - 事件回调必须验签——否则任何人 POST 一个 openid 就能登录别人; - 回调无论如何回 "success",否则微信重试并给用户弹"公众号故障"; - User.wechat_openid 用部分唯一索引(WHERE <> ''),避开存量空串互撞。 配置(appid/secret/token)后台可改、secret AES 加密入库。管理端「运维 → 登录设置」 列出还需在公众平台做的事(服务器 URL / Token 一致 / 明文模式 / IP 白名单)。 本地验证(真流程,非 mock):验签回 echostr 与微信算法一致;模拟 subscribe 事件 → 建号 + 置票 → PC 轮询拿到 token+user → 库里确有该 openid 用户。真微信推真事件 留待部署后扫码。前端 web 登录页加「微信扫码/邮箱」双 tab,默认微信。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
import { SubscriptionPage } from "./pages/SubscriptionPage";
|
|
import { LoginConfigPage } from "./pages/LoginConfigPage";
|
|
import { lazy, type ReactNode } from "react";
|
|
|
|
// 路由注册表 —— 控制台的单一事实源:导航 + 内容都从这里派生。
|
|
// 新增页面 = 在此加一条;real 页面用 lazy 懒加载(代码分割)。
|
|
|
|
|
|
const DashboardPage = lazy(() => import("./pages/DashboardPage").then((m) => ({ default: m.DashboardPage })));
|
|
const ModelConfigPage = lazy(() => import("./pages/ModelConfigPage").then((m) => ({ default: m.ModelConfigPage })));
|
|
const DatasourcesPage = lazy(() => import("./pages/DatasourcesPage").then((m) => ({ default: m.DatasourcesPage })));
|
|
const StatusPage = lazy(() => import("./pages/StatusPage").then((m) => ({ default: m.StatusPage })));
|
|
const TasksPage = lazy(() => import("./pages/TasksPage").then((m) => ({ default: m.TasksPage })));
|
|
const EvalsPage = lazy(() => import("./pages/EvalsPage").then((m) => ({ default: m.EvalsPage })));
|
|
const TenantsPage = lazy(() => import("./pages/TenantsPage").then((m) => ({ default: m.TenantsPage })));
|
|
const GuardrailsPage = lazy(() => import("./pages/GuardrailsPage").then((m) => ({ default: m.GuardrailsPage })));
|
|
const PromptsPage = lazy(() => import("./pages/PromptsPage").then((m) => ({ default: m.PromptsPage })));
|
|
const AuditPage = lazy(() => import("./pages/AuditPage").then((m) => ({ default: m.AuditPage })));
|
|
const PaymentConfigPage = lazy(() => import("./pages/PaymentConfigPage").then((m) => ({ default: m.PaymentConfigPage })));
|
|
const PaymentOrdersPage = lazy(() => import("./pages/PaymentOrdersPage").then((m) => ({ default: m.PaymentOrdersPage })));
|
|
const SpacesPage = lazy(() => import("./pages/SpacesPage").then((m) => ({ default: m.SpacesPage })));
|
|
|
|
export interface RouteDef {
|
|
path: string;
|
|
label: string;
|
|
group: string;
|
|
/** 二级菜单名。同 group 内 parent 相同的若干条会聚成一个可展开子菜单(如 运维 > 支付 > 配置/订单与对账)。 */
|
|
parent?: string;
|
|
ready?: boolean;
|
|
element: ReactNode;
|
|
}
|
|
|
|
/** 侧栏一个节点:要么是单条路由,要么是带子项的可展开二级菜单。 */
|
|
export type NavNode =
|
|
| { kind: "item"; route: RouteDef }
|
|
| { kind: "parent"; label: string; items: RouteDef[] };
|
|
|
|
export const routes: RouteDef[] = [
|
|
{
|
|
path: "dashboard",
|
|
label: "仪表盘",
|
|
group: "分析",
|
|
ready: true,
|
|
element: <DashboardPage />,
|
|
},
|
|
{
|
|
path: "status",
|
|
label: "服务状态",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <StatusPage />,
|
|
},
|
|
{
|
|
path: "tasks",
|
|
label: "任务观测",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <TasksPage />,
|
|
},
|
|
{
|
|
path: "evals",
|
|
label: "自动评测",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <EvalsPage />,
|
|
},
|
|
{
|
|
path: "audit",
|
|
label: "审计 & 安全",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <AuditPage />,
|
|
},
|
|
{
|
|
path: "models",
|
|
label: "模型配置",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <ModelConfigPage />,
|
|
},
|
|
{
|
|
path: "login-config",
|
|
label: "登录设置",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <LoginConfigPage />,
|
|
},
|
|
{
|
|
path: "datasources",
|
|
label: "数据源 & RAG",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <DatasourcesPage />,
|
|
},
|
|
{
|
|
path: "prompts",
|
|
label: "提示词",
|
|
group: "运维",
|
|
ready: true,
|
|
element: <PromptsPage />,
|
|
},
|
|
{
|
|
path: "payment/config",
|
|
label: "配置",
|
|
group: "运维",
|
|
parent: "支付",
|
|
ready: true,
|
|
element: <PaymentConfigPage />,
|
|
},
|
|
{
|
|
path: "payment/subscription",
|
|
label: "订阅",
|
|
group: "运维",
|
|
parent: "支付",
|
|
ready: true,
|
|
element: <SubscriptionPage />,
|
|
},
|
|
{
|
|
path: "payment/orders",
|
|
label: "订单与对账",
|
|
group: "运维",
|
|
parent: "支付",
|
|
ready: true,
|
|
element: <PaymentOrdersPage />,
|
|
},
|
|
{
|
|
path: "tenants",
|
|
label: "租户 & 用户",
|
|
group: "平台",
|
|
ready: true,
|
|
element: <TenantsPage />,
|
|
},
|
|
{
|
|
path: "spaces",
|
|
label: "空间",
|
|
group: "平台",
|
|
ready: true,
|
|
element: <SpacesPage />,
|
|
},
|
|
{
|
|
path: "guardrails",
|
|
label: "安全护栏",
|
|
group: "平台",
|
|
ready: true,
|
|
element: <GuardrailsPage />,
|
|
},
|
|
];
|
|
|
|
export const defaultPath = "dashboard";
|
|
|
|
// 派生分组导航(保持注册顺序)。
|
|
export function navGroups(): Array<{ group: string; nodes: NavNode[] }> {
|
|
const out: Array<{ group: string; nodes: NavNode[] }> = [];
|
|
for (const r of routes) {
|
|
let g = out.find((x) => x.group === r.group);
|
|
if (!g) {
|
|
g = { group: r.group, nodes: [] };
|
|
out.push(g);
|
|
}
|
|
if (!r.parent) {
|
|
g.nodes.push({ kind: "item", route: r });
|
|
continue;
|
|
}
|
|
// 同 parent 的归到同一个可展开节点(首次出现决定它在组内的位置)。
|
|
const exist = g.nodes.find((n): n is Extract<NavNode, { kind: "parent" }> => n.kind === "parent" && n.label === r.parent);
|
|
if (exist) exist.items.push(r);
|
|
else g.nodes.push({ kind: "parent", label: r.parent, items: [r] });
|
|
}
|
|
return out;
|
|
}
|