mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: add cache
This commit is contained in:
parent
f6a66f4eba
commit
478312975f
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,4 +3,6 @@ dist/
|
||||
.vercel
|
||||
.output
|
||||
.vinxi
|
||||
.cache
|
||||
.cache
|
||||
.data
|
||||
.wrangler
|
10
package.json
10
package.json
@ -22,17 +22,14 @@
|
||||
"@tanstack/react-router": "^1.58.9",
|
||||
"@tanstack/router-devtools": "^1.58.9",
|
||||
"@unocss/reset": "^0.62.4",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"favicons-scraper": "^1.3.2",
|
||||
"flat-cache": "^6.1.0",
|
||||
"h3": "^1.12.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jotai": "^2.10.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ofetch": "^1.4.0",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"react": "^18.3.1",
|
||||
@ -52,7 +49,9 @@
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc-778e1ed2-20240926",
|
||||
"ofetch": "^1.4.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"nitro-cloudflare-dev": "^0.1.6",
|
||||
"nitropack": "^2.9.7",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2",
|
||||
@ -61,6 +60,7 @@
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-with-nitro": "0.0.0-beta.4",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.1.1"
|
||||
"vitest": "^2.1.1",
|
||||
"wrangler": "^3.79.0"
|
||||
}
|
||||
}
|
||||
|
893
pnpm-lock.yaml
generated
893
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
schema.sql
Normal file
7
schema.sql
Normal file
@ -0,0 +1,7 @@
|
||||
DROP TABLE IF EXISTS cache;
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
updated INTEGER,
|
||||
expires INTEGER
|
||||
);
|
@ -1,9 +1,38 @@
|
||||
import { FlatCache } from "flat-cache"
|
||||
import { TTL } from "@shared/consts"
|
||||
import type { CacheInfo } from "@shared/types"
|
||||
|
||||
// init
|
||||
export const cache = new FlatCache({
|
||||
ttl: 60 * 60 * 1000, // 1 hour
|
||||
lruSize: 10000, // 10,000 items
|
||||
expirationInterval: 5 * 1000 * 60, // 5 minutes
|
||||
persistInterval: 5 * 1000 * 60, // 5 minutes
|
||||
})
|
||||
export class Cache {
|
||||
private db
|
||||
constructor(db: any) {
|
||||
this.db = db
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
updated INTEGER,
|
||||
expires INTEGER
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
async set(key: string, value: any) {
|
||||
const now = Date.now()
|
||||
return await this.db.prepare(
|
||||
`INSERT OR REPLACE INTO cache (id, data, updated, expires) VALUES (?, ?, ?, ?)`,
|
||||
).run(key, JSON.stringify(value), now, now + TTL)
|
||||
}
|
||||
|
||||
async get(key: string): Promise<CacheInfo> {
|
||||
const row = await this.db.prepare(`SELECT id, data, updated, expires FROM cache WHERE id = ?`).get(key)
|
||||
return row
|
||||
? {
|
||||
...row,
|
||||
data: JSON.parse(row.data),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
return await this.db.prepare(`DELETE FROM cache WHERE id = ?`).run(key)
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,52 @@
|
||||
import { defineEventHandler, getRouterParam } from "h3"
|
||||
import { fallback, sources } from "#/sources"
|
||||
// import { cache } from "#/cache"
|
||||
import { Cache } from "#/cache"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id") as keyof typeof sources
|
||||
// const { latest } = getQuery(event)
|
||||
// console.log(id, latest)
|
||||
if (!id) throw new Error("Invalid source id")
|
||||
// if (!latest) {
|
||||
// const _ = cache.get(id)
|
||||
// if (_) return _
|
||||
// }
|
||||
try {
|
||||
const id = getRouterParam(event, "id") as keyof typeof sources
|
||||
const query = getQuery(event)
|
||||
const latest = query.latest !== undefined && query.latest !== "false"
|
||||
|
||||
if (!sources[id]) {
|
||||
const _ = await fallback(id)
|
||||
// cache.set(id, _)
|
||||
return _
|
||||
} else {
|
||||
const _ = await sources[id]()
|
||||
// cache.set(id, _)
|
||||
return _
|
||||
if (!id) throw new Error("Invalid source id")
|
||||
const db = useDatabase()
|
||||
const cacheStore = db ? new Cache(db) : undefined
|
||||
if (cacheStore) {
|
||||
const cache = await cacheStore.get(id)
|
||||
if (cache) {
|
||||
if (!latest && cache.expires > Date.now()) {
|
||||
return {
|
||||
status: "cache",
|
||||
data: cache.data,
|
||||
}
|
||||
} else if (latest && Date.now() - cache.updated < 60 * 1000) {
|
||||
return {
|
||||
status: "success",
|
||||
data: cache.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sources[id]) {
|
||||
const data = await fallback(id)
|
||||
if (cacheStore) cacheStore.set(id, data)
|
||||
return {
|
||||
status: "success",
|
||||
data,
|
||||
}
|
||||
} else {
|
||||
const data = await sources[id]()
|
||||
if (cacheStore) cacheStore.set(id, data)
|
||||
return {
|
||||
status: "success",
|
||||
data,
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
return {
|
||||
status: "error",
|
||||
message: e.message ?? e,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { OResponse } from "@shared/types"
|
||||
import { $fetch } from "ofetch"
|
||||
import type { SourceInfo } from "@shared/types"
|
||||
|
||||
export interface Res {
|
||||
code: number
|
||||
@ -18,24 +17,22 @@ export interface Res {
|
||||
}[]
|
||||
}
|
||||
|
||||
export async function fallback(id: string): Promise<OResponse> {
|
||||
const res: Res = await $fetch(`https://smzdk.top/api/${id}/new`)
|
||||
export async function fallback(id: string): Promise<SourceInfo> {
|
||||
const url = `https://smzdk.top/api/${id}/new`
|
||||
const res: Res = await $fetch(url)
|
||||
if (res.code !== 200 || !res.data) throw new Error(res.message)
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
name: res.title,
|
||||
type: res.subtitle,
|
||||
updateTime: res.updateTime,
|
||||
items: res.data.map(item => ({
|
||||
extra: {
|
||||
date: item.time,
|
||||
},
|
||||
id: item.url,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
mobileUrl: item.mobileUrl,
|
||||
})),
|
||||
},
|
||||
name: res.title,
|
||||
type: res.subtitle,
|
||||
updateTime: res.updateTime,
|
||||
items: res.data.map(item => ({
|
||||
extra: {
|
||||
date: item.time,
|
||||
},
|
||||
id: item.url,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
mobileUrl: item.mobileUrl,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,17 @@
|
||||
import type { OResponse, RSS2JSON } from "@shared/types"
|
||||
import type { RSS2JSON, SourceInfo } from "@shared/types"
|
||||
import { rss2json } from "#/utils/rss2json"
|
||||
|
||||
export async function peopledaily(): Promise<OResponse> {
|
||||
export async function peopledaily(): Promise<SourceInfo> {
|
||||
const source = await rss2json("https://feedx.net/rss/people.xml")
|
||||
if (!source?.items.length) throw new Error("Cannot fetch data")
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
name: "人民日报",
|
||||
type: "报纸",
|
||||
updateTime: Date.now(),
|
||||
items: source.items.slice(0, 30).map((item: RSS2JSON) => ({
|
||||
title: item.title,
|
||||
url: item.link,
|
||||
id: item.link,
|
||||
})),
|
||||
},
|
||||
name: "人民日报",
|
||||
type: "报纸",
|
||||
updateTime: Date.now(),
|
||||
items: source.items.slice(0, 30).map((item: RSS2JSON) => ({
|
||||
title: item.title,
|
||||
url: item.link,
|
||||
id: item.link,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { OResponse } from "@shared/types"
|
||||
import { $fetch } from "ofetch"
|
||||
import type { SourceInfo } from "@shared/types"
|
||||
|
||||
interface Res {
|
||||
ok: number // 1 is ok
|
||||
@ -27,28 +26,25 @@ interface Res {
|
||||
}
|
||||
}
|
||||
|
||||
export async function weibo(): Promise<OResponse> {
|
||||
export async function weibo(): Promise<SourceInfo> {
|
||||
const url = "https://weibo.com/ajax/side/hotSearch"
|
||||
const res: Res = await $fetch(url)
|
||||
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
name: "微博热搜",
|
||||
updateTime: Date.now(),
|
||||
type: "热搜",
|
||||
items: res.data.realtime.filter(k => !k.icon_desc || k.icon_desc !== "荐").map((k) => {
|
||||
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
||||
return {
|
||||
id: k.num,
|
||||
title: k.word,
|
||||
extra: {
|
||||
icon: k.icon,
|
||||
},
|
||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
|
||||
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
|
||||
}
|
||||
}),
|
||||
},
|
||||
name: "微博热搜",
|
||||
updateTime: Date.now(),
|
||||
type: "热搜",
|
||||
items: res.data.realtime.filter(k => !k.icon_desc || k.icon_desc !== "荐").map((k) => {
|
||||
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
||||
return {
|
||||
id: k.num,
|
||||
title: k.word,
|
||||
extra: {
|
||||
icon: k.icon,
|
||||
},
|
||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
|
||||
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import { $fetch } from "ofetch"
|
||||
import * as cheerio from "cheerio"
|
||||
import iconv from "iconv-lite"
|
||||
import type { NewsItem, OResponse } from "@shared/types"
|
||||
import type { NewsItem, OResponse, SourceInfo } from "@shared/types"
|
||||
import { tranformToUTC } from "#/utils/date"
|
||||
|
||||
const columns = [
|
||||
@ -15,15 +14,16 @@ const columns = [
|
||||
"国际军事",
|
||||
"国际视野",
|
||||
] as const
|
||||
// type: "中国聚焦" | "人物记事" | "观点评论"
|
||||
export async function zaobao(type: typeof columns[number] = "中国聚焦"): Promise<OResponse> {
|
||||
export async function zaobao(type: typeof columns[number] = "中国聚焦"): Promise<SourceInfo> {
|
||||
const response = await $fetch("https://www.kzaobao.com/top.html", {
|
||||
responseType: "arrayBuffer",
|
||||
})
|
||||
const base = "https://www.kzaobao.com"
|
||||
const utf8String = iconv.decode(Buffer.from(response), "gb2312")
|
||||
const $ = cheerio.load(utf8String)
|
||||
const $main = $(`#cd0${columns.indexOf(type) + 1}`)
|
||||
// const all = []
|
||||
// columns.forEach((column, index) => {
|
||||
const $main = $(`#cd0${2}`)
|
||||
const news: NewsItem[] = []
|
||||
$main.find("tr").each((_, el) => {
|
||||
const a = $(el).find("h3>a")
|
||||
@ -42,13 +42,17 @@ export async function zaobao(type: typeof columns[number] = "中国聚焦"): Pro
|
||||
})
|
||||
}
|
||||
})
|
||||
// all.push({
|
||||
// type: column,
|
||||
// items: news,
|
||||
// })
|
||||
// })
|
||||
// console.log(all)
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
name: `联合早报`,
|
||||
type,
|
||||
updateTime: Date.now(),
|
||||
items: news,
|
||||
},
|
||||
name: `联合早报`,
|
||||
type,
|
||||
updateTime: Date.now(),
|
||||
// items: all[0].items,
|
||||
items: news,
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
import type { NewsItem, SourceInfo } from "@shared/types"
|
||||
|
||||
// 榜单数据
|
||||
export interface ListItem extends NewsItem { }
|
||||
|
||||
// 路由数据
|
||||
export interface RouterData extends SourceInfo { }
|
||||
|
||||
// 请求类型
|
||||
export interface Get {
|
||||
url: string
|
||||
headers?: Record<string, string | string[]>
|
||||
params?: Record<string, string | number>
|
||||
timeout?: number
|
||||
noCache?: boolean
|
||||
ttl?: number
|
||||
originaInfo?: boolean
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
url: string
|
||||
headers?: Record<string, string | string[]>
|
||||
body?: string | object | import("node:buffer").Buffer | undefined
|
||||
timeout?: number
|
||||
noCache?: boolean
|
||||
ttl?: number
|
||||
originaInfo?: boolean
|
||||
}
|
||||
|
||||
export interface Web {
|
||||
url: string
|
||||
timeout?: number
|
||||
noCache?: boolean
|
||||
ttl?: number
|
||||
userAgent?: string
|
||||
}
|
||||
|
||||
// 参数类型
|
||||
export interface Options {
|
||||
[key: string]: string | number | undefined
|
||||
}
|
1
shared/consts.ts
Normal file
1
shared/consts.ts
Normal file
@ -0,0 +1 @@
|
||||
export const TTL = 15 * 60 * 1000
|
@ -41,3 +41,10 @@ export interface RSS2JSON {
|
||||
published: number
|
||||
created: number
|
||||
}
|
||||
|
||||
export interface CacheInfo {
|
||||
id: SourceID
|
||||
data: SourceInfo
|
||||
updated: number
|
||||
expires: number
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"@shared/*": ["shared/*"]
|
||||
}
|
||||
},
|
||||
"include": ["server", "*.config.*", "shared", "test", "scripts"]
|
||||
"include": ["server", "*.config.*", "shared", "test", "scripts", "dist/.nitro/types"]
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import process from "node:process"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import nitroCloudflareBindings from "nitro-cloudflare-dev"
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import nitro from "vite-plugin-with-nitro"
|
||||
@ -20,14 +21,28 @@ export default defineConfig({
|
||||
react(),
|
||||
nitro({ ssr: false }, {
|
||||
srcDir: "server",
|
||||
modules: [nitroCloudflareBindings],
|
||||
experimental: {
|
||||
database: true,
|
||||
},
|
||||
database: {
|
||||
default: {
|
||||
connector: "cloudflare-d1",
|
||||
options: {
|
||||
bindingName: "CACHE_DB",
|
||||
},
|
||||
},
|
||||
},
|
||||
devDatabase: {
|
||||
default: {
|
||||
connector: "sqlite",
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
"@shared": fileURLToPath(new URL("shared", import.meta.url)),
|
||||
"#": fileURLToPath(new URL("server", import.meta.url)),
|
||||
},
|
||||
runtimeConfig: {
|
||||
// apiPrefix: "",
|
||||
},
|
||||
preset: process.env.VERCEL ? "vercel-edge" : "node-server",
|
||||
preset: "cloudflare-pages",
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
4
wrangler.toml
Normal file
4
wrangler.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[[d1_databases]]
|
||||
binding = "CACHE_DB" # i.e. available in your Worker on env.CACHE_DB
|
||||
database_name = "newsnow-cache"
|
||||
database_id = "1e091d20-2f57-48b4-b735-690ec65c725e"
|
Loading…
x
Reference in New Issue
Block a user