diff --git a/drizzle/orm.ts b/drizzle/orm.ts index dc696dc6..b01fa0f6 100644 --- a/drizzle/orm.ts +++ b/drizzle/orm.ts @@ -10,6 +10,7 @@ export type ISubject = typeof schema.chiiSubjects.$inferSelect; export type ISubjectFields = typeof schema.chiiSubjectFields.$inferSelect; export type ISubjectInterest = typeof schema.chiiSubjectInterests.$inferSelect; export type ISubjectRelation = typeof schema.chiiSubjectRelations.$inferSelect; +export type ISubjectRelatedBlog = typeof schema.chiiSubjectRelatedBlogs.$inferSelect; export type ISubjectTopic = typeof schema.chiiSubjectTopics.$inferSelect; export type ISubjectPost = typeof schema.chiiSubjectPosts.$inferSelect; export type ISubjectEpStatus = typeof schema.chiiEpStatus.$inferSelect; @@ -29,5 +30,8 @@ export type IPersonCollect = typeof schema.chiiPersonCollects.$inferSelect; export type IIndex = typeof schema.chiiIndexes.$inferSelect; export type IIndexCollect = typeof schema.chiiIndexCollects.$inferSelect; +export type IBlogEntry = typeof schema.chiiBlogEntries.$inferSelect; +export type IBlogPhoto = typeof schema.chiiBlogPhotos.$inferSelect; + export type ITimeline = typeof schema.chiiTimeline.$inferSelect; export type ITimelineComment = typeof schema.chiiTimelineComments.$inferSelect; diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 17eac40f..e6f3da3b 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -985,6 +985,26 @@ export const chiiSubjectInterests = mysqlTable( }, ); +export const chiiSubjectRelatedBlogs = mysqlTable( + 'chii_subject_related_blog', + { + id: mediumint('srb_id').autoincrement().notNull(), + uid: mediumint('srb_uid').notNull(), + subjectID: mediumint('srb_subject_id').notNull(), + entryID: mediumint('srb_entry_id').notNull(), // blog etry id + spoiler: mediumint('srb_spoiler').notNull(), + like: mediumint('srb_like').notNull(), + dislike: mediumint('srb_dislike').notNull(), + createdAt: int('srb_dateline').notNull(), + }, + (table) => { + return { + srbUid: index('srb_uid').on(table.uid, table.subjectID, table.entryID), + subjectRelated: index('subject_related').on(table.subjectID), + }; + }, +); + export const chiiSubjectPosts = mysqlTable( 'chii_subject_posts', { @@ -1184,3 +1204,70 @@ export const chiiUsergroup = mysqlTable('chii_usergroup', { perm: mediumtext('usr_grp_perm').notNull(), updatedAt: int('usr_grp_dateline').notNull(), }); + +export const chiiBlogComments = mysqlTable( + 'chii_blog_comments', + { + id: mediumint('blg_pst_id').autoincrement().notNull(), + mid: mediumint('blg_pst_mid').notNull(), // blog entry id + uid: mediumint('blg_pst_uid').notNull(), + related: mediumint('blg_pst_related').notNull(), + updatedAt: int('blg_pst_dateline').notNull(), + content: mediumtext('blg_pst_content').notNull(), + }, + (table) => { + return { + blgCmtEid: index('blg_cmt_eid').on(table.mid), + blgCmtUid: index('blg_cmt_uid').on(table.uid), + blgPstRelated: index('blg_pst_related').on(table.related), + }; + }, +); + +export const chiiBlogEntries = mysqlTable( + 'chii_blog_entry', + { + id: mediumint('entry_id').autoincrement().notNull(), + type: smallint('entry_type').notNull(), + uid: mediumint('entry_uid').notNull(), + title: varchar('entry_title', { length: 80 }).notNull(), + icon: varchar('entry_icon', { length: 255 }).notNull(), + content: mediumtext('entry_content').notNull(), + tags: mediumtext('entry_tags').notNull(), + views: mediumint('entry_views').notNull(), + replies: mediumint('entry_replies').notNull(), + createdAt: int('entry_dateline').notNull(), + updatedAt: int('entry_lastpost').notNull(), + like: int('entry_like').notNull(), // 未使用 + dislike: int('entry_dislike').notNull(), // 未使用 + noreply: smallint('entry_noreply').notNull(), + related: tinyint('entry_related').default(0).notNull(), + public: customBoolean('entry_public').default(true).notNull(), + }, + (table) => { + return { + entryType: index('entry_type').on(table.type, table.uid, table.noreply), + entryRelate: index('entry_relate').on(table.related), + entryPublic: index('entry_public').on(table.public), + entryDateline: index('entry_dateline').on(table.createdAt), + entryUid: index('entry_uid').on(table.uid, table.public), + }; + }, +); + +export const chiiBlogPhotos = mysqlTable( + 'chii_blog_photo', + { + id: mediumint('photo_id').autoincrement().notNull(), + eid: mediumint('photo_eid').notNull(), + uid: mediumint('photo_uid').notNull(), + target: varchar('photo_target', { length: 255 }).notNull(), + vote: mediumint('photo_vote').notNull(), + createdAt: int('photo_dateline').notNull(), + }, + (table) => { + return { + photoEid: index('photo_eid').on(table.eid), + }; + }, +); diff --git a/lib/types/convert.ts b/lib/types/convert.ts index 48d0e584..962c7a22 100644 --- a/lib/types/convert.ts +++ b/lib/types/convert.ts @@ -327,6 +327,37 @@ export function toSubjectStaffPosition(relation: orm.IPersonSubject): res.ISubje }; } +export function toBlogEntry(entry: orm.IBlogEntry, user: orm.IUser): res.IBlogEntry { + return { + id: entry.id, + type: entry.type, + user: toSlimUser(user), + title: entry.title, + icon: entry.icon, + content: entry.content, + tags: splitTags(entry.tags), + views: entry.views, + replies: entry.replies, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + noreply: entry.noreply, + related: entry.related, + public: entry.public, + }; +} + +export function toSlimBlogEntry(entry: orm.IBlogEntry): res.ISlimBlogEntry { + return { + id: entry.id, + type: entry.type, + title: entry.title, + summary: entry.content.slice(0, 120), + replies: entry.replies, + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + }; +} + export function toSubjectComment( interest: orm.ISubjectInterest, user: orm.IUser, @@ -340,6 +371,18 @@ export function toSubjectComment( }; } +export function toSubjectReview( + review: orm.ISubjectRelatedBlog, + blog: orm.IBlogEntry, + user: orm.IUser, +): res.ISubjectReview { + return { + id: review.id, + user: toSlimUser(user), + entry: toSlimBlogEntry(blog), + }; +} + export function toEpisode(episode: orm.IEpisode): res.IEpisode { return { id: episode.id, diff --git a/lib/types/res.ts b/lib/types/res.ts index 9ed102ff..19b9dd2f 100644 --- a/lib/types/res.ts +++ b/lib/types/res.ts @@ -417,6 +417,41 @@ export const SlimPerson = t.Object( }, ); +export type IBlogEntry = Static; +export const BlogEntry = t.Object( + { + id: t.Integer(), + type: t.Integer(), + user: t.Ref(SlimUser), + title: t.String(), + icon: t.String(), + content: t.String(), + tags: t.Array(t.String()), + views: t.Integer(), + replies: t.Integer(), + createdAt: t.Integer(), + updatedAt: t.Integer(), + noreply: t.Integer(), + related: t.Integer(), + public: t.Boolean(), + }, + { $id: 'BlogEntry', title: 'BlogEntry' }, +); + +export type ISlimBlogEntry = Static; +export const SlimBlogEntry = t.Object( + { + id: t.Integer(), + type: t.Integer(), + title: t.String(), + summary: t.String(), + replies: t.Integer(), + createdAt: t.Integer(), + updatedAt: t.Integer(), + }, + { $id: 'SlimBlogEntry', title: 'SlimBlogEntry' }, +); + export type ISubjectComment = Static; export const SubjectComment = t.Object( { @@ -429,6 +464,16 @@ export const SubjectComment = t.Object( { $id: 'SubjectComment', title: 'SubjectComment' }, ); +export type ISubjectReview = Static; +export const SubjectReview = t.Object( + { + id: t.Integer(), + user: t.Ref(SlimUser), + entry: t.Ref(SlimBlogEntry), + }, + { $id: 'SubjectReview', title: 'SubjectReview' }, +); + export type ISubjectRelation = Static; export const SubjectRelation = t.Object( { diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index 3b6f9af7..5456fe97 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -65,6 +65,55 @@ exports[`should build private api spec 1`] = ` - text - state type: object + BlogEntry: + properties: + content: + type: string + createdAt: + type: integer + icon: + type: string + id: + type: integer + noreply: + type: integer + public: + type: boolean + related: + type: integer + replies: + type: integer + tags: + items: + type: string + type: array + title: + type: string + type: + type: integer + updatedAt: + type: integer + user: + $ref: '#/components/schemas/SlimUser' + views: + type: integer + required: + - id + - type + - user + - title + - icon + - content + - tags + - views + - replies + - createdAt + - updatedAt + - noreply + - related + - public + title: BlogEntry + type: object Character: properties: collects: @@ -919,6 +968,32 @@ exports[`should build private api spec 1`] = ` - reactions title: Reply type: object + SlimBlogEntry: + properties: + createdAt: + type: integer + id: + type: integer + replies: + type: integer + summary: + type: string + title: + type: string + type: + type: integer + updatedAt: + type: integer + required: + - id + - type + - title + - summary + - replies + - createdAt + - updatedAt + title: SlimBlogEntry + type: object SlimCharacter: properties: comment: @@ -1592,6 +1667,20 @@ exports[`should build private api spec 1`] = ` - jp - desc type: object + SubjectReview: + properties: + entry: + $ref: '#/components/schemas/SlimBlogEntry' + id: + type: integer + user: + $ref: '#/components/schemas/SlimUser' + required: + - id + - user + - entry + title: SubjectReview + type: object SubjectStaff: properties: person: @@ -4816,6 +4905,62 @@ paths: summary: 获取条目的关联条目 tags: - subject + /p1/subjects/{subjectID}/reviews: + get: + operationId: getSubjectReviews + parameters: + - description: max 20 + in: query + name: limit + required: false + schema: + default: 5 + maximum: 20 + minimum: 1 + type: integer + - description: min 0 + in: query + name: offset + required: false + schema: + default: 0 + minimum: 0 + type: integer + - in: path + name: subjectID + required: true + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + properties: + data: + items: + $ref: '#/components/schemas/SubjectReview' + type: array + total: + 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: + - subject /p1/subjects/{subjectID}/staffs: get: operationId: getSubjectStaffs diff --git a/routes/private/routes/__snapshots__/subject.test.ts.snap b/routes/private/routes/__snapshots__/subject.test.ts.snap index c0b99a0e..9578d94e 100644 --- a/routes/private/routes/__snapshots__/subject.test.ts.snap +++ b/routes/private/routes/__snapshots__/subject.test.ts.snap @@ -569,9 +569,33 @@ Object { exports[`subject > should get subject reviews 1`] = ` Object { - "error": "Not Found", - "message": "Route GET:/subjects/184017/reviews?limit=2&offset=0 not found", - "statusCode": 404, + "data": Array [ + Object { + "entry": Object { + "createdAt": 1680065731, + "id": 319484, + "replies": 1, + "summary": "试试", + "title": "试试", + "type": 1, + "updatedAt": 1680072373, + }, + "id": 168120, + "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", + }, + "id": 287622, + "joinedAt": 0, + "nickname": "nickname 287622", + "sign": "sing 287622", + "username": "287622", + }, + }, + ], + "total": 1, } `; diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index c95bf2d9..c8397785 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -531,6 +531,70 @@ export async function setup(app: App) { }, ); + app.get( + '/subjects/:subjectID/reviews', + { + schema: { + summary: '获取条目的评论', + operationId: 'getSubjectReviews', + tags: [Tag.Subject], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + subjectID: t.Integer(), + }), + querystring: t.Object({ + limit: t.Optional( + t.Integer({ default: 5, minimum: 1, maximum: 20, description: 'max 20' }), + ), + offset: t.Optional(t.Integer({ default: 0, minimum: 0, description: 'min 0' })), + }), + response: { + 200: res.Paged(t.Ref(res.SubjectReview)), + }, + }, + }, + async ({ auth, params: { subjectID }, query: { limit = 5, offset = 0 } }) => { + const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + if (!subject) { + throw new NotFoundError(`subject ${subjectID}`); + } + const condition = op.and( + op.eq(schema.chiiSubjectRelatedBlogs.subjectID, subjectID), + op.eq(schema.chiiBlogEntries.public, true), + ); + const [{ count = 0 } = {}] = await db + .select({ count: op.count() }) + .from(schema.chiiSubjectRelatedBlogs) + .innerJoin(schema.chiiUsers, op.eq(schema.chiiSubjectRelatedBlogs.uid, schema.chiiUsers.id)) + .innerJoin( + schema.chiiBlogEntries, + op.eq(schema.chiiSubjectRelatedBlogs.entryID, schema.chiiBlogEntries.id), + ) + .where(condition) + .execute(); + const data = await db + .select() + .from(schema.chiiSubjectRelatedBlogs) + .innerJoin(schema.chiiUsers, op.eq(schema.chiiSubjectRelatedBlogs.uid, schema.chiiUsers.id)) + .innerJoin( + schema.chiiBlogEntries, + op.eq(schema.chiiSubjectRelatedBlogs.entryID, schema.chiiBlogEntries.id), + ) + .where(condition) + .orderBy(op.desc(schema.chiiBlogEntries.createdAt)) + .limit(limit) + .offset(offset) + .execute(); + const reviews = data.map((d) => + convert.toSubjectReview(d.chii_subject_related_blog, d.chii_blog_entry, d.chii_members), + ); + return { + data: reviews, + total: count, + }; + }, + ); + app.get( '/subjects/:subjectID/topics', { diff --git a/routes/res.ts b/routes/res.ts index 13d8d405..601c5050 100644 --- a/routes/res.ts +++ b/routes/res.ts @@ -4,6 +4,7 @@ import type { App } from '@app/routes/type.ts'; export function addSchemas(app: App) { app.addSchema(res.Avatar); + app.addSchema(res.BlogEntry); app.addSchema(res.Character); app.addSchema(res.CharacterRelation); app.addSchema(res.CharacterSubject); @@ -24,6 +25,7 @@ export function addSchemas(app: App) { app.addSchema(res.PersonWork); app.addSchema(res.Reaction); app.addSchema(res.Reply); + app.addSchema(res.SlimBlogEntry); app.addSchema(res.SlimCharacter); app.addSchema(res.SlimIndex); app.addSchema(res.SlimPerson); @@ -41,6 +43,7 @@ export function addSchemas(app: App) { app.addSchema(res.SubjectRec); app.addSchema(res.SubjectRelation); app.addSchema(res.SubjectRelationType); + app.addSchema(res.SubjectReview); app.addSchema(res.SubjectStaff); app.addSchema(res.SubjectStaffPosition); app.addSchema(res.SubjectStaffPositionType);