diff --git a/drizzle/orm.ts b/drizzle/orm.ts index e464673e9..e2e9c2b78 100644 --- a/drizzle/orm.ts +++ b/drizzle/orm.ts @@ -30,6 +30,7 @@ export type IPersonCollect = typeof schema.chiiPersonCollects.$inferSelect; export type IIndex = typeof schema.chiiIndexes.$inferSelect; export type IIndexCollect = typeof schema.chiiIndexCollects.$inferSelect; +export type IIndexRelated = typeof schema.chiiIndexRelated.$inferSelect; export type IBlogEntry = typeof schema.chiiBlogEntries.$inferSelect; export type IBlogPhoto = typeof schema.chiiBlogPhotos.$inferSelect; diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 361e529bf..5db2df823 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -231,12 +231,13 @@ export const chiiGroupPosts = mysqlTable('chii_group_posts', { export const chiiIndexes = mysqlTable('chii_index', { id: mediumint('idx_id').autoincrement().notNull(), type: tinyint('idx_type').default(0).notNull(), - title: varchar('idx_title', { length: 80 }).notNull(), - desc: mediumtext('idx_desc').notNull(), + title: htmlEscapedString('varchar')('idx_title', { length: 80 }).notNull(), + desc: htmlEscapedString('mediumtext')('idx_desc').notNull(), replies: mediumint('idx_replies').notNull(), total: mediumint('idx_subject_total').notNull(), collects: mediumint('idx_collects').notNull(), stats: mediumtext('idx_stats').notNull(), + award: mediumint('idx_award').default(0).notNull(), createdAt: int('idx_dateline').notNull(), updatedAt: int('idx_lasttouch').notNull(), uid: mediumint('idx_uid').notNull(), @@ -260,15 +261,16 @@ export const chiiIndexComments = mysqlTable('chii_index_comments', { }); export const chiiIndexRelated = mysqlTable('chii_index_related', { - idxRltId: mediumint('idx_rlt_id').autoincrement().notNull(), - idxRltCat: tinyint('idx_rlt_cat').notNull(), - idxRltRid: mediumint('idx_rlt_rid').notNull(), - idxRltType: smallint('idx_rlt_type').notNull(), - idxRltSid: mediumint('idx_rlt_sid').notNull(), - idxRltOrder: mediumint('idx_rlt_order').notNull(), - idxRltComment: mediumtext('idx_rlt_comment').notNull(), - idxRltDateline: int('idx_rlt_dateline').notNull(), - idxRltBan: tinyint('idx_rlt_ban').default(0).notNull(), + id: mediumint('idx_rlt_id').autoincrement().notNull(), + cat: tinyint('idx_rlt_cat').notNull(), + rid: mediumint('idx_rlt_rid').notNull(), + type: smallint('idx_rlt_type').notNull(), + sid: mediumint('idx_rlt_sid').notNull(), + order: mediumint('idx_rlt_order').notNull(), + award: varchar('idx_rlt_award', { length: 255 }).notNull(), + comment: htmlEscapedString('mediumtext')('idx_rlt_comment').notNull(), + createdAt: int('idx_rlt_dateline').notNull(), + ban: tinyint('idx_rlt_ban').default(0).notNull(), }); export const chiiLikes = mysqlTable('chii_likes', { diff --git a/lib/index/cache.ts b/lib/index/cache.ts index e682eada2..ad4e373b6 100644 --- a/lib/index/cache.ts +++ b/lib/index/cache.ts @@ -1,3 +1,3 @@ export function getSlimCacheKey(id: number): string { - return `idx:v3:slim:${id}`; + return `idx:v4:slim:${id}`; } diff --git a/lib/index/types.ts b/lib/index/types.ts new file mode 100644 index 000000000..fc2fe12b5 --- /dev/null +++ b/lib/index/types.ts @@ -0,0 +1,12 @@ +export enum IndexType { + User = 0, + Public = 1, + Award = 2, +} + +export enum IndexRelatedCategory { + Subject = 0, + Character = 1, + Person = 2, + Ep = 3, +} diff --git a/lib/openapi/index.ts b/lib/openapi/index.ts index c24844e48..c5d743d01 100644 --- a/lib/openapi/index.ts +++ b/lib/openapi/index.ts @@ -9,6 +9,7 @@ export const Tag = { Episode: 'episode', Friend: 'friend', Group: 'group', + Index: 'index', Person: 'person', Search: 'search', Subject: 'subject', diff --git a/lib/types/common.ts b/lib/types/common.ts index ddf95f7fb..c541a1876 100644 --- a/lib/types/common.ts +++ b/lib/types/common.ts @@ -59,6 +59,21 @@ export const CollectionType = t.Integer({ - 5 = 抛弃`, }); +export const IndexRelatedCategory = t.Integer({ + $id: 'IndexRelatedCategory', + enum: [0, 1, 2, 3], + 'x-ms-enum': { + name: 'IndexRelatedCategory', + modelAsString: false, + }, + 'x-enum-varnames': ['Subject', 'Character', 'Person', 'Episode'], + description: `目录关联类型 + - 0 = 条目 + - 1 = 角色 + - 2 = 人物 + - 3 = 剧集`, +}); + export const EpisodeCollectionStatus = t.Integer({ $id: 'EpisodeCollectionStatus', enum: [0, 1, 2, 3], @@ -91,6 +106,20 @@ export const GroupMemberRole = t.Integer({ - 3 = 禁言成员`, }); +export const IndexType = t.Integer({ + $id: 'IndexType', + enum: [0, 1, 2], + 'x-ms-enum': { + name: 'IndexType', + modelAsString: false, + }, + 'x-enum-varnames': ['User', 'Public', 'Award'], + description: `目录类型 + - 0 = 用户 + - 1 = 公共 + - 2 = TBA`, +}); + export type IEpisodeWikiInfo = Static; export const EpisodeWikiInfo = t.Object( { diff --git a/lib/types/convert.ts b/lib/types/convert.ts index 15f36a5d8..2de16e8e2 100644 --- a/lib/types/convert.ts +++ b/lib/types/convert.ts @@ -517,6 +517,7 @@ export function toPerson(person: orm.IPerson): res.IPerson { export function toSlimIndex(index: orm.IIndex): res.ISlimIndex { return { id: index.id, + uid: index.uid, type: index.type, title: index.title, total: index.total, @@ -527,6 +528,7 @@ export function toSlimIndex(index: orm.IIndex): res.ISlimIndex { export function toIndex(index: orm.IIndex): res.IIndex { return { id: index.id, + uid: index.uid, type: index.type, title: index.title, desc: index.desc, @@ -534,11 +536,26 @@ export function toIndex(index: orm.IIndex): res.IIndex { total: index.total, collects: index.collects, stats: toIndexStats(index.stats), + award: index.award, createdAt: index.createdAt, updatedAt: index.updatedAt, }; } +export function toIndexRelated(related: orm.IIndexRelated): res.IIndexRelated { + return { + id: related.id, + cat: related.cat, + rid: related.rid, + type: related.type, + sid: related.sid, + order: related.order, + comment: related.comment, + award: related.award, + createdAt: related.createdAt, + }; +} + export function toCharacterSubjectRelation( subject: orm.ISubject, fields: orm.ISubjectFields, diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index 20cd022a8..d2a42c238 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -527,6 +527,37 @@ async function fetchEpisodeItemByID(episodeID: number): Promise> { + const cached = await redis.mget(episodeIDs.map((id) => getSubjectEpCacheKey(id))); + const result: Record = {}; + const missing = []; + for (const [idx, id] of episodeIDs.entries()) { + if (cached[idx]) { + const item = JSON.parse(cached[idx]) as res.IEpisode; + item.desc = undefined; + result[id] = item; + } else { + missing.push(id); + } + } + if (missing.length > 0) { + const data = await db + .select() + .from(schema.chiiEpisodes) + .where(op.inArray(schema.chiiEpisodes.id, missing)); + for (const d of data) { + const item = convert.toEpisode(d); + await redis.setex(getSubjectEpCacheKey(d.id), ONE_MONTH, JSON.stringify(item)); + item.desc = undefined; + result[d.id] = item; + } + } + return result; +} + /** Cached */ export async function fetchSlimCharacterByID( id: number, diff --git a/lib/types/res.ts b/lib/types/res.ts index 39641e06d..b7732d5f2 100644 --- a/lib/types/res.ts +++ b/lib/types/res.ts @@ -9,6 +9,8 @@ import { EpisodeCollectionStatus, EpisodeType, GroupMemberRole, + IndexRelatedCategory, + IndexType, Ref, SubjectType, } from '@app/lib/types/common.ts'; @@ -814,16 +816,19 @@ export type IIndex = Static; export const Index = t.Object( { id: t.Integer(), - type: t.Integer(), + uid: t.Integer(), + type: Ref(IndexType), title: t.String(), desc: t.String(), replies: t.Integer(), total: t.Integer(), collects: t.Integer(), stats: Ref(IndexStats), + award: t.Integer(), createdAt: t.Integer(), updatedAt: t.Integer(), collectedAt: t.Optional(t.Integer()), + user: t.Optional(Ref(SlimUser)), }, { $id: 'Index', title: 'Index' }, ); @@ -832,7 +837,8 @@ export type ISlimIndex = Static; export const SlimIndex = t.Object( { id: t.Integer(), - type: t.Integer(), + uid: t.Integer(), + type: Ref(IndexType), title: t.String(), total: t.Integer(), createdAt: t.Integer(), @@ -840,6 +846,26 @@ export const SlimIndex = t.Object( { $id: 'SlimIndex', title: 'SlimIndex' }, ); +export type IIndexRelated = Static; +export const IndexRelated = t.Object( + { + id: t.Integer(), + cat: Ref(IndexRelatedCategory), + rid: t.Integer(), + type: t.Integer(), + sid: t.Integer(), + order: t.Integer(), + comment: t.String(), + award: t.String(), + createdAt: t.Integer(), + subject: t.Optional(Ref(SlimSubject)), + character: t.Optional(Ref(SlimCharacter)), + person: t.Optional(Ref(SlimPerson)), + episode: t.Optional(Ref(Episode)), + }, + { $id: 'IndexRelated', title: 'IndexRelated' }, +); + export type IGroupMember = Static; export const GroupMember = t.Object( { diff --git a/lib/user/stats.ts b/lib/user/stats.ts index bc4d18159..ed3c66393 100644 --- a/lib/user/stats.ts +++ b/lib/user/stats.ts @@ -1,4 +1,5 @@ import { db, op, schema } from '@app/drizzle'; +import { IndexType } from '@app/lib/index/types.ts'; import redis from '@app/lib/redis'; import { CollectionPrivacy, CollectionType, SubjectType } from '@app/lib/subject/type.ts'; import type * as res from '@app/lib/types/res.ts'; @@ -62,7 +63,13 @@ export async function countUserIndex(uid: number): Promise const [{ create = 0 } = {}] = await db .select({ create: op.count() }) .from(schema.chiiIndexes) - .where(op.and(op.eq(schema.chiiIndexes.uid, uid), op.eq(schema.chiiIndexes.ban, 0))); + .where( + op.and( + op.eq(schema.chiiIndexes.uid, uid), + op.eq(schema.chiiIndexes.ban, 0), + op.eq(schema.chiiIndexes.type, IndexType.User), + ), + ); const [{ collect = 0 } = {}] = await db .select({ collect: op.count() }) .from(schema.chiiIndexCollects) diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index 14cd61079..c93625881 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -776,6 +776,8 @@ exports[`should build private api spec 1`] = ` type: object Index: properties: + award: + type: integer collectedAt: type: integer collects: @@ -795,11 +797,16 @@ exports[`should build private api spec 1`] = ` total: type: integer type: + $ref: '#/components/schemas/IndexType' + uid: type: integer updatedAt: type: integer + user: + $ref: '#/components/schemas/SlimUser' required: - id + - uid - type - title - desc @@ -807,15 +814,95 @@ exports[`should build private api spec 1`] = ` - total - collects - stats + - award - createdAt - updatedAt title: Index type: object + IndexRelated: + properties: + award: + type: string + cat: + $ref: '#/components/schemas/IndexRelatedCategory' + character: + $ref: '#/components/schemas/SlimCharacter' + comment: + type: string + createdAt: + type: integer + episode: + $ref: '#/components/schemas/Episode' + id: + type: integer + order: + type: integer + person: + $ref: '#/components/schemas/SlimPerson' + rid: + type: integer + sid: + type: integer + subject: + $ref: '#/components/schemas/SlimSubject' + type: + type: integer + required: + - id + - cat + - rid + - type + - sid + - order + - comment + - award + - createdAt + title: IndexRelated + type: object + IndexRelatedCategory: + description: |- + 目录关联类型 + - 0 = 条目 + - 1 = 角色 + - 2 = 人物 + - 3 = 剧集 + enum: + - 0 + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - Subject + - Character + - Person + - Episode + x-ms-enum: + modelAsString: false + name: IndexRelatedCategory IndexStats: additionalProperties: type: integer title: IndexStats type: object + IndexType: + description: |- + 目录类型 + - 0 = 用户 + - 1 = 公共 + - 2 = TBA + enum: + - 0 + - 1 + - 2 + type: integer + x-enum-varnames: + - User + - Public + - Award + x-ms-enum: + modelAsString: false + name: IndexType Infobox: items: properties: @@ -1417,9 +1504,12 @@ exports[`should build private api spec 1`] = ` total: type: integer type: + $ref: '#/components/schemas/IndexType' + uid: type: integer required: - id + - uid - type - title - total @@ -4835,6 +4925,102 @@ paths: summary: 创建小组话题 tags: - group + /p1/indexes/{indexID}: + get: + operationId: getIndex + parameters: + - in: path + name: indexID + required: true + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Index' + description: Default Response + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 意料之外的服务器错误 + description: 意料之外的服务器错误 + security: + - CookiesSession: [] + HTTPBearer: [] + summary: 获取目录详情 + tags: + - index + /p1/indexes/{indexID}/related: + get: + operationId: getIndexRelated + parameters: + - in: query + name: cat + required: false + schema: + $ref: '#/components/schemas/IndexRelatedCategory' + - in: query + name: type + required: false + schema: + $ref: '#/components/schemas/SubjectType' + - description: max 100 + in: query + name: limit + required: false + schema: + default: 20 + maximum: 100 + minimum: 1 + type: integer + - description: min 0 + in: query + name: offset + required: false + schema: + default: 0 + minimum: 0 + type: integer + - in: path + name: indexID + required: true + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + properties: + data: + items: + $ref: '#/components/schemas/IndexRelated' + type: array + total: + description: limit+offset 为参数的请求表示总条数,page 为参数的请求表示总页数 + type: integer + required: + - data + - total + type: object + description: Default Response + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 意料之外的服务器错误 + description: 意料之外的服务器错误 + security: + - CookiesSession: [] + HTTPBearer: [] + summary: 获取目录的关联内容 + tags: + - index /p1/login: post: description: >- diff --git a/routes/private/index.ts b/routes/private/index.ts index 4a1928262..1ccf20271 100644 --- a/routes/private/index.ts +++ b/routes/private/index.ts @@ -16,6 +16,7 @@ import * as collection from './routes/collection.ts'; import * as episode from './routes/episode.ts'; import * as friend from './routes/friend.ts'; import * as group from './routes/group.ts'; +import * as index from './routes/index.ts'; import * as misc from './routes/misc.ts'; import * as person from './routes/person.ts'; import * as search from './routes/search.ts'; @@ -78,6 +79,7 @@ async function API(app: App) { await app.register(episode.setup); await app.register(friend.setup); await app.register(group.setup); + await app.register(index.setup); await app.register(misc.setup); await app.register(search.setup); await app.register(person.setup); diff --git a/routes/private/routes/__snapshots__/index.test.ts.snap b/routes/private/routes/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000..cf232e238 --- /dev/null +++ b/routes/private/routes/__snapshots__/index.test.ts.snap @@ -0,0 +1,263 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`get index > should get index 1`] = ` +Object { + "award": 0, + "collects": 96, + "createdAt": 1352366596, + "desc": "[url]http://www.tudou.com/programs/view/W6eIoxnHs6g/[/url] +有美国动画混入,所以准确的说是在日本播放的动画最高收视率(而且是关东地区的 +基本大部分是70年代的,那个年代娱乐贫乏优势真大", + "id": 15045, + "replies": 8, + "stats": Object { + "2": 101, + }, + "title": "日本动画最高收视率TOP100", + "total": 101, + "type": 0, + "uid": 14127, + "updatedAt": 1356922367, + "user": Object { + "avatar": Object { + "large": "https://lain.bgm.tv/pic/user/l/icon.jpg", + "medium": "https://lain.bgm.tv/pic/user/m/icon.jpg", + "small": "https://lain.bgm.tv/pic/user/s/icon.jpg", + }, + "group": 0, + "id": 14127, + "joinedAt": 0, + "nickname": "nickname 14127", + "sign": "sing 14127", + "username": "14127", + }, +} +`; + +exports[`get index > should get index related 1`] = ` +Object { + "data": Array [ + Object { + "award": "", + "cat": 0, + "comment": "1位 40.3", + "createdAt": 1352382566, + "id": 29277, + "order": 1, + "rid": 15045, + "sid": 806, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "2位 39.9", + "createdAt": 1352382511, + "id": 29276, + "order": 2, + "rid": 15045, + "sid": 28165, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "3位 39.4", + "createdAt": 1352382444, + "id": 29275, + "order": 3, + "rid": 15045, + "sid": 32585, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "4位 36.9", + "createdAt": 1352382378, + "id": 29274, + "order": 4, + "rid": 15045, + "sid": 47243, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "5位 36.7", + "createdAt": 1352382135, + "id": 29272, + "order": 5, + "rid": 15045, + "sid": 41983, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "5位 36.7", + "createdAt": 1352382314, + "id": 29273, + "order": 5, + "rid": 15045, + "sid": 53805, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "7位 36", + "createdAt": 1352382072, + "id": 29271, + "order": 7, + "rid": 15045, + "sid": 37258, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "8位 35.6", + "createdAt": 1352382022, + "id": 29270, + "order": 8, + "rid": 15045, + "sid": 53804, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "9位 34.5", + "createdAt": 1352381764, + "id": 29266, + "order": 9, + "rid": 15045, + "sid": 53739, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "10位 33.7 (有奇怪的美国动画混进去了!", + "createdAt": 1352381582, + "id": 29264, + "order": 10, + "rid": 15045, + "sid": 25713, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "11位 33.6", + "createdAt": 1352380988, + "id": 29258, + "order": 11, + "rid": 15045, + "sid": 53802, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "12位 32.5", + "createdAt": 1352380796, + "id": 29257, + "order": 12, + "rid": 15045, + "sid": 36359, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "13位 31.9", + "createdAt": 1352380648, + "id": 29252, + "order": 13, + "rid": 15045, + "sid": 53801, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "13位 31.9", + "createdAt": 1352380716, + "id": 29254, + "order": 13, + "rid": 15045, + "sid": 2741, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "15位 31.8", + "createdAt": 1352380464, + "id": 29246, + "order": 15, + "rid": 15045, + "sid": 53800, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "16位 31.6", + "createdAt": 1352380102, + "id": 29245, + "order": 16, + "rid": 15045, + "sid": 10404, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "17位 31.2", + "createdAt": 1352379920, + "id": 29241, + "order": 17, + "rid": 15045, + "sid": 37460, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "18位 30.7", + "createdAt": 1352379781, + "id": 29239, + "order": 18, + "rid": 15045, + "sid": 53794, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "19位 30.4", + "createdAt": 1352379633, + "id": 29235, + "order": 19, + "rid": 15045, + "sid": 10390, + "type": 2, + }, + Object { + "award": "", + "cat": 0, + "comment": "20位 30.1", + "createdAt": 1352379532, + "id": 29234, + "order": 20, + "rid": 15045, + "sid": 10374, + "type": 2, + }, + ], + "total": 101, +} +`; diff --git a/routes/private/routes/__snapshots__/user.test.ts.snap b/routes/private/routes/__snapshots__/user.test.ts.snap index e0fafc81c..98eb6419a 100644 --- a/routes/private/routes/__snapshots__/user.test.ts.snap +++ b/routes/private/routes/__snapshots__/user.test.ts.snap @@ -148,6 +148,7 @@ Object { "title": "日本动画最高收视率TOP100", "total": 101, "type": 0, + "uid": 14127, }, ], "total": 1, diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 657bfa869..ffab4674e 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -4,6 +4,7 @@ import { DateTime } from 'luxon'; import { db, op, type orm, schema } from '@app/drizzle'; import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, NotFoundError, UnexpectedNotFoundError } from '@app/lib/error'; +import { IndexType } from '@app/lib/index/types'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { PersonCat } from '@app/lib/person/type.ts'; import { @@ -724,6 +725,7 @@ export async function setup(app: App) { }, async ({ auth, query: { limit = 20, offset = 0 } }) => { const conditions = op.and( + op.eq(schema.chiiIndexes.type, IndexType.User), op.eq(schema.chiiIndexCollects.uid, auth.userID), op.ne(schema.chiiIndexes.ban, 1), ); diff --git a/routes/private/routes/index.test.ts b/routes/private/routes/index.test.ts new file mode 100644 index 000000000..ae6865035 --- /dev/null +++ b/routes/private/routes/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vitest'; + +import { createTestServer } from '@app/tests/utils.ts'; + +import { setup } from './index.ts'; + +describe('get index', () => { + test('should get index', async () => { + const app = createTestServer(); + await app.register(setup); + const res = await app.inject({ + method: 'get', + url: '/indexes/15045', + }); + expect(res.json()).toMatchSnapshot(); + }); + + test('should get index related', async () => { + const app = createTestServer(); + await app.register(setup); + const res = await app.inject({ + method: 'get', + url: '/indexes/15045/related', + }); + expect(res.json()).toMatchSnapshot(); + }); +}); diff --git a/routes/private/routes/index.ts b/routes/private/routes/index.ts new file mode 100644 index 000000000..735791759 --- /dev/null +++ b/routes/private/routes/index.ts @@ -0,0 +1,152 @@ +import { Type as t } from '@sinclair/typebox'; + +import { db, op, schema } from '@app/drizzle'; +import { NotFoundError } from '@app/lib/error.ts'; +import { IndexRelatedCategory } from '@app/lib/index/types.ts'; +import { Security, Tag } from '@app/lib/openapi/index.ts'; +import * as convert from '@app/lib/types/convert.ts'; +import * as fetcher from '@app/lib/types/fetcher.ts'; +import * as req from '@app/lib/types/req.ts'; +import * as res from '@app/lib/types/res.ts'; +import type { App } from '@app/routes/type.ts'; + +// eslint-disable-next-line @typescript-eslint/require-await +export async function setup(app: App) { + app.get( + '/indexes/:indexID', + { + schema: { + summary: '获取目录详情', + operationId: 'getIndex', + tags: [Tag.Index], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + indexID: t.Integer(), + }), + response: { + 200: res.Ref(res.Index), + }, + }, + }, + async ({ params: { indexID } }) => { + const [data] = await db + .select() + .from(schema.chiiIndexes) + .where(op.eq(schema.chiiIndexes.id, indexID)); + if (!data) { + throw new NotFoundError('index'); + } + const index = convert.toIndex(data); + const user = await fetcher.fetchSlimUserByID(index.uid); + if (user) { + index.user = user; + } + return index; + }, + ); + + app.get( + '/indexes/:indexID/related', + { + schema: { + summary: '获取目录的关联内容', + operationId: 'getIndexRelated', + tags: [Tag.Index], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + indexID: t.Integer(), + }), + querystring: t.Object({ + cat: t.Optional(req.Ref(req.IndexRelatedCategory)), + type: t.Optional(req.Ref(req.SubjectType)), + limit: t.Optional( + t.Integer({ default: 20, minimum: 1, maximum: 100, description: 'max 100' }), + ), + offset: t.Optional(t.Integer({ default: 0, minimum: 0, description: 'min 0' })), + }), + response: { + 200: res.Paged(res.Ref(res.IndexRelated)), + }, + }, + }, + async ({ params: { indexID }, query: { cat, type, limit = 20, offset = 0 } }) => { + const index = await fetcher.fetchSlimIndexByID(indexID); + if (!index) { + throw new NotFoundError('index'); + } + + const conditions = [ + op.eq(schema.chiiIndexRelated.rid, indexID), + op.eq(schema.chiiIndexRelated.cat, IndexRelatedCategory.Subject), + op.eq(schema.chiiIndexRelated.ban, 0), + ]; + if (cat) { + conditions.push(op.eq(schema.chiiIndexRelated.cat, cat)); + } + if (type) { + conditions.push(op.eq(schema.chiiIndexRelated.type, type)); + } + + const [{ count = 0 } = {}] = await db + .select({ count: op.count() }) + .from(schema.chiiIndexRelated) + .where(op.and(...conditions)); + + const data = await db + .select() + .from(schema.chiiIndexRelated) + .where(op.and(...conditions)) + .orderBy(op.asc(schema.chiiIndexRelated.order), op.asc(schema.chiiIndexRelated.id)) + .limit(limit) + .offset(offset); + const items = data.map((item) => convert.toIndexRelated(item)); + + const subjectIDs = items + .filter((item) => item.cat === IndexRelatedCategory.Subject) + .map((item) => item.sid); + const subjects = await fetcher.fetchSlimSubjectsByIDs(subjectIDs); + + const characterIDs = items + .filter((item) => item.cat === IndexRelatedCategory.Character) + .map((item) => item.sid); + const characters = await fetcher.fetchSlimCharactersByIDs(characterIDs); + + const personIDs = items + .filter((item) => item.cat === IndexRelatedCategory.Person) + .map((item) => item.sid); + const persons = await fetcher.fetchSlimPersonsByIDs(personIDs); + + const episodeIDs = items + .filter((item) => item.cat === IndexRelatedCategory.Ep) + .map((item) => item.sid); + const episodes = await fetcher.fetchSlimEpisodesByIDs(episodeIDs); + + const result = []; + for (const item of items) { + switch (item.cat) { + case IndexRelatedCategory.Subject: { + item.subject = subjects[item.sid]; + break; + } + case IndexRelatedCategory.Character: { + item.character = characters[item.sid]; + break; + } + case IndexRelatedCategory.Person: { + item.person = persons[item.sid]; + break; + } + case IndexRelatedCategory.Ep: { + item.episode = episodes[item.sid]; + break; + } + } + result.push(item); + } + return { + data: result, + total: count, + }; + }, + ); +} diff --git a/routes/private/routes/user.ts b/routes/private/routes/user.ts index bfc58d5e7..c418ce55c 100644 --- a/routes/private/routes/user.ts +++ b/routes/private/routes/user.ts @@ -2,6 +2,7 @@ import { Type as t } from '@sinclair/typebox'; import { db, op, schema } from '@app/drizzle'; import { NotFoundError } from '@app/lib/error.ts'; +import { IndexType } from '@app/lib/index/types'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { PersonCat } from '@app/lib/person/type.ts'; import { CollectionPrivacy } from '@app/lib/subject/type.ts'; @@ -436,6 +437,7 @@ export async function setup(app: App) { } const conditions = op.and( + op.eq(schema.chiiIndexes.type, IndexType.User), op.eq(schema.chiiIndexCollects.uid, user.id), op.ne(schema.chiiIndexes.ban, 1), ); @@ -550,6 +552,7 @@ export async function setup(app: App) { } const conditions = op.and( + op.eq(schema.chiiIndexes.type, IndexType.User), op.eq(schema.chiiIndexes.uid, user.id), op.ne(schema.chiiIndexes.ban, 1), ); diff --git a/routes/schemas.ts b/routes/schemas.ts index 08d8b69a9..075b663a7 100644 --- a/routes/schemas.ts +++ b/routes/schemas.ts @@ -11,11 +11,13 @@ export function addSchemas(app: App) { function addCommonSchemas(app: App) { app.addSchema(common.CollectionType); - app.addSchema(common.EpisodeType); - app.addSchema(common.SubjectType); - app.addSchema(common.GroupMemberRole); app.addSchema(common.EpisodeCollectionStatus); + app.addSchema(common.EpisodeType); app.addSchema(common.EpisodeWikiInfo); + app.addSchema(common.GroupMemberRole); + app.addSchema(common.IndexRelatedCategory); + app.addSchema(common.IndexType); + app.addSchema(common.SubjectType); } function addRequestSchemas(app: App) { @@ -58,6 +60,7 @@ function addResponseSchemas(app: App) { app.addSchema(res.GroupTopic); app.addSchema(res.Index); app.addSchema(res.IndexStats); + app.addSchema(res.IndexRelated); app.addSchema(res.Infobox); app.addSchema(res.Permissions); app.addSchema(res.Person);