Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- 微信真机调试开关(M5):`frontend/.env` 设 `VITE_WX_DEBUG=true` 时 `wx.config({debug:true})`,在微信真机上以 alert 弹窗输出签名校验细节,便于定位 `invalid signature`(默认关闭,上线留空)
- 微信 JS-SDK 扫码签到(M5)后端 `backend/src/wechat/`:`GET /api/wechat/jssdk-signature?url=` 返回 `{appId,timestamp,nonceStr,signature}`;`WechatClient` 内存缓存 `access_token` / `jsapi_ticket`(默认 7200s,提前 300s 刷新;`tokio::sync::RwLock` 保证不跨 `await` 持锁),签名串 `jsapi_ticket=..&noncestr=..&timestamp=..&url=..` 取 **plain SHA1**(新增 `sha1` 依赖),`url` 自动去除 `#` 及其后部分;公众号凭证经 `WECHAT_APP_ID` / `WECHAT_APP_SECRET` 注入,未配置时返回 40307(不 panic)
- 微信 JS-SDK 扫码签到(M5)前端:`frontend/src/lib/wechat.ts`(`MicroMessenger` UA 检测、jweixin 1.6.0 动态注入且幂等复用、`wx.config` → `wx.scanQRCode` 封装,用户取消返回 `null`)与 `frontend/src/api/wechat.ts`(签名获取)
- 可选 ICP 备案号页脚(`frontend/src/components/IcpFooter.tsx`):仅当部署方配置 `VITE_ICP_BEIAN` 时渲染(链接工信部 `beian.miit.gov.cn`),未配置时零 DOM 痕迹;页脚挂在登录页,号码只写在部署机 `frontend/.env.local`(已 gitignore),仓库与开源发布不含任何具体号码,保证可随时 `git pull` 更新
- 查看当前课程(F5,M4)后端 `backend/src/courses/`:`GET /api/courses` 实时透传雨课堂「正在上课」课程(`/api/v3/classroom/on-lesson` ∩ `/v/course_meta/learning_list/`,对齐 `course_helper` 的 `getCoursesList()`),本站不落库;上游会话失效(50000)映射为 40101 统一登录引导
- 查看当前课程前端页面 `frontend/src/pages/Courses.tsx`:展示课程名 / 教师 / 课堂名(班级)/ 课程头像(无头像时以课程名首字兜底),支持手动刷新、加载/错误提示与无课程空态;导航栏新增「当前课程」入口(`/courses`)
- 分享房间模块(F3,M3)后端 `backend/src/ws/`:`Hub` 内存态房间管理(创建/加入/密码校验+限速/成员列表/历史消息 FIFO≤100 条+有效期淘汰/自定义生命周期);`GET /ws` 握手校验本站会话、单用户单连接(旧连接 close 4009);`POST /api/rooms`、`DELETE /api/rooms/{id}`、`GET /api/plaza`;后台 sweep 任务(心跳假死判死、lobby 空闲 10 分钟回收 close 4000、消息频率超限 close 4008、房间到期/14 天无消息回收)
Expand Down
4 changes: 4 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ VITE_CAPTCHA_APP_ID=2091064951
# 微信 JS-SDK 调试开关:true 时 wx.config 以 alert 弹窗输出签名校验细节
# 仅用于真机排查 invalid signature,上线务必留空
VITE_WX_DEBUG=

# ICP 备案号(可选):填写后登录页底部展示备案号并链接工信部,留空则完全不渲染
# 备案号仅属于部署方,建议只写在生产机 frontend/.env.local(已 gitignore),不要提交到仓库
VITE_ICP_BEIAN=
31 changes: 31 additions & 0 deletions frontend/src/components/IcpFooter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'

afterEach(() => {
cleanup()
vi.unstubAllEnvs()
})

/** 配置项在模块加载时读取,故需重置模块后动态导入以覆盖不同环境值 */
async function renderFooter(icpBeian: string) {
vi.resetModules()
vi.stubEnv('VITE_ICP_BEIAN', icpBeian)
const { default: IcpFooter } = await import('./IcpFooter')
return render(<IcpFooter />)
}

describe('IcpFooter ICP 备案页脚', () => {
it('未配置备案号时零 DOM 痕迹', async () => {
const { container } = await renderFooter('')
expect(container.innerHTML).toBe('')
})

it('配置备案号后渲染号码并链接工信部', async () => {
const { container } = await renderFooter('京ICP备00000000号-1')
const link = container.querySelector('a')
expect(link?.textContent).toBe('京ICP备00000000号-1')
expect(link?.getAttribute('href')).toBe('https://beian.miit.gov.cn/')
expect(link?.getAttribute('target')).toBe('_blank')
expect(link?.getAttribute('rel')).toBe('noreferrer')
})
})
21 changes: 21 additions & 0 deletions frontend/src/components/IcpFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// ICP 备案号页脚:仅当部署方配置了 VITE_ICP_BEIAN 时才渲染,未配置时零 DOM 痕迹
// 备案号属于部署方信息,通过生产机 frontend/.env.local 注入(已 gitignore),
// 仓库不包含任何具体号码,保证开源项目可随时 git pull 更新

import { config } from '../config'

export default function IcpFooter() {
if (!config.icpBeian) return null
return (
<footer className="mt-8 text-center text-xs text-muted">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noreferrer"
className="transition-colors hover:text-ink"
>
{config.icpBeian}
</a>
</footer>
)
}
2 changes: 2 additions & 0 deletions frontend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export const config = {
apiBaseUrl: import.meta.env.VITE_API_BASE_URL ?? '',
/** 腾讯验证码 AppId 兜底值(优先使用后端 /api/health 下发的值) */
captchaAppIdFallback: import.meta.env.VITE_CAPTCHA_APP_ID ?? '2091064951',
/** ICP 备案号:仅部署方在生产机 frontend/.env.local 配置(已 gitignore);留空则页脚不渲染 */
icpBeian: import.meta.env.VITE_ICP_BEIAN ?? '',
} as const
3 changes: 3 additions & 0 deletions frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { ApiError, api } from '../api/client'
import { showCaptcha } from '../captcha'
import IcpFooter from '../components/IcpFooter'
import { useAuth } from '../stores/auth'

type Tab = 'password' | 'sms' | 'qrcode'
Expand Down Expand Up @@ -222,6 +223,8 @@ export default function Login() {
<p className="text-sm text-muted">请用微信扫码,二维码过期会自动刷新</p>
</div>
)}

<IcpFooter />
</main>
)
}
Loading