From 58dc28fe141533d5cc4fc602134c8b278e50b617 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 26 Jan 2025 16:35:40 +0800 Subject: [PATCH 01/65] z --- lib/types/req.ts | 12 ++++++++++++ routes/private/routes/subject.ts | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/types/req.ts b/lib/types/req.ts index 0db08ca47..841fc2ab0 100644 --- a/lib/types/req.ts +++ b/lib/types/req.ts @@ -1,6 +1,7 @@ import type { Static } from '@sinclair/typebox'; import { Type as t } from '@sinclair/typebox'; +import { CollectionType, Ref } from '@app/lib/types/common.ts'; import * as examples from '@app/lib/types/examples.ts'; export * from '@app/lib/types/common.ts'; @@ -98,3 +99,14 @@ export const UpdateEpisodeComment = t.Object( }, { $id: 'UpdateEpisodeComment' }, ); + +export type ICollectSubject = Static; +export const CollectSubject = t.Object({ + type: t.Optional(Ref(CollectionType)), + rate: t.Optional(t.Integer({ minimum: 0, maximum: 10, description: '评分,0 表示删除评分' })), + epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), + volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), + comment: t.Optional(t.String({ description: '评价' })), + private: t.Optional(t.Boolean({ description: '仅自己可见' })), + tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), +}); diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index b19de79c7..15835305e 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -656,6 +656,30 @@ export async function setup(app: App) { }, ); + app.post( + '/subjects/:subjectID/collect', + { + schema: { + summary: '新增或修改条目收藏', + operationId: 'collectSubject', + tags: [Tag.Subject], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + subjectID: t.Integer(), + }), + body: req.Ref(req.CollectSubject), + }, + preHandler: [requireLogin('collecting a subject')], + }, + async ({ auth, params: { subjectID } }) => { + const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + if (!subject) { + throw new NotFoundError(`subject ${subjectID}`); + } + // TODO: + }, + ); + app.get( '/subjects/:subjectID/topics', { From 8dbc75a352368f595c85027b2502fc1d93c41c7c Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 26 Jan 2025 16:46:19 +0800 Subject: [PATCH 02/65] z --- routes/private/routes/subject.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 15835305e..80ff41078 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -676,6 +676,7 @@ export async function setup(app: App) { if (!subject) { throw new NotFoundError(`subject ${subjectID}`); } + // const interest = await fetcher.fetchSubjectInterest(auth.userID, subjectID); // TODO: }, ); From c0f512b856b812e7e4897b1974775c017681a9a5 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 31 Jan 2025 09:45:40 +0800 Subject: [PATCH 03/65] z --- routes/schemas.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/schemas.ts b/routes/schemas.ts index d70fc9231..8cef416bf 100644 --- a/routes/schemas.ts +++ b/routes/schemas.ts @@ -8,6 +8,7 @@ export function addSchemas(app: App) { app.addSchema(common.EpisodeType); app.addSchema(common.SubjectType); + app.addSchema(req.CollectSubject); app.addSchema(req.CreateEpisodeComment); app.addSchema(req.CreatePost); app.addSchema(req.CreateTopic); From 82b82a3736fb77138e76991be216f06e27dfa9fc Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 31 Jan 2025 15:21:18 +0800 Subject: [PATCH 04/65] z --- lib/types/req.ts | 2 +- routes/private/routes/subject.ts | 71 +++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/types/req.ts b/lib/types/req.ts index 1104a2b20..d07ca3515 100644 --- a/lib/types/req.ts +++ b/lib/types/req.ts @@ -130,7 +130,7 @@ export const CollectSubject = t.Object({ epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), comment: t.Optional(t.String({ description: '评价' })), - private: t.Optional(t.Boolean({ description: '仅自己可见' })), + priv: t.Optional(t.Boolean({ description: '仅自己可见' })), tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), }); diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index c4048f9b6..ce034f557 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -764,11 +764,80 @@ export async function setup(app: App) { }, preHandler: [requireLogin('collecting a subject')], }, - async ({ auth, params: { subjectID } }) => { + async ({ + auth, + params: { subjectID }, + body: { type, rate, epStatus, volStatus, comment, priv, tags }, + }) => { const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); if (!subject) { throw new NotFoundError(`subject ${subjectID}`); } + if (auth.permission.ban_post) { + throw new NotAllowedError('collect subject'); + } + if (comment) { + if (!Dam.allCharacterPrintable(comment)) { + throw new BadRequestError('comment contains invalid invisible character'); + } + comment = comment.normalize('NFC'); + if (comment.length > 380) { + throw new BadRequestError('comment too long'); + } + } + await rateLimit(LimitAction.Subject, auth.userID); + const _ = { + uid: auth.userID, + subjectID, + subjectType: subject.type, + type, + rate, + epStatus, + volStatus, + comment, + private: priv, + tag: tags?.join(' '), + }; + + // await db.transaction(async (t) => { + // const [interest] = await t + // .select() + // .from(schema.chiiSubjectInterests) + // .where( + // op.and( + // op.eq(schema.chiiSubjectInterests.uid, auth.userID), + // op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + // ), + // ) + // .limit(1); + // if (interest) { + // await t + // .update(schema.chiiSubjectInterests) + // .set({ + // type, + // rate, + // epStatus, + // volStatus, + // comment, + // private: priv, + // tag: tags?.join(' '), + // }) + // .where(op.eq(schema.chiiSubjectInterests.id, interest.id)); + // } else { + // await t.insert(schema.chiiSubjectInterests).values({ + // uid: auth.userID, + // subjectID, + // subjectType: subject.type, + // type, + // rate, + // epStatus, + // volStatus, + // comment, + // private: priv, + // tag: tags?.join(' '), + // }); + // } + // }); // const interest = await fetcher.fetchSubjectInterest(auth.userID, subjectID); // TODO: }, From 4ce9000cb96eef52bd91d484b78fa2fd374a62d8 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 18:07:11 +0800 Subject: [PATCH 05/65] z --- lib/types/req.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/types/req.ts b/lib/types/req.ts index dd50f37e5..46a433157 100644 --- a/lib/types/req.ts +++ b/lib/types/req.ts @@ -109,15 +109,18 @@ export const UpdateEpisodeComment = t.Object( ); export type ICollectSubject = Static; -export const CollectSubject = t.Object({ - type: t.Optional(Ref(CollectionType)), - rate: t.Optional(t.Integer({ minimum: 0, maximum: 10, description: '评分,0 表示删除评分' })), - epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), - volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), - comment: t.Optional(t.String({ description: '评价' })), - priv: t.Optional(t.Boolean({ description: '仅自己可见' })), - tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), -}); +export const CollectSubject = t.Object( + { + type: t.Optional(Ref(CollectionType)), + rate: t.Optional(t.Integer({ minimum: 0, maximum: 10, description: '评分,0 表示删除评分' })), + epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), + volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), + comment: t.Optional(t.String({ description: '评价' })), + priv: t.Optional(t.Boolean({ description: '仅自己可见' })), + tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), + }, + { $id: 'CollectSubject' }, +); export type ICreateTimelineSay = Static; export const CreateTimelineSay = t.Object( From 31dc44c9f5b9bb8508e69c94e47f2133837013fa Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:31:12 +0800 Subject: [PATCH 06/65] z --- drizzle/db.ts | 10 +- drizzle/schema.ts | 4 +- lib/auth/index.ts | 3 + lib/subject/type.ts | 28 +++- lib/types/convert.ts | 4 +- lib/types/fetcher.ts | 2 +- lib/types/res.ts | 16 ++- lib/user/stats.ts | 4 +- routes/hooks/pre-handler.ts | 6 +- routes/private/routes/subject.ts | 216 ++++++++++++++++++++++--------- routes/private/routes/user.ts | 4 +- routes/schemas.ts | 1 + 12 files changed, 225 insertions(+), 73 deletions(-) diff --git a/drizzle/db.ts b/drizzle/db.ts index 548a8b81c..9de131b08 100644 --- a/drizzle/db.ts +++ b/drizzle/db.ts @@ -1,4 +1,4 @@ -import type { ExtractTablesWithRelations } from 'drizzle-orm'; +import { type AnyColumn, type ExtractTablesWithRelations, sql } from 'drizzle-orm'; import type { MySqlTransaction } from 'drizzle-orm/mysql-core'; import { drizzle, @@ -33,6 +33,14 @@ export const db = drizzle(poolConnection, { : undefined, }); +export const increment = (column: AnyColumn, value = 1) => { + return sql`${column} + ${value}`; +}; + +export const decrement = (column: AnyColumn, value = 1) => { + return sql`${column} - ${value}`; +}; + export * as op from 'drizzle-orm'; export type Txn = MySqlTransaction< diff --git a/drizzle/schema.ts b/drizzle/schema.ts index ce1b9368a..4dab26507 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -498,7 +498,7 @@ export const chiiSubjects = mysqlTable('chii_subjects', { volumes: mediumint('field_volumes').notNull(), eps: mediumint('field_eps').notNull(), wish: mediumint('subject_wish').notNull(), - done: mediumint('subject_collect').notNull(), + collect: mediumint('subject_collect').notNull(), doing: mediumint('subject_doing').notNull(), onHold: mediumint('subject_on_hold').notNull(), dropped: mediumint('subject_dropped').notNull(), @@ -572,7 +572,7 @@ export const chiiSubjectInterests = mysqlTable('chii_subject_interests', { createIp: char('interest_create_ip', { length: 15 }).notNull(), updateIp: char('interest_lasttouch_ip', { length: 15 }).notNull(), updatedAt: int('interest_lasttouch').default(0).notNull(), - private: customBoolean('interest_private').notNull(), + private: tinyint('interest_private').notNull(), }); export const chiiSubjectRelatedBlogs = mysqlTable('chii_subject_related_blog', { diff --git a/lib/auth/index.ts b/lib/auth/index.ts index 47c73f4a7..858acee92 100644 --- a/lib/auth/index.ts +++ b/lib/auth/index.ts @@ -62,6 +62,7 @@ export interface IAuth { /** Unix time seconds */ regTime: number; groupID: UserGroup; + ip: string; } export async function byHeader(key: string | string[] | undefined): Promise { @@ -157,6 +158,7 @@ export function emptyAuth(): IAuth { allowNsfw: false, regTime: 0, groupID: 0, + ip: '', }; } @@ -173,6 +175,7 @@ async function userToAuth(user: IUser): Promise { DateTime.now().toUnixInteger() - user.regTime >= 60 * 60 * 24 * 90, regTime: user.regTime, groupID: user.groupID, + ip: '', }; } diff --git a/lib/subject/type.ts b/lib/subject/type.ts index 3dad8e07c..a5e9a6289 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -28,10 +28,36 @@ export enum CollectionType { OnHold = 4, Dropped = 5, } - export const CollectionTypeValues = new Set([1, 2, 3, 4, 5]); export const CollectionTypeProfileValues = new Set([1, 2]); +export function getCollectionTypeField(type: CollectionType) { + switch (type) { + case CollectionType.Wish: { + return 'wish'; + } + case CollectionType.Collect: { + return 'collect'; + } + case CollectionType.Doing: { + return 'doing'; + } + case CollectionType.OnHold: { + return 'onHold'; + } + case CollectionType.Dropped: { + return 'dropped'; + } + } +} + +export enum CollectionPrivacy { + Public = 0, + Private = 1, + Ban = 2, +} +export const SubjectInterestPrivacyValues = new Set([0, 1, 2]); + export enum EpisodeCollectionStatus { None = 0, // 撤消/删除 Wish = 1, // 想看 diff --git a/lib/types/convert.ts b/lib/types/convert.ts index 3269b1136..cb4dd64d5 100644 --- a/lib/types/convert.ts +++ b/lib/types/convert.ts @@ -213,7 +213,7 @@ function toSubjectAirtime(fields: orm.ISubjectFields): res.ISubjectAirtime { function toSubjectCollection(subject: orm.ISubject): res.ISubjectCollection { return { [CollectionType.Wish]: subject.wish, - [CollectionType.Collect]: subject.done, + [CollectionType.Collect]: subject.collect, [CollectionType.Doing]: subject.doing, [CollectionType.OnHold]: subject.onHold, [CollectionType.Dropped]: subject.dropped, @@ -311,7 +311,7 @@ export function toSubjectInterest(interest: orm.ISubjectInterest): res.ISubjectI tags: splitTags(interest.tag), epStatus: interest.epStatus, volStatus: interest.volStatus, - private: interest.private, + privacy: interest.private, updatedAt: interest.updatedAt, }; } diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index a19e1d1f1..28577632d 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -348,7 +348,7 @@ export async function fetchSubjectIDsByFilter( break; } case SubjectSort.Collects: { - sorts.push(op.desc(schema.chiiSubjects.done)); + sorts.push(op.desc(schema.chiiSubjects.collect)); break; } case SubjectSort.Date: { diff --git a/lib/types/res.ts b/lib/types/res.ts index 540cab4d6..fe2490a6d 100644 --- a/lib/types/res.ts +++ b/lib/types/res.ts @@ -340,6 +340,20 @@ export const SubjectImages = t.Object( { $id: 'SubjectImages', title: 'SubjectImages' }, ); +export const SubjectInterestPrivacy = t.Integer({ + $id: 'SubjectInterestPrivacy', + enum: [0, 1, 2], + 'x-ms-enum': { + name: 'SubjectInterestPrivacy', + modelAsString: false, + }, + 'x-enum-varnames': ['Public', 'Private', 'Ban'], + description: `条目收藏隐私 + - 0 = 公开 + - 1 = 仅自己可见 + - 2 = 封禁`, +}); + export type ISubjectInterest = Static; export const SubjectInterest = t.Object( { @@ -349,7 +363,7 @@ export const SubjectInterest = t.Object( tags: t.Array(t.String()), epStatus: t.Integer(), volStatus: t.Integer(), - private: t.Boolean(), + privacy: Ref(SubjectInterestPrivacy), updatedAt: t.Integer(), }, { $id: 'SubjectInterest', title: 'SubjectInterest' }, diff --git a/lib/user/stats.ts b/lib/user/stats.ts index 279892fe3..8ee8b27a9 100644 --- a/lib/user/stats.ts +++ b/lib/user/stats.ts @@ -1,7 +1,7 @@ import { db, op } from '@app/drizzle/db.ts'; import * as schema from '@app/drizzle/schema.ts'; import redis from '@app/lib/redis'; -import { CollectionType, SubjectType } from '@app/lib/subject/type.ts'; +import { CollectionPrivacy, CollectionType, SubjectType } from '@app/lib/subject/type.ts'; import type * as res from '@app/lib/types/res.ts'; import { getStatsCacheKey } from './cache.ts'; @@ -121,7 +121,7 @@ export async function countUserSubjectCollection( .where( op.and( op.eq(schema.chiiSubjectInterests.uid, uid), - op.eq(schema.chiiSubjectInterests.private, false), + op.eq(schema.chiiSubjectInterests.private, CollectionPrivacy.Public), op.ne(schema.chiiSubjectInterests.type, 0), ), ) diff --git a/routes/hooks/pre-handler.ts b/routes/hooks/pre-handler.ts index 30bdffd96..8723f4a94 100644 --- a/routes/hooks/pre-handler.ts +++ b/routes/hooks/pre-handler.ts @@ -67,7 +67,9 @@ async function legacySessionAuth(req: FastifyRequest): Promise { } if (user.password === passwordCrypt) { - req.auth = await auth.byUserID(userID); + const a = await auth.byUserID(userID); + a.ip = req.ip; + req.auth = a; req.requestContext.set('user', req.auth.userID); return true; } @@ -94,6 +96,7 @@ export async function sessionAuth(req: FastifyRequest, res: FastifyReply) { return; } + a.ip = req.ip; req.auth = a; req.requestContext.set('user', a.userID); } @@ -112,6 +115,7 @@ export async function accessTokenAuth(req: FastifyRequest): Promise { } const a = await auth.byHeader(token); if (a) { + a.ip = req.ip; req.auth = a; req.requestContext.set('user', a.userID); return true; diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 5d54ce573..2713b0fad 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -1,7 +1,7 @@ import { Type as t } from '@sinclair/typebox'; import { DateTime } from 'luxon'; -import { db, op } from '@app/drizzle/db.ts'; +import { db, decrement, increment, op } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { NotAllowedError } from '@app/lib/auth/index.ts'; @@ -16,7 +16,12 @@ import { fetchSubjectCollectReactions, fetchTopicReactions, LikeType } from '@ap import * as Notify from '@app/lib/notify.ts'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { turnstile } from '@app/lib/services/turnstile.ts'; -import type { SubjectFilter, SubjectSort } from '@app/lib/subject/type.ts'; +import { + CollectionPrivacy, + getCollectionTypeField, + type SubjectFilter, + type SubjectSort, +} from '@app/lib/subject/type.ts'; import { CanViewTopicContent, CanViewTopicReply } from '@app/lib/topic/display.ts'; import { canEditTopic, canReplyPost } from '@app/lib/topic/state'; import { CommentState, TopicDisplay } from '@app/lib/topic/type.ts'; @@ -673,7 +678,7 @@ export async function setup(app: App) { } const condition = op.and( op.eq(schema.chiiSubjectInterests.subjectID, subjectID), - op.eq(schema.chiiSubjectInterests.private, false), + op.eq(schema.chiiSubjectInterests.private, CollectionPrivacy.Public), op.eq(schema.chiiSubjectInterests.hasComment, 1), type ? op.eq(schema.chiiSubjectInterests.type, type) : undefined, ); @@ -787,12 +792,13 @@ export async function setup(app: App) { params: { subjectID }, body: { type, rate, epStatus, volStatus, comment, priv, tags }, }) => { - const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); - if (!subject) { + const slimSubject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + if (!slimSubject) { throw new NotFoundError(`subject ${subjectID}`); } - if (auth.permission.ban_post) { - throw new NotAllowedError('collect subject'); + let privacy = CollectionPrivacy.Public; + if (priv !== undefined) { + privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; } if (comment) { if (!Dam.allCharacterPrintable(comment)) { @@ -802,62 +808,152 @@ export async function setup(app: App) { if (comment.length > 380) { throw new BadRequestError('comment too long'); } + if (dam.needReview(comment) || auth.permission.ban_post) { + privacy = CollectionPrivacy.Ban; + } } + await rateLimit(LimitAction.Subject, auth.userID); - const _ = { - uid: auth.userID, - subjectID, - subjectType: subject.type, - type, - rate, - epStatus, - volStatus, - comment, - private: priv, - tag: tags?.join(' '), - }; - // await db.transaction(async (t) => { - // const [interest] = await t - // .select() - // .from(schema.chiiSubjectInterests) - // .where( - // op.and( - // op.eq(schema.chiiSubjectInterests.uid, auth.userID), - // op.eq(schema.chiiSubjectInterests.subjectID, subjectID), - // ), - // ) - // .limit(1); - // if (interest) { - // await t - // .update(schema.chiiSubjectInterests) - // .set({ - // type, - // rate, - // epStatus, - // volStatus, - // comment, - // private: priv, - // tag: tags?.join(' '), - // }) - // .where(op.eq(schema.chiiSubjectInterests.id, interest.id)); - // } else { - // await t.insert(schema.chiiSubjectInterests).values({ - // uid: auth.userID, - // subjectID, - // subjectType: subject.type, - // type, - // rate, - // epStatus, - // volStatus, - // comment, - // private: priv, - // tag: tags?.join(' '), - // }); - // } - // }); - // const interest = await fetcher.fetchSubjectInterest(auth.userID, subjectID); - // TODO: + // TODO: 插入 tag 并生成 tag 字符串 + // if (Censor::isNeedReview($_POST['tags'])) { + // $interest_tag = ''; + // } else { + // $interest_tag = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); + // TagCore::UpdateSubjectTags($subject['subject_id']); + // } + + let interestID = 0; + let interestTypeUpdated = false; + await db.transaction(async (t) => { + let needUpdateRate = false; + + const [subject] = await t + .select() + .from(schema.chiiSubjects) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + if (!subject) { + throw new NotFoundError(`subject ${subjectID}`); + } + const [interest] = await t + .select() + .from(schema.chiiSubjectInterests) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) + .limit(1); + if (interest) { + interestID = interest.id; + const oldType = interest.type; + const oldRate = interest.rate; + const oldPrivacy = interest.private; + const toUpdate: Partial = {}; + if (type && oldType !== type) { + interestTypeUpdated = true; + const now = DateTime.now().toUnixInteger(); + toUpdate.type = type; + toUpdate.updatedAt = now; + toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; + //若收藏类型改变,则更新数据 + await t + .update(schema.chiiSubjects) + .set({ + [getCollectionTypeField(type)]: increment( + schema.chiiSubjects[getCollectionTypeField(type)], + ), + [getCollectionTypeField(oldType)]: decrement( + schema.chiiSubjects[getCollectionTypeField(oldType)], + ), + }) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + } + if (rate && oldRate !== rate) { + needUpdateRate = true; + toUpdate.rate = rate; + } + if (epStatus !== undefined) { + toUpdate.epStatus = epStatus; + } + if (volStatus !== undefined) { + toUpdate.volStatus = volStatus; + } + if (comment !== undefined) { + toUpdate.comment = comment; + } + if (oldPrivacy !== privacy) { + needUpdateRate = true; + toUpdate.private = privacy; + } + if (tags !== undefined) { + toUpdate.tag = tags.join(' '); + } + if (Object.keys(toUpdate).length > 0) { + await t + .update(schema.chiiSubjectInterests) + .set(toUpdate) + .where(op.eq(schema.chiiSubjectInterests.id, interestID)) + .limit(1); + } + } else { + const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values({ + uid: auth.userID, + subjectID, + subjectType: slimSubject.type, + rate: rate ?? 0, + type: type ?? 0, + hasComment: comment ? 1 : 0, + comment: comment ?? '', + tag: tags?.join(' ') ?? '', + epStatus: epStatus ?? 0, + volStatus: volStatus ?? 0, + wishDateline: 0, + doingDateline: 0, + collectDateline: 0, + onHoldDateline: 0, + droppedDateline: 0, + createIp: auth.ip, + updateIp: auth.ip, + updatedAt: DateTime.now().toUnixInteger(), + private: privacy, + }); + interestID = insertId; + interestTypeUpdated = true; + if (rate) { + needUpdateRate = true; + } + // 收藏计数+1 + if (type) { + const field = getCollectionTypeField(type) as keyof orm.ISubject; + await t + .update(schema.chiiSubjects) + .set({ + [field]: increment(schema.chiiSubjects[field]), + }) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + } + } + + if (needUpdateRate) { + // await updateSubjectRating(); + } + }); + + // $db->memDelete(MC_SBJ_COLLECT . $subject['subject_id']); // 更新条目收藏缓存 + // $db->memDelete(MC_COLLECT_PSN . $uid); //删除用户收藏统计缓存 + // $db->memDelete(MC_USR_ALL_COLLECT . $uid); + // TagCore::cleanUserCollectTagCache($uid, $subject['subject_type_id'], $interest_type['id'], $interest_info['interest_type']); + // CacheCore::cleanWatchingListCache($uid); + + // 插入时间线 + if (interestTypeUpdated && privacy === CollectionPrivacy.Public) { + // TODO: + } }, ); diff --git a/routes/private/routes/user.ts b/routes/private/routes/user.ts index be95bc8d8..a9c54e06f 100644 --- a/routes/private/routes/user.ts +++ b/routes/private/routes/user.ts @@ -4,7 +4,7 @@ import { db, op } from '@app/drizzle/db.ts'; import * as schema from '@app/drizzle/schema'; import { NotFoundError } from '@app/lib/error.ts'; import { Security, Tag } from '@app/lib/openapi/index.ts'; -import { PersonType } from '@app/lib/subject/type.ts'; +import { CollectionPrivacy, PersonType } from '@app/lib/subject/type.ts'; import { fetchTimelineByIDs } from '@app/lib/timeline/item.ts'; import { getTimelineUser } from '@app/lib/timeline/user'; import * as convert from '@app/lib/types/convert.ts'; @@ -234,7 +234,7 @@ export async function setup(app: App) { : op.ne(schema.chiiSubjectInterests.type, 0), op.ne(schema.chiiSubjects.ban, 1), op.eq(schema.chiiSubjectFields.fieldRedirect, 0), - op.eq(schema.chiiSubjectInterests.private, false), + op.eq(schema.chiiSubjectInterests.private, CollectionPrivacy.Public), auth.allowNsfw ? undefined : op.eq(schema.chiiSubjects.nsfw, false), ); diff --git a/routes/schemas.ts b/routes/schemas.ts index 465efa3b3..c64cc7e72 100644 --- a/routes/schemas.ts +++ b/routes/schemas.ts @@ -65,6 +65,7 @@ export function addSchemas(app: App) { app.addSchema(res.SubjectComment); app.addSchema(res.SubjectImages); app.addSchema(res.SubjectInterest); + app.addSchema(res.SubjectInterestPrivacy); app.addSchema(res.SubjectPlatform); app.addSchema(res.SubjectPosition); app.addSchema(res.SubjectPositionStaff); From c728c57050c9bd0f41eb5aefc404eff4d899e9ed Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:34:35 +0800 Subject: [PATCH 07/65] z --- routes/private/routes/subject.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 2713b0fad..2f841a70b 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -834,7 +834,7 @@ export async function setup(app: App) { .where(op.eq(schema.chiiSubjects.id, subjectID)) .limit(1); if (!subject) { - throw new NotFoundError(`subject ${subjectID}`); + throw new UnexpectedNotFoundError(`subject ${subjectID}`); } const [interest] = await t .select() @@ -900,12 +900,16 @@ export async function setup(app: App) { .limit(1); } } else { - const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values({ + if (!type) { + throw new BadRequestError('type is required on new subject interest'); + } + const now = DateTime.now().toUnixInteger(); + const toInsert: typeof schema.chiiSubjectInterests.$inferInsert = { uid: auth.userID, subjectID, subjectType: slimSubject.type, rate: rate ?? 0, - type: type ?? 0, + type, hasComment: comment ? 1 : 0, comment: comment ?? '', tag: tags?.join(' ') ?? '', @@ -920,7 +924,9 @@ export async function setup(app: App) { updateIp: auth.ip, updatedAt: DateTime.now().toUnixInteger(), private: privacy, - }); + }; + toInsert[`${getCollectionTypeField(type)}Dateline`] = DateTime.now().toUnixInteger(); + const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); interestID = insertId; interestTypeUpdated = true; if (rate) { From 618d8e0fb47defda20857ade20eb4c6bb538f6ee Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:34:54 +0800 Subject: [PATCH 08/65] z --- routes/private/routes/subject.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 2f841a70b..34cda2698 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -922,10 +922,10 @@ export async function setup(app: App) { droppedDateline: 0, createIp: auth.ip, updateIp: auth.ip, - updatedAt: DateTime.now().toUnixInteger(), + updatedAt: now, private: privacy, }; - toInsert[`${getCollectionTypeField(type)}Dateline`] = DateTime.now().toUnixInteger(); + toInsert[`${getCollectionTypeField(type)}Dateline`] = now; const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); interestID = insertId; interestTypeUpdated = true; From 7d892edaaa70ee14bb97c527dd007ee1cc0d0172 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:37:58 +0800 Subject: [PATCH 09/65] z --- routes/private/routes/subject.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 34cda2698..964af0fe9 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -796,10 +796,7 @@ export async function setup(app: App) { if (!slimSubject) { throw new NotFoundError(`subject ${subjectID}`); } - let privacy = CollectionPrivacy.Public; - if (priv !== undefined) { - privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; - } + let privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; if (comment) { if (!Dam.allCharacterPrintable(comment)) { throw new BadRequestError('comment contains invalid invisible character'); From d2b59b31adb5c5d62b3843b9ed30d3ab09964978 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:48:59 +0800 Subject: [PATCH 10/65] z --- routes/private/routes/subject.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 964af0fe9..780be7c5a 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -810,15 +810,25 @@ export async function setup(app: App) { } } - await rateLimit(LimitAction.Subject, auth.userID); + tags = tags?.map((t) => t.trim().normalize('NFKC')); + if (tags !== undefined) { + if (tags.length > 10) { + throw new BadRequestError('too many tags'); + } + if (dam.needReview(tags.join(' '))) { + tags = undefined; + } else { + for (const tag of tags) { + if (tag.length < 2) { + throw new BadRequestError('tag too short'); + } + } + // 插入 tag 并生成 tag 字符串 + // tags = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); + } + } - // TODO: 插入 tag 并生成 tag 字符串 - // if (Censor::isNeedReview($_POST['tags'])) { - // $interest_tag = ''; - // } else { - // $interest_tag = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); - // TagCore::UpdateSubjectTags($subject['subject_id']); - // } + await rateLimit(LimitAction.Subject, auth.userID); let interestID = 0; let interestTypeUpdated = false; From 1ce779775a0d271b8bfebe39bf8503e2297ca09b Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 2 Feb 2025 21:51:10 +0800 Subject: [PATCH 11/65] z --- routes/private/routes/subject.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index 780be7c5a..91c7e6991 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -809,7 +809,6 @@ export async function setup(app: App) { privacy = CollectionPrivacy.Ban; } } - tags = tags?.map((t) => t.trim().normalize('NFKC')); if (tags !== undefined) { if (tags.length > 10) { From fb58128e3f8e5ff6200fc5f780c7fbbd24797d56 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Tue, 4 Feb 2025 11:07:01 +0800 Subject: [PATCH 12/65] z --- drizzle/schema.ts | 30 ++++++++++++++--------------- lib/subject/utils.ts | 13 +++++++++++++ lib/types/convert.ts | 26 ++++++++++++------------- lib/types/fetcher.ts | 4 ++-- routes/private/routes/collection.ts | 2 +- routes/private/routes/user.ts | 2 +- 6 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 lib/subject/utils.ts diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 4dab26507..6280cf3a6 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -512,25 +512,25 @@ export const chiiSubjects = mysqlTable('chii_subjects', { export const chiiSubjectFields = mysqlTable('chii_subject_fields', { id: mediumint('field_sid').autoincrement().notNull(), - fieldTid: smallint('field_tid').notNull(), - fieldTags: mediumtext('field_tags').notNull(), - fieldRate1: mediumint('field_rate_1').notNull(), - fieldRate2: mediumint('field_rate_2').notNull(), - fieldRate3: mediumint('field_rate_3').notNull(), - fieldRate4: mediumint('field_rate_4').notNull(), - fieldRate5: mediumint('field_rate_5').notNull(), - fieldRate6: mediumint('field_rate_6').notNull(), - fieldRate7: mediumint('field_rate_7').notNull(), - fieldRate8: mediumint('field_rate_8').notNull(), - fieldRate9: mediumint('field_rate_9').notNull(), - fieldRate10: mediumint('field_rate_10').notNull(), - fieldAirtime: tinyint('field_airtime').notNull(), - fieldRank: int('field_rank').default(0).notNull(), + tid: smallint('field_tid').notNull(), + tags: mediumtext('field_tags').notNull(), + rate1: mediumint('field_rate_1').notNull(), + rate2: mediumint('field_rate_2').notNull(), + rate3: mediumint('field_rate_3').notNull(), + rate4: mediumint('field_rate_4').notNull(), + rate5: mediumint('field_rate_5').notNull(), + rate6: mediumint('field_rate_6').notNull(), + rate7: mediumint('field_rate_7').notNull(), + rate8: mediumint('field_rate_8').notNull(), + rate9: mediumint('field_rate_9').notNull(), + rate10: mediumint('field_rate_10').notNull(), + airtime: tinyint('field_airtime').notNull(), + rank: int('field_rank').default(0).notNull(), year: year('field_year').notNull(), month: tinyint('field_mon').notNull(), weekDay: tinyint('field_week_day').notNull(), date: date('field_date', { mode: 'string' }).notNull(), - fieldRedirect: mediumint('field_redirect').notNull(), + redirect: mediumint('field_redirect').notNull(), }); export const chiiSubjectAlias = mysqlTable('chii_subject_alias', { diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts new file mode 100644 index 000000000..ac8e388f7 --- /dev/null +++ b/lib/subject/utils.ts @@ -0,0 +1,13 @@ +// import { db, op } from '@app/drizzle/db.ts'; +// import * as schema from '@app/drizzle/schema'; + +// export async function updateSubjectRating( +// subjectID: number, +// oldRate: number | undefined, +// newRate: number | undefined, +// ) { +// // TODO: +// } +export async function updateSubjectRating() { + // TODO: +} diff --git a/lib/types/convert.ts b/lib/types/convert.ts index cb4dd64d5..80d503712 100644 --- a/lib/types/convert.ts +++ b/lib/types/convert.ts @@ -239,21 +239,21 @@ function toSubjectPlatform(subject: orm.ISubject): res.ISubjectPlatform { function toSubjectRating(fields: orm.ISubjectFields): res.ISubjectRating { const ratingCount = [ - fields.fieldRate1, - fields.fieldRate2, - fields.fieldRate3, - fields.fieldRate4, - fields.fieldRate5, - fields.fieldRate6, - fields.fieldRate7, - fields.fieldRate8, - fields.fieldRate9, - fields.fieldRate10, + fields.rate1, + fields.rate2, + fields.rate3, + fields.rate4, + fields.rate5, + fields.rate6, + fields.rate7, + fields.rate8, + fields.rate9, + fields.rate10, ]; const total = ratingCount.reduce((a, b) => a + b, 0); const totalScore = ratingCount.reduce((a, b, i) => a + b * (i + 1), 0); const rating = { - rank: fields.fieldRank, + rank: fields.rank, total: total, score: total === 0 ? 0 : Math.round((totalScore * 100) / total) / 100, count: ratingCount, @@ -293,13 +293,13 @@ export function toSubject(subject: orm.ISubject, fields: orm.ISubjectFields): re nsfw: subject.nsfw, platform: toSubjectPlatform(subject), rating: toSubjectRating(fields), - redirect: fields.fieldRedirect, + redirect: fields.redirect, series: subject.series, seriesEntry: subject.seriesEntry, summary: subject.summary, type: subject.typeID, volumes: subject.volumes, - tags: toSubjectTags(fields.fieldTags), + tags: toSubjectTags(fields.tags), }; } diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index 28577632d..66e6f5d17 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -340,8 +340,8 @@ export async function fetchSubjectIDsByFilter( const sorts = []; switch (sort) { case SubjectSort.Rank: { - conditions.push(op.ne(schema.chiiSubjectFields.fieldRank, 0)); - sorts.push(op.asc(schema.chiiSubjectFields.fieldRank)); + conditions.push(op.ne(schema.chiiSubjectFields.rank, 0)); + sorts.push(op.asc(schema.chiiSubjectFields.rank)); break; } case SubjectSort.Trends: { diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 942561388..166029b64 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -44,7 +44,7 @@ export async function setup(app: App) { : op.ne(schema.chiiSubjectInterests.type, 0), since ? op.gte(schema.chiiSubjectInterests.updatedAt, since) : undefined, op.ne(schema.chiiSubjects.ban, 1), - op.eq(schema.chiiSubjectFields.fieldRedirect, 0), + op.eq(schema.chiiSubjectFields.redirect, 0), auth.allowNsfw ? undefined : op.eq(schema.chiiSubjects.nsfw, false), ); diff --git a/routes/private/routes/user.ts b/routes/private/routes/user.ts index a9c54e06f..fae1e266d 100644 --- a/routes/private/routes/user.ts +++ b/routes/private/routes/user.ts @@ -233,7 +233,7 @@ export async function setup(app: App) { ? op.eq(schema.chiiSubjectInterests.type, type) : op.ne(schema.chiiSubjectInterests.type, 0), op.ne(schema.chiiSubjects.ban, 1), - op.eq(schema.chiiSubjectFields.fieldRedirect, 0), + op.eq(schema.chiiSubjectFields.redirect, 0), op.eq(schema.chiiSubjectInterests.private, CollectionPrivacy.Public), auth.allowNsfw ? undefined : op.eq(schema.chiiSubjects.nsfw, false), ); From a670bc198a39c45ed1082434a088bb8b4fd130cb Mon Sep 17 00:00:00 2001 From: everpcpc Date: Tue, 4 Feb 2025 19:12:52 +0800 Subject: [PATCH 13/65] z --- routes/private/routes/collection.ts | 209 +++++++++++++++++++++++++++- routes/private/routes/subject.ts | 201 +------------------------- 2 files changed, 209 insertions(+), 201 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 166029b64..ebb882e77 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -1,13 +1,21 @@ import { Type as t } from '@sinclair/typebox'; +import { DateTime } from 'luxon'; -import { db, op } from '@app/drizzle/db.ts'; +import { db, decrement, increment, op } from '@app/drizzle/db.ts'; +import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; +import { Dam, dam } from '@app/lib/dam'; +import { BadRequestError, UnexpectedNotFoundError } from '@app/lib/error'; +import { NotFoundError } from '@app/lib/error'; import { Security, Tag } from '@app/lib/openapi/index.ts'; -import { PersonType } from '@app/lib/subject/type.ts'; +import { CollectionPrivacy, getCollectionTypeField, PersonType } from '@app/lib/subject/type.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 { LimitAction } from '@app/lib/utils/rate-limit'; import { requireLogin } from '@app/routes/hooks/pre-handler.ts'; +import { rateLimit } from '@app/routes/hooks/rate-limit'; import type { App } from '@app/routes/type.ts'; // eslint-disable-next-line @typescript-eslint/require-await @@ -93,6 +101,203 @@ export async function setup(app: App) { }, ); + app.put( + '/collections/subjects/:subjectID', + { + schema: { + summary: '新增或修改条目收藏', + operationId: 'updateSubjectCollection', + tags: [Tag.Collection], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + subjectID: t.Integer(), + }), + body: req.Ref(req.CollectSubject), + }, + preHandler: [requireLogin('update subject collection')], + }, + async ({ + auth, + params: { subjectID }, + body: { type, rate, epStatus, volStatus, comment, priv, tags }, + }) => { + const slimSubject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + if (!slimSubject) { + throw new NotFoundError(`subject ${subjectID}`); + } + let privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; + if (comment) { + if (!Dam.allCharacterPrintable(comment)) { + throw new BadRequestError('comment contains invalid invisible character'); + } + comment = comment.normalize('NFC'); + if (comment.length > 380) { + throw new BadRequestError('comment too long'); + } + if (dam.needReview(comment) || auth.permission.ban_post) { + privacy = CollectionPrivacy.Ban; + } + } + tags = tags?.map((t) => t.trim().normalize('NFKC')); + if (tags !== undefined) { + if (tags.length > 10) { + throw new BadRequestError('too many tags'); + } + if (dam.needReview(tags.join(' '))) { + tags = undefined; + } else { + for (const tag of tags) { + if (tag.length < 2) { + throw new BadRequestError('tag too short'); + } + } + // 插入 tag 并生成 tag 字符串 + // tags = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); + } + } + + await rateLimit(LimitAction.Subject, auth.userID); + + let interestID = 0; + let interestTypeUpdated = false; + await db.transaction(async (t) => { + let needUpdateRate = false; + + const [subject] = await t + .select() + .from(schema.chiiSubjects) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + if (!subject) { + throw new UnexpectedNotFoundError(`subject ${subjectID}`); + } + const [interest] = await t + .select() + .from(schema.chiiSubjectInterests) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) + .limit(1); + if (interest) { + interestID = interest.id; + const oldType = interest.type; + const oldRate = interest.rate; + const oldPrivacy = interest.privacy; + const toUpdate: Partial = {}; + if (type && oldType !== type) { + interestTypeUpdated = true; + const now = DateTime.now().toUnixInteger(); + toUpdate.type = type; + toUpdate.updatedAt = now; + toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; + //若收藏类型改变,则更新数据 + await t + .update(schema.chiiSubjects) + .set({ + [getCollectionTypeField(type)]: increment( + schema.chiiSubjects[getCollectionTypeField(type)], + ), + [getCollectionTypeField(oldType)]: decrement( + schema.chiiSubjects[getCollectionTypeField(oldType)], + ), + }) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + } + if (rate && oldRate !== rate) { + needUpdateRate = true; + toUpdate.rate = rate; + } + if (epStatus !== undefined) { + toUpdate.epStatus = epStatus; + } + if (volStatus !== undefined) { + toUpdate.volStatus = volStatus; + } + if (comment !== undefined) { + toUpdate.comment = comment; + } + if (oldPrivacy !== privacy) { + needUpdateRate = true; + toUpdate.privacy = privacy; + } + if (tags !== undefined) { + toUpdate.tag = tags.join(' '); + } + if (Object.keys(toUpdate).length > 0) { + await t + .update(schema.chiiSubjectInterests) + .set(toUpdate) + .where(op.eq(schema.chiiSubjectInterests.id, interestID)) + .limit(1); + } + } else { + if (!type) { + throw new BadRequestError('type is required on new subject interest'); + } + const now = DateTime.now().toUnixInteger(); + const toInsert: typeof schema.chiiSubjectInterests.$inferInsert = { + uid: auth.userID, + subjectID, + subjectType: slimSubject.type, + rate: rate ?? 0, + type, + hasComment: comment ? 1 : 0, + comment: comment ?? '', + tag: tags?.join(' ') ?? '', + epStatus: epStatus ?? 0, + volStatus: volStatus ?? 0, + wishDateline: 0, + doingDateline: 0, + collectDateline: 0, + onHoldDateline: 0, + droppedDateline: 0, + createIp: auth.ip, + updateIp: auth.ip, + updatedAt: now, + privacy: privacy, + }; + toInsert[`${getCollectionTypeField(type)}Dateline`] = now; + const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); + interestID = insertId; + interestTypeUpdated = true; + if (rate) { + needUpdateRate = true; + } + // 收藏计数+1 + if (type) { + const field = getCollectionTypeField(type) as keyof orm.ISubject; + await t + .update(schema.chiiSubjects) + .set({ + [field]: increment(schema.chiiSubjects[field]), + }) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); + } + } + + if (needUpdateRate) { + // await updateSubjectRating(); + } + }); + + // $db->memDelete(MC_SBJ_COLLECT . $subject['subject_id']); // 更新条目收藏缓存 + // $db->memDelete(MC_COLLECT_PSN . $uid); //删除用户收藏统计缓存 + // $db->memDelete(MC_USR_ALL_COLLECT . $uid); + // TagCore::cleanUserCollectTagCache($uid, $subject['subject_type_id'], $interest_type['id'], $interest_info['interest_type']); + // CacheCore::cleanWatchingListCache($uid); + + // 插入时间线 + if (interestTypeUpdated && privacy === CollectionPrivacy.Public) { + // TODO: + } + }, + ); + app.get( '/collections/characters', { diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index b8df0da1d..e45a015b1 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -1,7 +1,7 @@ import { Type as t } from '@sinclair/typebox'; import { DateTime } from 'luxon'; -import { db, decrement, increment, op } from '@app/drizzle/db.ts'; +import { db, op } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { NotAllowedError } from '@app/lib/auth/index.ts'; @@ -17,7 +17,7 @@ import * as Notify from '@app/lib/notify.ts'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { turnstile } from '@app/lib/services/turnstile.ts'; import type { SubjectFilter, SubjectSort } from '@app/lib/subject/type.ts'; -import { CollectionPrivacy, getCollectionTypeField } from '@app/lib/subject/type.ts'; +import { CollectionPrivacy } from '@app/lib/subject/type.ts'; import { CanViewTopicContent, CanViewTopicReply } from '@app/lib/topic/display.ts'; import { canEditTopic, canReplyPost } from '@app/lib/topic/state'; import { CommentState, TopicDisplay } from '@app/lib/topic/type.ts'; @@ -768,203 +768,6 @@ export async function setup(app: App) { }, ); - app.post( - '/subjects/:subjectID/collect', - { - schema: { - summary: '新增或修改条目收藏', - operationId: 'collectSubject', - tags: [Tag.Subject], - security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], - params: t.Object({ - subjectID: t.Integer(), - }), - body: req.Ref(req.CollectSubject), - }, - preHandler: [requireLogin('collecting a subject')], - }, - async ({ - auth, - params: { subjectID }, - body: { type, rate, epStatus, volStatus, comment, priv, tags }, - }) => { - const slimSubject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); - if (!slimSubject) { - throw new NotFoundError(`subject ${subjectID}`); - } - let privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; - if (comment) { - if (!Dam.allCharacterPrintable(comment)) { - throw new BadRequestError('comment contains invalid invisible character'); - } - comment = comment.normalize('NFC'); - if (comment.length > 380) { - throw new BadRequestError('comment too long'); - } - if (dam.needReview(comment) || auth.permission.ban_post) { - privacy = CollectionPrivacy.Ban; - } - } - tags = tags?.map((t) => t.trim().normalize('NFKC')); - if (tags !== undefined) { - if (tags.length > 10) { - throw new BadRequestError('too many tags'); - } - if (dam.needReview(tags.join(' '))) { - tags = undefined; - } else { - for (const tag of tags) { - if (tag.length < 2) { - throw new BadRequestError('tag too short'); - } - } - // 插入 tag 并生成 tag 字符串 - // tags = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); - } - } - - await rateLimit(LimitAction.Subject, auth.userID); - - let interestID = 0; - let interestTypeUpdated = false; - await db.transaction(async (t) => { - let needUpdateRate = false; - - const [subject] = await t - .select() - .from(schema.chiiSubjects) - .where(op.eq(schema.chiiSubjects.id, subjectID)) - .limit(1); - if (!subject) { - throw new UnexpectedNotFoundError(`subject ${subjectID}`); - } - const [interest] = await t - .select() - .from(schema.chiiSubjectInterests) - .where( - op.and( - op.eq(schema.chiiSubjectInterests.uid, auth.userID), - op.eq(schema.chiiSubjectInterests.subjectID, subjectID), - ), - ) - .limit(1); - if (interest) { - interestID = interest.id; - const oldType = interest.type; - const oldRate = interest.rate; - const oldPrivacy = interest.privacy; - const toUpdate: Partial = {}; - if (type && oldType !== type) { - interestTypeUpdated = true; - const now = DateTime.now().toUnixInteger(); - toUpdate.type = type; - toUpdate.updatedAt = now; - toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; - //若收藏类型改变,则更新数据 - await t - .update(schema.chiiSubjects) - .set({ - [getCollectionTypeField(type)]: increment( - schema.chiiSubjects[getCollectionTypeField(type)], - ), - [getCollectionTypeField(oldType)]: decrement( - schema.chiiSubjects[getCollectionTypeField(oldType)], - ), - }) - .where(op.eq(schema.chiiSubjects.id, subjectID)) - .limit(1); - } - if (rate && oldRate !== rate) { - needUpdateRate = true; - toUpdate.rate = rate; - } - if (epStatus !== undefined) { - toUpdate.epStatus = epStatus; - } - if (volStatus !== undefined) { - toUpdate.volStatus = volStatus; - } - if (comment !== undefined) { - toUpdate.comment = comment; - } - if (oldPrivacy !== privacy) { - needUpdateRate = true; - toUpdate.privacy = privacy; - } - if (tags !== undefined) { - toUpdate.tag = tags.join(' '); - } - if (Object.keys(toUpdate).length > 0) { - await t - .update(schema.chiiSubjectInterests) - .set(toUpdate) - .where(op.eq(schema.chiiSubjectInterests.id, interestID)) - .limit(1); - } - } else { - if (!type) { - throw new BadRequestError('type is required on new subject interest'); - } - const now = DateTime.now().toUnixInteger(); - const toInsert: typeof schema.chiiSubjectInterests.$inferInsert = { - uid: auth.userID, - subjectID, - subjectType: slimSubject.type, - rate: rate ?? 0, - type, - hasComment: comment ? 1 : 0, - comment: comment ?? '', - tag: tags?.join(' ') ?? '', - epStatus: epStatus ?? 0, - volStatus: volStatus ?? 0, - wishDateline: 0, - doingDateline: 0, - collectDateline: 0, - onHoldDateline: 0, - droppedDateline: 0, - createIp: auth.ip, - updateIp: auth.ip, - updatedAt: now, - privacy: privacy, - }; - toInsert[`${getCollectionTypeField(type)}Dateline`] = now; - const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); - interestID = insertId; - interestTypeUpdated = true; - if (rate) { - needUpdateRate = true; - } - // 收藏计数+1 - if (type) { - const field = getCollectionTypeField(type) as keyof orm.ISubject; - await t - .update(schema.chiiSubjects) - .set({ - [field]: increment(schema.chiiSubjects[field]), - }) - .where(op.eq(schema.chiiSubjects.id, subjectID)) - .limit(1); - } - } - - if (needUpdateRate) { - // await updateSubjectRating(); - } - }); - - // $db->memDelete(MC_SBJ_COLLECT . $subject['subject_id']); // 更新条目收藏缓存 - // $db->memDelete(MC_COLLECT_PSN . $uid); //删除用户收藏统计缓存 - // $db->memDelete(MC_USR_ALL_COLLECT . $uid); - // TagCore::cleanUserCollectTagCache($uid, $subject['subject_type_id'], $interest_type['id'], $interest_info['interest_type']); - // CacheCore::cleanWatchingListCache($uid); - - // 插入时间线 - if (interestTypeUpdated && privacy === CollectionPrivacy.Public) { - // TODO: - } - }, - ); - app.get( '/subjects/:subjectID/topics', { From cc36e5d7e8887cda8f8810f04ac987148501a27c Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 21:47:03 +0800 Subject: [PATCH 14/65] z --- routes/private/routes/collection.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index beda122c9..08e27aa56 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -9,6 +9,7 @@ import { BadRequestError, UnexpectedNotFoundError } from '@app/lib/error'; import { NotFoundError } from '@app/lib/error'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { CollectionPrivacy, getCollectionTypeField, PersonType } from '@app/lib/subject/type.ts'; +import { TimelineWriter } from '@app/lib/timeline/writer'; 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'; @@ -293,7 +294,9 @@ export async function setup(app: App) { // 插入时间线 if (interestTypeUpdated && privacy === CollectionPrivacy.Public) { - // TODO: + await TimelineWriter.subject(auth.userID, subjectID); + } else if (epStatus !== undefined || volStatus !== undefined) { + await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); } }, ); From e85b77d9e125e6bb6542447e9f725b27d3262405 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 21:52:46 +0800 Subject: [PATCH 15/65] z --- routes/private/routes/collection.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 08e27aa56..1367a9f86 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -126,7 +126,10 @@ export async function setup(app: App) { if (!slimSubject) { throw new NotFoundError(`subject ${subjectID}`); } - let privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; + let privacy: CollectionPrivacy | undefined; + if (priv !== undefined) { + privacy = priv ? CollectionPrivacy.Private : CollectionPrivacy.Public; + } if (comment) { if (!Dam.allCharacterPrintable(comment)) { throw new BadRequestError('comment contains invalid invisible character'); @@ -187,6 +190,9 @@ export async function setup(app: App) { const oldType = interest.type; const oldRate = interest.rate; const oldPrivacy = interest.privacy; + if (privacy === undefined) { + privacy = oldPrivacy; + } const toUpdate: Partial = {}; if (type && oldType !== type) { interestTypeUpdated = true; @@ -239,6 +245,9 @@ export async function setup(app: App) { if (!type) { throw new BadRequestError('type is required on new subject interest'); } + if (privacy === undefined) { + privacy = CollectionPrivacy.Public; + } const now = DateTime.now().toUnixInteger(); const toInsert: typeof schema.chiiSubjectInterests.$inferInsert = { uid: auth.userID, @@ -293,10 +302,12 @@ export async function setup(app: App) { // CacheCore::cleanWatchingListCache($uid); // 插入时间线 - if (interestTypeUpdated && privacy === CollectionPrivacy.Public) { - await TimelineWriter.subject(auth.userID, subjectID); - } else if (epStatus !== undefined || volStatus !== undefined) { - await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + if (privacy === CollectionPrivacy.Public) { + if (interestTypeUpdated) { + await TimelineWriter.subject(auth.userID, subjectID); + } else if (epStatus !== undefined || volStatus !== undefined) { + await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + } } }, ); From 4416ed04c0161119a02fc6c144aaf6d93c09760d Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 22:07:11 +0800 Subject: [PATCH 16/65] z --- lib/types/req.ts | 15 ++++-- routes/private/routes/collection.ts | 82 +++++++++++++++++++++-------- routes/schemas.ts | 1 + 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/lib/types/req.ts b/lib/types/req.ts index e6c87fbae..fcca7853c 100644 --- a/lib/types/req.ts +++ b/lib/types/req.ts @@ -1,7 +1,7 @@ import type { Static } from '@sinclair/typebox'; -import { Ref, Type as t } from '@sinclair/typebox'; +import { Type as t } from '@sinclair/typebox'; -import { CollectionType } from '@app/lib/types/common.ts'; +import { CollectionType, Ref } from '@app/lib/types/common.ts'; export * from '@app/lib/types/common.ts'; @@ -117,11 +117,18 @@ export const CollectSubject = t.Object( { type: t.Optional(Ref(CollectionType)), rate: t.Optional(t.Integer({ minimum: 0, maximum: 10, description: '评分,0 表示删除评分' })), - epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), - volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), comment: t.Optional(t.String({ description: '评价' })), priv: t.Optional(t.Boolean({ description: '仅自己可见' })), tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), }, { $id: 'CollectSubject' }, ); + +export type IUpdateSubjectProgress = Static; +export const UpdateSubjectProgress = t.Object( + { + epStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目章节进度' })), + volStatus: t.Optional(t.Integer({ minimum: 0, description: '书籍条目卷数进度' })), + }, + { $id: 'UpdateSubjectProgress' }, +); diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 1367a9f86..1c55affef 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -8,7 +8,12 @@ import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, UnexpectedNotFoundError } from '@app/lib/error'; import { NotFoundError } from '@app/lib/error'; import { Security, Tag } from '@app/lib/openapi/index.ts'; -import { CollectionPrivacy, getCollectionTypeField, PersonType } from '@app/lib/subject/type.ts'; +import { + CollectionPrivacy, + getCollectionTypeField, + PersonType, + SubjectType, +} from '@app/lib/subject/type.ts'; import { TimelineWriter } from '@app/lib/timeline/writer'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -102,6 +107,55 @@ export async function setup(app: App) { }, ); + app.patch( + '/collections/subjects/:subjectID', + { + schema: { + summary: '修改条目进度', + operationId: 'updateSubjectProgress', + tags: [Tag.Collection], + security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], + params: t.Object({ + subjectID: t.Integer(), + }), + body: req.Ref(req.UpdateSubjectProgress), + }, + preHandler: [requireLogin('update subject progress')], + }, + async ({ auth, params: { subjectID }, body: { epStatus, volStatus } }) => { + const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + if (!subject) { + throw new NotFoundError(`subject ${subjectID}`); + } + switch (subject.type) { + case SubjectType.Book: + case SubjectType.Anime: + case SubjectType.Real: { + break; + } + default: { + throw new BadRequestError(`subject not supported for progress`); + } + } + const toUpdate: Partial = {}; + if (epStatus !== undefined) { + toUpdate.epStatus = epStatus; + } + if (volStatus !== undefined) { + toUpdate.volStatus = volStatus; + } + if (Object.keys(toUpdate).length === 0) { + throw new BadRequestError('no update'); + } + await db + .update(schema.chiiSubjectInterests) + .set(toUpdate) + .where(op.eq(schema.chiiSubjectInterests.subjectID, subjectID)) + .limit(1); + await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + }, + ); + app.put( '/collections/subjects/:subjectID', { @@ -117,11 +171,7 @@ export async function setup(app: App) { }, preHandler: [requireLogin('update subject collection')], }, - async ({ - auth, - params: { subjectID }, - body: { type, rate, epStatus, volStatus, comment, priv, tags }, - }) => { + async ({ auth, params: { subjectID }, body: { type, rate, comment, priv, tags } }) => { const slimSubject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); if (!slimSubject) { throw new NotFoundError(`subject ${subjectID}`); @@ -218,12 +268,6 @@ export async function setup(app: App) { needUpdateRate = true; toUpdate.rate = rate; } - if (epStatus !== undefined) { - toUpdate.epStatus = epStatus; - } - if (volStatus !== undefined) { - toUpdate.volStatus = volStatus; - } if (comment !== undefined) { toUpdate.comment = comment; } @@ -258,8 +302,8 @@ export async function setup(app: App) { hasComment: comment ? 1 : 0, comment: comment ?? '', tag: tags?.join(' ') ?? '', - epStatus: epStatus ?? 0, - volStatus: volStatus ?? 0, + epStatus: 0, + volStatus: 0, wishDateline: 0, doingDateline: 0, collectDateline: 0, @@ -268,7 +312,7 @@ export async function setup(app: App) { createIp: auth.ip, updateIp: auth.ip, updatedAt: now, - privacy: privacy, + privacy, }; toInsert[`${getCollectionTypeField(type)}Dateline`] = now; const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); @@ -302,12 +346,8 @@ export async function setup(app: App) { // CacheCore::cleanWatchingListCache($uid); // 插入时间线 - if (privacy === CollectionPrivacy.Public) { - if (interestTypeUpdated) { - await TimelineWriter.subject(auth.userID, subjectID); - } else if (epStatus !== undefined || volStatus !== undefined) { - await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); - } + if (privacy === CollectionPrivacy.Public && interestTypeUpdated) { + await TimelineWriter.subject(auth.userID, subjectID); } }, ); diff --git a/routes/schemas.ts b/routes/schemas.ts index 16ed2a63a..463f043a2 100644 --- a/routes/schemas.ts +++ b/routes/schemas.ts @@ -18,6 +18,7 @@ export function addSchemas(app: App) { app.addSchema(req.TurnstileToken); app.addSchema(req.UpdateContent); app.addSchema(req.UpdateTopic); + app.addSchema(req.UpdateSubjectProgress); app.addSchema(res.Avatar); app.addSchema(res.BlogEntry); From e33317542b8c9358cd87e5b9b939cb3a8ced2f3a Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 22:09:52 +0800 Subject: [PATCH 17/65] z --- routes/private/routes/collection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 1c55affef..b8d0f786d 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -147,6 +147,7 @@ export async function setup(app: App) { if (Object.keys(toUpdate).length === 0) { throw new BadRequestError('no update'); } + toUpdate.updatedAt = DateTime.now().toUnixInteger(); await db .update(schema.chiiSubjectInterests) .set(toUpdate) From 74aa259c3ec5587d437e51f2237ec1d49fdb6772 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 22:44:09 +0800 Subject: [PATCH 18/65] z --- lib/subject/utils.ts | 59 +++++++++++++++++++++++------ routes/private/routes/collection.ts | 14 +++++-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index ac8e388f7..09f1d125d 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -1,13 +1,50 @@ -// import { db, op } from '@app/drizzle/db.ts'; -// import * as schema from '@app/drizzle/schema'; +import { decr, incr, op, type Txn } from '@app/drizzle/db.ts'; +import type * as orm from '@app/drizzle/orm.ts'; +import * as schema from '@app/drizzle/schema'; -// export async function updateSubjectRating( -// subjectID: number, -// oldRate: number | undefined, -// newRate: number | undefined, -// ) { -// // TODO: -// } -export async function updateSubjectRating() { - // TODO: +function getField(rate: number) { + return [ + undefined, + 'rate1', + 'rate2', + 'rate3', + 'rate4', + 'rate5', + 'rate6', + 'rate7', + 'rate8', + 'rate9', + 'rate10', + ][rate]; +} + +/** 更新条目评分,需要在事务中执行 */ +export async function updateSubjectRating( + t: Txn, + subjectID: number, + oldRate: number, + newRate: number, +) { + if (oldRate === newRate) { + return; + } + const newField = getField(newRate); + const oldField = getField(oldRate); + const toUpdate: Record = {}; + if (newField) { + const field = newField as keyof orm.ISubjectFields; + toUpdate[field] = incr(schema.chiiSubjectFields[field]); + } + if (oldField) { + const field = oldField as keyof orm.ISubjectFields; + toUpdate[field] = decr(schema.chiiSubjectFields[field]); + } + if (Object.keys(toUpdate).length === 0) { + return; + } + await t + .update(schema.chiiSubjects) + .set(toUpdate) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); } diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index b8d0f786d..95847ec35 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -14,6 +14,7 @@ import { PersonType, SubjectType, } from '@app/lib/subject/type.ts'; +import { updateSubjectRating } from '@app/lib/subject/utils.ts'; import { TimelineWriter } from '@app/lib/timeline/writer'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -236,10 +237,14 @@ export async function setup(app: App) { ), ) .limit(1); + let oldRate = 0; + if (rate !== undefined) { + rate = 0; + } if (interest) { interestID = interest.id; + oldRate = interest.rate; const oldType = interest.type; - const oldRate = interest.rate; const oldPrivacy = interest.privacy; if (privacy === undefined) { privacy = oldPrivacy; @@ -265,7 +270,7 @@ export async function setup(app: App) { .where(op.eq(schema.chiiSubjects.id, subjectID)) .limit(1); } - if (rate && oldRate !== rate) { + if (oldRate !== rate) { needUpdateRate = true; toUpdate.rate = rate; } @@ -298,7 +303,7 @@ export async function setup(app: App) { uid: auth.userID, subjectID, subjectType: slimSubject.type, - rate: rate ?? 0, + rate, type, hasComment: comment ? 1 : 0, comment: comment ?? '', @@ -335,8 +340,9 @@ export async function setup(app: App) { } } + // 更新评分 if (needUpdateRate) { - // await updateSubjectRating(); + await updateSubjectRating(t, subjectID, oldRate, rate ?? 0); } }); From b14f7401a83af44fd9e632261e9e140160c33134 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Fri, 7 Feb 2025 22:45:40 +0800 Subject: [PATCH 19/65] z --- routes/__snapshots__/index.test.ts.snap | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index b68e20962..c58766a9b 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -189,6 +189,27 @@ exports[`should build private api spec 1`] = ` - subject - type type: object + CollectSubject: + properties: + comment: + description: 评价 + type: string + priv: + description: 仅自己可见 + type: boolean + rate: + description: 评分,0 表示删除评分 + maximum: 10 + minimum: 0 + type: integer + tags: + items: + description: 标签, 不能包含空格 + type: string + type: array + type: + $ref: '#/components/schemas/CollectionType' + type: object CollectionType: description: |- 条目收藏状态 @@ -2359,6 +2380,17 @@ exports[`should build private api spec 1`] = ` required: - content type: object + UpdateSubjectProgress: + properties: + epStatus: + description: 书籍条目章节进度 + minimum: 0 + type: integer + volStatus: + description: 书籍条目卷数进度 + minimum: 0 + type: integer + type: object UpdateTopic: properties: content: @@ -3676,6 +3708,61 @@ paths: summary: 获取当前用户的条目收藏 tags: - collection + /p1/collections/subjects/{subjectID}: + patch: + operationId: updateSubjectProgress + parameters: + - in: path + name: subjectID + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateSubjectProgress' + responses: + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 意料之外的服务器错误 + description: 意料之外的服务器错误 + security: + - CookiesSession: [] + HTTPBearer: [] + summary: 修改条目进度 + tags: + - collection + put: + operationId: updateSubjectCollection + parameters: + - in: path + name: subjectID + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CollectSubject' + responses: + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: 意料之外的服务器错误 + description: 意料之外的服务器错误 + security: + - CookiesSession: [] + HTTPBearer: [] + summary: 新增或修改条目收藏 + tags: + - collection /p1/debug: get: description: debug 路由 From 6ba2d27184c67e68bb8fca2c01730a86c2589163 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 08:56:44 +0800 Subject: [PATCH 20/65] z --- routes/private/routes/misc.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/routes/private/routes/misc.test.ts b/routes/private/routes/misc.test.ts index 62a04edd3..2045526f2 100644 --- a/routes/private/routes/misc.test.ts +++ b/routes/private/routes/misc.test.ts @@ -1,22 +1,23 @@ import { DateTime } from 'luxon'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { db, op } from '@app/drizzle/db.ts'; +import * as schema from '@app/drizzle/schema.ts'; import { emptyAuth } from '@app/lib/auth/index.ts'; import { Notify, NotifyType } from '@app/lib/notify.ts'; -import { NotifyFieldRepo, NotifyRepo } from '@app/lib/orm/index.ts'; import { createTestServer } from '@app/tests/utils.ts'; import { setup } from './misc.ts'; describe('notify', () => { beforeEach(async () => { - await NotifyRepo.delete({}); - await NotifyFieldRepo.delete({}); + await db.delete(schema.chiiNotify); + await db.delete(schema.chiiNotifyField); }); afterEach(async () => { - await NotifyRepo.delete({}); - await NotifyFieldRepo.delete({}); + await db.delete(schema.chiiNotify); + await db.delete(schema.chiiNotifyField); }); test('should list notify', async () => { From 59167b34828e11aa5846256b35671dced17e3c82 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 08:57:02 +0800 Subject: [PATCH 21/65] z --- routes/private/routes/collection.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 95847ec35..a2ed8aaa9 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -346,12 +346,6 @@ export async function setup(app: App) { } }); - // $db->memDelete(MC_SBJ_COLLECT . $subject['subject_id']); // 更新条目收藏缓存 - // $db->memDelete(MC_COLLECT_PSN . $uid); //删除用户收藏统计缓存 - // $db->memDelete(MC_USR_ALL_COLLECT . $uid); - // TagCore::cleanUserCollectTagCache($uid, $subject['subject_type_id'], $interest_type['id'], $interest_info['interest_type']); - // CacheCore::cleanWatchingListCache($uid); - // 插入时间线 if (privacy === CollectionPrivacy.Public && interestTypeUpdated) { await TimelineWriter.subject(auth.userID, subjectID); From 63400d8bb29323bad2a1650d39b856a9b37d4d1a Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 08:58:14 +0800 Subject: [PATCH 22/65] z --- routes/private/routes/collection.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index a2ed8aaa9..c2699b57b 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -7,6 +7,7 @@ import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, UnexpectedNotFoundError } from '@app/lib/error'; import { NotFoundError } from '@app/lib/error'; +import { logger } from '@app/lib/logger'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { CollectionPrivacy, @@ -154,7 +155,14 @@ export async function setup(app: App) { .set(toUpdate) .where(op.eq(schema.chiiSubjectInterests.subjectID, subjectID)) .limit(1); - await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + try { + await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + } catch (error) { + logger.error(`failed to write timeline for subject ${subjectID}`, { + error, + userID: auth.userID, + }); + } }, ); @@ -348,7 +356,14 @@ export async function setup(app: App) { // 插入时间线 if (privacy === CollectionPrivacy.Public && interestTypeUpdated) { - await TimelineWriter.subject(auth.userID, subjectID); + try { + await TimelineWriter.subject(auth.userID, subjectID); + } catch (error) { + logger.error(`failed to write timeline for subject ${subjectID}`, { + error, + userID: auth.userID, + }); + } } }, ); From 12a6c1fec57353d83a0beab89f407c86b926a5a0 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:10:59 +0800 Subject: [PATCH 23/65] z --- routes/private/routes/collection.ts | 32 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index c2699b57b..7acb82a7b 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -139,6 +139,19 @@ export async function setup(app: App) { throw new BadRequestError(`subject not supported for progress`); } } + const [interest] = await db + .select() + .from(schema.chiiSubjectInterests) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) + .limit(1); + if (!interest) { + throw new NotFoundError(`subject not collected`); + } const toUpdate: Partial = {}; if (epStatus !== undefined) { toUpdate.epStatus = epStatus; @@ -153,7 +166,12 @@ export async function setup(app: App) { await db .update(schema.chiiSubjectInterests) .set(toUpdate) - .where(op.eq(schema.chiiSubjectInterests.subjectID, subjectID)) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) .limit(1); try { await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); @@ -222,7 +240,6 @@ export async function setup(app: App) { await rateLimit(LimitAction.Subject, auth.userID); - let interestID = 0; let interestTypeUpdated = false; await db.transaction(async (t) => { let needUpdateRate = false; @@ -250,7 +267,6 @@ export async function setup(app: App) { rate = 0; } if (interest) { - interestID = interest.id; oldRate = interest.rate; const oldType = interest.type; const oldPrivacy = interest.privacy; @@ -296,7 +312,12 @@ export async function setup(app: App) { await t .update(schema.chiiSubjectInterests) .set(toUpdate) - .where(op.eq(schema.chiiSubjectInterests.id, interestID)) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) .limit(1); } } else { @@ -329,8 +350,7 @@ export async function setup(app: App) { privacy, }; toInsert[`${getCollectionTypeField(type)}Dateline`] = now; - const [{ insertId }] = await t.insert(schema.chiiSubjectInterests).values(toInsert); - interestID = insertId; + await t.insert(schema.chiiSubjectInterests).values(toInsert); interestTypeUpdated = true; if (rate) { needUpdateRate = true; From 3fedfc140ecc8000610c9e9a834c3d5dafa206ca Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:13:54 +0800 Subject: [PATCH 24/65] z --- routes/private/routes/collection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 7acb82a7b..0d6de02e0 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -349,7 +349,8 @@ export async function setup(app: App) { updatedAt: now, privacy, }; - toInsert[`${getCollectionTypeField(type)}Dateline`] = now; + const field = getCollectionTypeField(type); + toInsert[`${field}Dateline`] = now; await t.insert(schema.chiiSubjectInterests).values(toInsert); interestTypeUpdated = true; if (rate) { @@ -357,7 +358,6 @@ export async function setup(app: App) { } // 收藏计数+1 if (type) { - const field = getCollectionTypeField(type) as keyof orm.ISubject; await t .update(schema.chiiSubjects) .set({ From 7c29aada190c562e4f7af6937b78a963bbbccb24 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:16:03 +0800 Subject: [PATCH 25/65] z --- routes/__snapshots__/index.test.ts.snap | 2 +- routes/private/routes/collection.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index c58766a9b..9277001b8 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -3733,7 +3733,7 @@ paths: security: - CookiesSession: [] HTTPBearer: [] - summary: 修改条目进度 + summary: 更新条目进度 tags: - collection put: diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 0d6de02e0..fdffa373a 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -113,7 +113,7 @@ export async function setup(app: App) { '/collections/subjects/:subjectID', { schema: { - summary: '修改条目进度', + summary: '更新条目进度', operationId: 'updateSubjectProgress', tags: [Tag.Collection], security: [{ [Security.CookiesSession]: [], [Security.HTTPBearer]: [] }], From fc26af1308f7c5e0c2bc21e3bf9366fd3d465d7a Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:20:36 +0800 Subject: [PATCH 26/65] z --- routes/private/routes/collection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index fdffa373a..3b110c386 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -279,6 +279,7 @@ export async function setup(app: App) { const now = DateTime.now().toUnixInteger(); toUpdate.type = type; toUpdate.updatedAt = now; + toUpdate.updateIp = auth.ip; toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; //若收藏类型改变,则更新数据 await t From 583a0bddd7cb974c7a4e5a9ca1bcb581a1690797 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:37:16 +0800 Subject: [PATCH 27/65] z --- lib/subject/utils.ts | 35 ++++++++++++++++++++++++++--- routes/private/routes/collection.ts | 27 +++++----------------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 09f1d125d..592292e29 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -2,7 +2,36 @@ import { decr, incr, op, type Txn } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; -function getField(rate: number) { +import type { CollectionType } from './type'; +import { getCollectionTypeField } from './type'; + +/** 更新条目收藏计数,需要在事务中执行 */ +export async function updateSubjectCollection( + t: Txn, + subjectID: number, + newType: CollectionType, + oldType?: CollectionType, +) { + if (oldType && oldType === newType) { + return; + } + const toUpdate: Record = {}; + toUpdate[getCollectionTypeField(newType)] = incr( + schema.chiiSubjects[getCollectionTypeField(newType)], + ); + if (oldType) { + toUpdate[getCollectionTypeField(oldType)] = decr( + schema.chiiSubjects[getCollectionTypeField(oldType)], + ); + } + await t + .update(schema.chiiSubjects) + .set(toUpdate) + .where(op.eq(schema.chiiSubjects.id, subjectID)) + .limit(1); +} + +function getRatingField(rate: number) { return [ undefined, 'rate1', @@ -28,8 +57,8 @@ export async function updateSubjectRating( if (oldRate === newRate) { return; } - const newField = getField(newRate); - const oldField = getField(oldRate); + const newField = getRatingField(newRate); + const oldField = getRatingField(oldRate); const toUpdate: Record = {}; if (newField) { const field = newField as keyof orm.ISubjectFields; diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 3b110c386..68d8d95ec 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -1,7 +1,7 @@ import { Type as t } from '@sinclair/typebox'; import { DateTime } from 'luxon'; -import { db, decr, incr, op } from '@app/drizzle/db.ts'; +import { db, op } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; @@ -15,7 +15,7 @@ import { PersonType, SubjectType, } from '@app/lib/subject/type.ts'; -import { updateSubjectRating } from '@app/lib/subject/utils.ts'; +import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; import { TimelineWriter } from '@app/lib/timeline/writer'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -282,18 +282,7 @@ export async function setup(app: App) { toUpdate.updateIp = auth.ip; toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; //若收藏类型改变,则更新数据 - await t - .update(schema.chiiSubjects) - .set({ - [getCollectionTypeField(type)]: incr( - schema.chiiSubjects[getCollectionTypeField(type)], - ), - [getCollectionTypeField(oldType)]: decr( - schema.chiiSubjects[getCollectionTypeField(oldType)], - ), - }) - .where(op.eq(schema.chiiSubjects.id, subjectID)) - .limit(1); + await updateSubjectCollection(t, subjectID, type, oldType); } if (oldRate !== rate) { needUpdateRate = true; @@ -357,15 +346,9 @@ export async function setup(app: App) { if (rate) { needUpdateRate = true; } - // 收藏计数+1 if (type) { - await t - .update(schema.chiiSubjects) - .set({ - [field]: incr(schema.chiiSubjects[field]), - }) - .where(op.eq(schema.chiiSubjects.id, subjectID)) - .limit(1); + // 收藏计数+1 + await updateSubjectCollection(t, subjectID, type); } } From b3c981a38e1fcbdebb54394a6543a84944ab6b24 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sat, 8 Feb 2025 09:41:23 +0800 Subject: [PATCH 28/65] z --- routes/private/routes/collection.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 68d8d95ec..d39dbb578 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -240,7 +240,7 @@ export async function setup(app: App) { await rateLimit(LimitAction.Subject, auth.userID); - let interestTypeUpdated = false; + let needTimeline = false; await db.transaction(async (t) => { let needUpdateRate = false; @@ -275,7 +275,7 @@ export async function setup(app: App) { } const toUpdate: Partial = {}; if (type && oldType !== type) { - interestTypeUpdated = true; + needTimeline = true; const now = DateTime.now().toUnixInteger(); toUpdate.type = type; toUpdate.updatedAt = now; @@ -342,7 +342,7 @@ export async function setup(app: App) { const field = getCollectionTypeField(type); toInsert[`${field}Dateline`] = now; await t.insert(schema.chiiSubjectInterests).values(toInsert); - interestTypeUpdated = true; + needTimeline = true; if (rate) { needUpdateRate = true; } @@ -359,7 +359,7 @@ export async function setup(app: App) { }); // 插入时间线 - if (privacy === CollectionPrivacy.Public && interestTypeUpdated) { + if (privacy === CollectionPrivacy.Public && needTimeline) { try { await TimelineWriter.subject(auth.userID, subjectID); } catch (error) { From dac6579b8e8bf8ec24b068d9af646bd335e0782d Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 08:20:38 +0800 Subject: [PATCH 29/65] z --- lib/auth/index.ts | 3 --- lib/types/req.ts | 2 +- routes/hooks/pre-handler.ts | 6 +----- routes/private/routes/collection.ts | 13 +++++++++---- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/auth/index.ts b/lib/auth/index.ts index 572b41604..cf002b387 100644 --- a/lib/auth/index.ts +++ b/lib/auth/index.ts @@ -62,7 +62,6 @@ export interface IAuth { /** Unix time seconds */ regTime: number; groupID: UserGroup; - ip: string; } export async function byHeader(key: string | string[] | undefined): Promise { @@ -139,7 +138,6 @@ export function emptyAuth(): IAuth { allowNsfw: false, regTime: 0, groupID: 0, - ip: '', }; } @@ -156,7 +154,6 @@ async function userToAuth(user: res.ISlimUser): Promise { DateTime.now().toUnixInteger() - user.joinedAt >= 60 * 60 * 24 * 90, regTime: user.joinedAt, groupID: user.group, - ip: '', }; } diff --git a/lib/types/req.ts b/lib/types/req.ts index fcca7853c..5bbaecade 100644 --- a/lib/types/req.ts +++ b/lib/types/req.ts @@ -118,7 +118,7 @@ export const CollectSubject = t.Object( type: t.Optional(Ref(CollectionType)), rate: t.Optional(t.Integer({ minimum: 0, maximum: 10, description: '评分,0 表示删除评分' })), comment: t.Optional(t.String({ description: '评价' })), - priv: t.Optional(t.Boolean({ description: '仅自己可见' })), + private: t.Optional(t.Boolean({ description: '仅自己可见' })), tags: t.Optional(t.Array(t.String({ description: '标签, 不能包含空格' }))), }, { $id: 'CollectSubject' }, diff --git a/routes/hooks/pre-handler.ts b/routes/hooks/pre-handler.ts index 14340777e..1f16fc98a 100644 --- a/routes/hooks/pre-handler.ts +++ b/routes/hooks/pre-handler.ts @@ -77,9 +77,7 @@ async function legacySessionAuth(req: FastifyRequest): Promise { } if (user.password === passwordCrypt) { - const a = await auth.byUserID(userID); - a.ip = req.ip; - req.auth = a; + req.auth = await auth.byUserID(userID); req.requestContext.set('user', req.auth.userID); return true; } @@ -106,7 +104,6 @@ export async function sessionAuth(req: FastifyRequest, res: FastifyReply) { return; } - a.ip = req.ip; req.auth = a; req.requestContext.set('user', a.userID); } @@ -125,7 +122,6 @@ export async function accessTokenAuth(req: FastifyRequest): Promise { } const a = await auth.byHeader(token); if (a) { - a.ip = req.ip; req.auth = a; req.requestContext.set('user', a.userID); return true; diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index d39dbb578..cc1fb1338 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -199,7 +199,12 @@ export async function setup(app: App) { }, preHandler: [requireLogin('update subject collection')], }, - async ({ auth, params: { subjectID }, body: { type, rate, comment, priv, tags } }) => { + async ({ + ip, + auth, + params: { subjectID }, + body: { type, rate, comment, private: priv, tags }, + }) => { const slimSubject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); if (!slimSubject) { throw new NotFoundError(`subject ${subjectID}`); @@ -279,7 +284,7 @@ export async function setup(app: App) { const now = DateTime.now().toUnixInteger(); toUpdate.type = type; toUpdate.updatedAt = now; - toUpdate.updateIp = auth.ip; + toUpdate.updateIp = ip; toUpdate[`${getCollectionTypeField(type)}Dateline`] = now; //若收藏类型改变,则更新数据 await updateSubjectCollection(t, subjectID, type, oldType); @@ -334,8 +339,8 @@ export async function setup(app: App) { collectDateline: 0, onHoldDateline: 0, droppedDateline: 0, - createIp: auth.ip, - updateIp: auth.ip, + createIp: ip, + updateIp: ip, updatedAt: now, privacy, }; From 7df87a7e97e01e138a53b224338679830dede5c8 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 08:21:47 +0800 Subject: [PATCH 30/65] z --- routes/__snapshots__/index.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index 523385dad..c64d9f01c 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -194,7 +194,7 @@ exports[`should build private api spec 1`] = ` comment: description: 评价 type: string - priv: + private: description: 仅自己可见 type: boolean rate: From 3a36580ee74355b38979140f59aa2be39e2375cf Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 08:23:33 +0800 Subject: [PATCH 31/65] z --- routes/private/routes/collection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index cc1fb1338..ae187738a 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -124,7 +124,7 @@ export async function setup(app: App) { }, preHandler: [requireLogin('update subject progress')], }, - async ({ auth, params: { subjectID }, body: { epStatus, volStatus } }) => { + async ({ ip, auth, params: { subjectID }, body: { epStatus, volStatus } }) => { const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); if (!subject) { throw new NotFoundError(`subject ${subjectID}`); @@ -163,6 +163,7 @@ export async function setup(app: App) { throw new BadRequestError('no update'); } toUpdate.updatedAt = DateTime.now().toUnixInteger(); + toUpdate.updateIp = ip; await db .update(schema.chiiSubjectInterests) .set(toUpdate) From 6299238f8cc167890b89f2d82d5447e42c80129d Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 09:25:54 +0800 Subject: [PATCH 32/65] z --- bin/mq.ts | 110 +++++++++++++++++------------------------- lib/kafka.ts | 62 ++++++++++++++++++++++++ lib/timeline/kafka.ts | 20 ++++++++ 3 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 lib/kafka.ts create mode 100644 lib/timeline/kafka.ts diff --git a/bin/mq.ts b/bin/mq.ts index 6ce5a5bc3..e17c47d40 100644 --- a/bin/mq.ts +++ b/bin/mq.ts @@ -1,5 +1,3 @@ -import { KafkaJS } from '@confluentinc/kafka-javascript'; - import { handle as handleBlogEvent } from '@app/event/blog'; import { handle as handleCharacterEvent } from '@app/event/character'; import { handle as handleGroupEvent } from '@app/event/group'; @@ -13,10 +11,13 @@ import { import { handle as handleTimelineEvent } from '@app/event/timeline'; import type { Payload } from '@app/event/type'; import { handle as handleUserEvent } from '@app/event/user'; -import config from '@app/lib/config.ts'; +import { newConsumer } from '@app/lib/kafka.ts'; import { logger } from '@app/lib/logger'; +import { handleTimelineMessage } from '@app/lib/timeline/kafka.ts'; const TOPICS = [ + 'timeline', + // 'debezium.chii.bangumi.chii_pms', // 'debezium.chii.bangumi.chii_subject_revisions', 'debezium.chii.bangumi.chii_blog_entry', @@ -31,53 +32,40 @@ const TOPICS = [ 'debezium.chii.bangumi.chii_timeline', ]; -async function onMessage(key: string, value: string) { +type Handler = (key: string, value: string) => Promise; + +const binlogHandlers: Record = { + chii_blog_entry: handleBlogEvent, + chii_characters: handleCharacterEvent, + chii_episodes: handleEpisodeEvent, + chii_groups: handleGroupEvent, + chii_index: handleIndexEvent, + chii_members: handleUserEvent, + chii_persons: handlePersonEvent, + chii_subject_fields: handleSubjectFieldsEvent, + chii_subjects: handleSubjectEvent, + chii_timeline: handleTimelineEvent, +}; + +async function onBinlogMessage(key: string, value: string) { const payload = JSON.parse(value) as Payload; - switch (payload.source.table) { - case 'chii_blog_entry': { - await handleBlogEvent(key, value); - break; - } - case 'chii_characters': { - await handleCharacterEvent(key, value); - break; - } - case 'chii_episodes': { - await handleEpisodeEvent(key, value); - break; - } - case 'chii_groups': { - await handleGroupEvent(key, value); - break; - } - case 'chii_index': { - await handleIndexEvent(key, value); - break; - } - case 'chii_members': { - await handleUserEvent(key, value); - break; - } - case 'chii_persons': { - await handlePersonEvent(key, value); - break; - } - case 'chii_subject_fields': { - await handleSubjectFieldsEvent(key, value); - break; - } - case 'chii_subjects': { - await handleSubjectEvent(key, value); - break; - } - case 'chii_timeline': { - await handleTimelineEvent(key, value); - break; - } - default: { - break; - } + const handler = binlogHandlers[payload.source.table]; + if (!handler) { + return; + } + await handler(key, value); +} + +const serviceHandlers: Record = { + timeline: handleTimelineMessage, +}; + +async function onServiceMessage(topic: string, key: string, value: string) { + const handler = serviceHandlers[topic]; + if (!handler) { + return; } + await handler(key, value); } async function main() { @@ -87,25 +75,9 @@ async function main() { return; } - if (!config.kafkaBrokers) { - logger.error('KAFKA_BROKERS is not set'); - return; - } - const { Kafka, logLevel } = KafkaJS; - - const kafka = new Kafka({ - log_level: logLevel.WARN, - 'client.id': 'server-private', - }); - const consumer = kafka.consumer({ - 'bootstrap.servers': config.kafkaBrokers, - 'group.id': 'server-private', - }); - await consumer.connect(); - await consumer.subscribe({ topics: TOPICS }); - + const consumer = await newConsumer(TOPICS); await consumer.run({ - eachMessage: async ({ message }) => { + eachMessage: async ({ topic, message }) => { if (!message.key) { return; } @@ -113,7 +85,11 @@ async function main() { return; } try { - await onMessage(message.key.toString(), message.value.toString()); + if (topic.startsWith('debezium.')) { + await onBinlogMessage(message.key.toString(), message.value.toString()); + } else { + await onServiceMessage(topic, message.key.toString(), message.value.toString()); + } } catch (error) { logger.error(`Error processing message ${message.key.toString()}: ${error}`); } diff --git a/lib/kafka.ts b/lib/kafka.ts new file mode 100644 index 000000000..2cd292ab6 --- /dev/null +++ b/lib/kafka.ts @@ -0,0 +1,62 @@ +import { KafkaJS } from '@confluentinc/kafka-javascript'; + +import config from '@app/lib/config.ts'; + +class Producer { + private producer: KafkaJS.Producer | null = null; + + async initialize() { + if (this.producer) { + return; + } + + const { Kafka, logLevel } = KafkaJS; + + const kafka = new Kafka({ + log_level: logLevel.WARN, + 'client.id': 'server-private', + }); + + if (!config.kafkaBrokers) { + throw new Error('KAFKA_BROKERS is not set'); + } + + const producer = kafka.producer({ + 'bootstrap.servers': config.kafkaBrokers, + }); + await producer.connect(); + this.producer = producer; + } + + async send(topic: string, key: string, value: string) { + await this.initialize(); + if (!this.producer) { + throw new Error('Producer not initialized'); + } + await this.producer.send({ + topic, + messages: [{ key, value }], + }); + } +} + +export const producer = new Producer(); + +export async function newConsumer(topics: string[]) { + const { Kafka, logLevel } = KafkaJS; + + const kafka = new Kafka({ + log_level: logLevel.WARN, + 'client.id': 'server-private', + }); + if (!config.kafkaBrokers) { + throw new Error('KAFKA_BROKERS is not set'); + } + const consumer = kafka.consumer({ + 'bootstrap.servers': config.kafkaBrokers, + 'group.id': 'server-private', + }); + await consumer.connect(); + await consumer.subscribe({ topics }); + return consumer; +} diff --git a/lib/timeline/kafka.ts b/lib/timeline/kafka.ts new file mode 100644 index 000000000..9a2f0beb1 --- /dev/null +++ b/lib/timeline/kafka.ts @@ -0,0 +1,20 @@ +import { logger } from '@app/lib/logger'; + +import { TimelineWriter } from './writer'; + +export async function handleTimelineMessage(op: string, details: string) { + switch (op) { + case 'subject': { + const payload = JSON.parse(details) as { + userID: number; + subjectID: number; + }; + await TimelineWriter.subject(payload.userID, payload.subjectID); + break; + } + default: { + logger.error(`Unknown timeline operation: ${op}`); + break; + } + } +} From f913d9fb22704ebfcbfbac4c31924822c4ef64b7 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 09:28:33 +0800 Subject: [PATCH 33/65] z --- lib/timeline/kafka.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/timeline/kafka.ts b/lib/timeline/kafka.ts index 9a2f0beb1..b9564e0d6 100644 --- a/lib/timeline/kafka.ts +++ b/lib/timeline/kafka.ts @@ -1,4 +1,5 @@ import { logger } from '@app/lib/logger'; +import type { EpisodeCollectionStatus } from '@app/lib/subject/type'; import { TimelineWriter } from './writer'; @@ -12,6 +13,44 @@ export async function handleTimelineMessage(op: string, details: string) { await TimelineWriter.subject(payload.userID, payload.subjectID); break; } + case 'progressEpisode': { + const payload = JSON.parse(details) as { + userID: number; + subjectID: number; + episodeID: number; + status: EpisodeCollectionStatus; + }; + await TimelineWriter.progressEpisode( + payload.userID, + payload.subjectID, + payload.episodeID, + payload.status, + ); + break; + } + case 'progressSubject': { + const payload = JSON.parse(details) as { + userID: number; + subjectID: number; + epsUpdate?: number; + volsUpdate?: number; + }; + await TimelineWriter.progressSubject( + payload.userID, + payload.subjectID, + payload.epsUpdate, + payload.volsUpdate, + ); + break; + } + case 'statusTsukkomi': { + const payload = JSON.parse(details) as { + userID: number; + text: string; + }; + await TimelineWriter.statusTsukkomi(payload.userID, payload.text); + break; + } default: { logger.error(`Unknown timeline operation: ${op}`); break; From b1278548a5a4a7eebd4fcdc9e08fd5762d4b96c4 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 09:31:46 +0800 Subject: [PATCH 34/65] z --- routes/private/routes/collection.ts | 38 ++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index ae187738a..ed2e87f5b 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -5,8 +5,8 @@ import { db, op } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; -import { BadRequestError, UnexpectedNotFoundError } from '@app/lib/error'; -import { NotFoundError } from '@app/lib/error'; +import { BadRequestError, NotFoundError, UnexpectedNotFoundError } from '@app/lib/error'; +import { producer } from '@app/lib/kafka'; import { logger } from '@app/lib/logger'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { @@ -16,7 +16,6 @@ import { SubjectType, } from '@app/lib/subject/type.ts'; import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; -import { TimelineWriter } from '@app/lib/timeline/writer'; 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'; @@ -174,14 +173,24 @@ export async function setup(app: App) { ), ) .limit(1); - try { - await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); - } catch (error) { - logger.error(`failed to write timeline for subject ${subjectID}`, { - error, + await producer.send( + 'timeline', + 'progressSubject', + JSON.stringify({ userID: auth.userID, - }); - } + subjectID, + epsUpdate: epStatus, + volsUpdate: volStatus, + }), + ); + // try { + // await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); + // } catch (error) { + // logger.error(`failed to write timeline for subject ${subjectID}`, { + // error, + // userID: auth.userID, + // }); + // } }, ); @@ -367,7 +376,14 @@ export async function setup(app: App) { // 插入时间线 if (privacy === CollectionPrivacy.Public && needTimeline) { try { - await TimelineWriter.subject(auth.userID, subjectID); + await producer.send( + 'timeline', + 'subject', + JSON.stringify({ + userID: auth.userID, + subjectID, + }), + ); } catch (error) { logger.error(`failed to write timeline for subject ${subjectID}`, { error, From a9c9a376c34137cac7db3ee74eb9136630282f7e Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 09:32:39 +0800 Subject: [PATCH 35/65] z --- routes/private/routes/collection.ts | 30 +++++++---------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index ed2e87f5b..ae1c1a74f 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -7,7 +7,6 @@ import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, NotFoundError, UnexpectedNotFoundError } from '@app/lib/error'; import { producer } from '@app/lib/kafka'; -import { logger } from '@app/lib/logger'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { CollectionPrivacy, @@ -183,14 +182,6 @@ export async function setup(app: App) { volsUpdate: volStatus, }), ); - // try { - // await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus); - // } catch (error) { - // logger.error(`failed to write timeline for subject ${subjectID}`, { - // error, - // userID: auth.userID, - // }); - // } }, ); @@ -375,21 +366,14 @@ export async function setup(app: App) { // 插入时间线 if (privacy === CollectionPrivacy.Public && needTimeline) { - try { - await producer.send( - 'timeline', - 'subject', - JSON.stringify({ - userID: auth.userID, - subjectID, - }), - ); - } catch (error) { - logger.error(`failed to write timeline for subject ${subjectID}`, { - error, + await producer.send( + 'timeline', + 'subject', + JSON.stringify({ userID: auth.userID, - }); - } + subjectID, + }), + ); } }, ); From 51c76345779d3c7c9015f2cc4ae3c90a42c661c5 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 09:47:57 +0800 Subject: [PATCH 36/65] z --- routes/private/routes/collection.ts | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index ae1c1a74f..af8793f98 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -6,7 +6,7 @@ import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, NotFoundError, UnexpectedNotFoundError } from '@app/lib/error'; -import { producer } from '@app/lib/kafka'; +import { logger } from '@app/lib/logger'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { CollectionPrivacy, @@ -15,6 +15,7 @@ import { SubjectType, } from '@app/lib/subject/type.ts'; import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; +import { TimelineWriter } from '@app/lib/timeline/writer'; 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'; @@ -172,15 +173,14 @@ export async function setup(app: App) { ), ) .limit(1); - await producer.send( - 'timeline', - 'progressSubject', - JSON.stringify({ - userID: auth.userID, - subjectID, - epsUpdate: epStatus, - volsUpdate: volStatus, - }), + + await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus).catch( + (error: unknown) => { + logger.error(`failed to write timeline for subject ${subjectID}`, { + error: error as Error, + userID: auth.userID, + }); + }, ); }, ); @@ -366,14 +366,12 @@ export async function setup(app: App) { // 插入时间线 if (privacy === CollectionPrivacy.Public && needTimeline) { - await producer.send( - 'timeline', - 'subject', - JSON.stringify({ + await TimelineWriter.subject(auth.userID, subjectID).catch((error: unknown) => { + logger.error(`failed to write timeline for subject ${subjectID}`, { + error: error as Error, userID: auth.userID, - subjectID, - }), - ); + }); + }); } }, ); From d298e0962a68ca9b5bec3724a5e36bdd8fe0bbd3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 19:15:38 +0800 Subject: [PATCH 37/65] z --- routes/private/routes/collection.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index af8793f98..36a7a7f9a 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -6,7 +6,6 @@ import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import { Dam, dam } from '@app/lib/dam'; import { BadRequestError, NotFoundError, UnexpectedNotFoundError } from '@app/lib/error'; -import { logger } from '@app/lib/logger'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import { CollectionPrivacy, @@ -15,7 +14,7 @@ import { SubjectType, } from '@app/lib/subject/type.ts'; import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; -import { TimelineWriter } from '@app/lib/timeline/writer'; +import { TimelineEmitter } from '@app/lib/timeline/kafka'; 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'; @@ -174,14 +173,12 @@ export async function setup(app: App) { ) .limit(1); - await TimelineWriter.progressSubject(auth.userID, subjectID, epStatus, volStatus).catch( - (error: unknown) => { - logger.error(`failed to write timeline for subject ${subjectID}`, { - error: error as Error, - userID: auth.userID, - }); - }, - ); + await TimelineEmitter.emit('progressSubject', { + userID: auth.userID, + subjectID, + epsUpdate: epStatus, + volsUpdate: volStatus, + }); }, ); @@ -366,11 +363,9 @@ export async function setup(app: App) { // 插入时间线 if (privacy === CollectionPrivacy.Public && needTimeline) { - await TimelineWriter.subject(auth.userID, subjectID).catch((error: unknown) => { - logger.error(`failed to write timeline for subject ${subjectID}`, { - error: error as Error, - userID: auth.userID, - }); + await TimelineEmitter.emit('subject', { + userID: auth.userID, + subjectID, }); } }, From fed0bd26d83aacab1bd4e65ce683b6a13b092a3b Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 22:14:02 +0800 Subject: [PATCH 38/65] z --- routes/private/routes/collection.ts | 48 ++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 36a7a7f9a..bac0c95db 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -14,7 +14,7 @@ import { SubjectType, } from '@app/lib/subject/type.ts'; import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; -import { TimelineEmitter } from '@app/lib/timeline/kafka'; +import { AsyncTimelineWriter } from '@app/lib/timeline/writer.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'; @@ -119,11 +119,14 @@ export async function setup(app: App) { subjectID: t.Integer(), }), body: req.Ref(req.UpdateSubjectProgress), + response: { + 200: t.Object({}), + }, }, preHandler: [requireLogin('update subject progress')], }, async ({ ip, auth, params: { subjectID }, body: { epStatus, volStatus } }) => { - const subject = await fetcher.fetchSlimSubjectByID(subjectID, auth.allowNsfw); + const subject = await fetcher.fetchSubjectByID(subjectID, auth.allowNsfw); if (!subject) { throw new NotFoundError(`subject ${subjectID}`); } @@ -173,12 +176,21 @@ export async function setup(app: App) { ) .limit(1); - await TimelineEmitter.emit('progressSubject', { - userID: auth.userID, - subjectID, - epsUpdate: epStatus, - volsUpdate: volStatus, + await AsyncTimelineWriter.progressSubject({ + uid: auth.userID, + subject: { + id: subjectID, + type: subject.type, + eps: subject.eps, + volumes: subject.volumes, + }, + collect: { + epsUpdate: epStatus, + volsUpdate: volStatus, + }, + createdAt: DateTime.now().toUnixInteger(), }); + return {}; }, ); @@ -194,6 +206,9 @@ export async function setup(app: App) { subjectID: t.Integer(), }), body: req.Ref(req.CollectSubject), + response: { + 200: t.Object({}), + }, }, preHandler: [requireLogin('update subject collection')], }, @@ -362,12 +377,23 @@ export async function setup(app: App) { }); // 插入时间线 - if (privacy === CollectionPrivacy.Public && needTimeline) { - await TimelineEmitter.emit('subject', { - userID: auth.userID, - subjectID, + if (privacy === CollectionPrivacy.Public && needTimeline && type) { + await AsyncTimelineWriter.subject({ + uid: auth.userID, + subject: { + id: subjectID, + type: slimSubject.type, + }, + collect: { + id: subjectID, + type, + rate: rate ?? 0, + comment: comment ?? '', + }, + createdAt: DateTime.now().toUnixInteger(), }); } + return {}; }, ); From 960b117a0e2c8c52bb71f4e35fa3224d87f9f816 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 22:17:30 +0800 Subject: [PATCH 39/65] z --- routes/private/routes/collection.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index bac0c95db..011f88d86 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -179,7 +179,7 @@ export async function setup(app: App) { await AsyncTimelineWriter.progressSubject({ uid: auth.userID, subject: { - id: subjectID, + id: subject.id, type: subject.type, eps: subject.eps, volumes: subject.volumes, @@ -259,6 +259,7 @@ export async function setup(app: App) { await rateLimit(LimitAction.Subject, auth.userID); let needTimeline = false; + let interestID = 0; await db.transaction(async (t) => { let needUpdateRate = false; @@ -285,6 +286,7 @@ export async function setup(app: App) { rate = 0; } if (interest) { + interestID = interest.id; oldRate = interest.rate; const oldType = interest.type; const oldPrivacy = interest.privacy; @@ -359,7 +361,8 @@ export async function setup(app: App) { }; const field = getCollectionTypeField(type); toInsert[`${field}Dateline`] = now; - await t.insert(schema.chiiSubjectInterests).values(toInsert); + const [result] = await t.insert(schema.chiiSubjectInterests).values(toInsert); + interestID = result.insertId; needTimeline = true; if (rate) { needUpdateRate = true; @@ -381,11 +384,11 @@ export async function setup(app: App) { await AsyncTimelineWriter.subject({ uid: auth.userID, subject: { - id: subjectID, + id: slimSubject.id, type: slimSubject.type, }, collect: { - id: subjectID, + id: interestID, type, rate: rate ?? 0, comment: comment ?? '', From 6d9ebed2579cf94c22fed01d16e2c6351e95ce6c Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 22:20:31 +0800 Subject: [PATCH 40/65] z --- routes/__snapshots__/index.test.ts.snap | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/routes/__snapshots__/index.test.ts.snap b/routes/__snapshots__/index.test.ts.snap index c64d9f01c..bf91f0a7c 100644 --- a/routes/__snapshots__/index.test.ts.snap +++ b/routes/__snapshots__/index.test.ts.snap @@ -3729,6 +3729,13 @@ paths: schema: $ref: '#/components/schemas/UpdateSubjectProgress' responses: + '200': + content: + application/json: + schema: + properties: {} + type: object + description: Default Response '500': content: application/json: @@ -3756,6 +3763,13 @@ paths: schema: $ref: '#/components/schemas/CollectSubject' responses: + '200': + content: + application/json: + schema: + properties: {} + type: object + description: Default Response '500': content: application/json: From 5a7f215503a5e8a6f997f2f1c1bf27509e5d7cd3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Sun, 9 Feb 2025 22:32:14 +0800 Subject: [PATCH 41/65] z --- routes/private/routes/collection.ts | 97 +++++++++++++++++++---------- 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 011f88d86..82512e1df 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -130,16 +130,6 @@ export async function setup(app: App) { if (!subject) { throw new NotFoundError(`subject ${subjectID}`); } - switch (subject.type) { - case SubjectType.Book: - case SubjectType.Anime: - case SubjectType.Real: { - break; - } - default: { - throw new BadRequestError(`subject not supported for progress`); - } - } const [interest] = await db .select() .from(schema.chiiSubjectInterests) @@ -153,28 +143,71 @@ export async function setup(app: App) { if (!interest) { throw new NotFoundError(`subject not collected`); } - const toUpdate: Partial = {}; - if (epStatus !== undefined) { - toUpdate.epStatus = epStatus; - } - if (volStatus !== undefined) { - toUpdate.volStatus = volStatus; - } - if (Object.keys(toUpdate).length === 0) { - throw new BadRequestError('no update'); - } - toUpdate.updatedAt = DateTime.now().toUnixInteger(); - toUpdate.updateIp = ip; - await db - .update(schema.chiiSubjectInterests) - .set(toUpdate) - .where( - op.and( - op.eq(schema.chiiSubjectInterests.uid, auth.userID), - op.eq(schema.chiiSubjectInterests.subjectID, subjectID), - ), - ) - .limit(1); + + await db.transaction(async (t) => { + const toUpdate: Partial = {}; + switch (subject.type) { + case SubjectType.Anime: + case SubjectType.Real: { + if (epStatus === undefined) { + break; + } + toUpdate.epStatus = epStatus; + const episodes = await t + .select({ id: schema.chiiEpisodes.id }) + .from(schema.chiiEpisodes) + .where( + op.and( + op.eq(schema.chiiEpisodes.subjectID, subjectID), + // 只更新 main 类型的剧集 + op.eq(schema.chiiEpisodes.type, 0), + op.eq(schema.chiiEpisodes.ban, 0), + ), + ) + .orderBy( + op.asc(schema.chiiEpisodes.disc), + op.asc(schema.chiiEpisodes.type), + op.asc(schema.chiiEpisodes.sort), + ) + .limit(epStatus); + const episodeIDs = episodes.map((e) => e.id); + if (episodeIDs.length === 0) { + break; + } + // TODO: mark episodes as watched + + break; + } + case SubjectType.Book: { + if (epStatus !== undefined) { + toUpdate.epStatus = epStatus; + } + if (volStatus !== undefined) { + toUpdate.volStatus = volStatus; + } + break; + } + default: { + throw new BadRequestError(`subject not supported for progress`); + } + } + + if (Object.keys(toUpdate).length === 0) { + throw new BadRequestError('no update'); + } + toUpdate.updatedAt = DateTime.now().toUnixInteger(); + toUpdate.updateIp = ip; + await t + .update(schema.chiiSubjectInterests) + .set(toUpdate) + .where( + op.and( + op.eq(schema.chiiSubjectInterests.uid, auth.userID), + op.eq(schema.chiiSubjectInterests.subjectID, subjectID), + ), + ) + .limit(1); + }); await AsyncTimelineWriter.progressSubject({ uid: auth.userID, From 4348374514ee3f29e06679f4c1fae38a11fdaa22 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:20:47 +0800 Subject: [PATCH 42/65] z --- lib/subject/utils.ts | 52 ++++++++++++++++++++++++++++- lib/types/fetcher.ts | 8 ++--- routes/private/routes/collection.ts | 6 +--- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 592292e29..b353f706c 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -1,9 +1,12 @@ +import * as php from '@trim21/php-serialize'; +import { DateTime } from 'luxon'; + import { decr, incr, op, type Txn } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; import type { CollectionType } from './type'; -import { getCollectionTypeField } from './type'; +import { EpisodeCollectionStatus, getCollectionTypeField } from './type'; /** 更新条目收藏计数,需要在事务中执行 */ export async function updateSubjectCollection( @@ -77,3 +80,50 @@ export async function updateSubjectRating( .where(op.eq(schema.chiiSubjects.id, subjectID)) .limit(1); } + +export async function markEpisodesAsWatched( + t: Txn, + uid: number, + sid: number, + episodeIDs: number[], +) { + const epStatusList: Record = {}; + for (const episodeID of episodeIDs) { + epStatusList[episodeID] = { + eid: episodeID.toString(), + type: EpisodeCollectionStatus.Done, + }; + } + const [current] = await t + .select() + .from(schema.chiiEpStatus) + .where(op.and(op.eq(schema.chiiEpStatus.uid, uid), op.eq(schema.chiiEpStatus.sid, sid))); + if (current) { + if (current.status) { + const oldList = php.parse(current.status) as Record; + for (const [eid, x] of Object.entries(oldList)) { + const episodeID = Number.parseInt(eid); + if (Number.isNaN(episodeID)) { + continue; + } + if (!episodeIDs.includes(episodeID)) { + epStatusList[episodeID] = x; + } + } + } + const newStatus = php.stringify(epStatusList); + await t + .update(schema.chiiEpStatus) + .set({ status: newStatus, updatedAt: DateTime.now().toUnixInteger() }) + .where(op.eq(schema.chiiEpStatus.id, current.id)) + .limit(1); + } else { + const newStatus = php.stringify(epStatusList); + await t.insert(schema.chiiEpStatus).values({ + uid, + sid, + status: newStatus, + updatedAt: DateTime.now().toUnixInteger(), + }); + } +} diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index eced23741..da6b96c15 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -458,16 +458,16 @@ export async function fetchSubjectEpStatus( userID: number, subjectID: number, ): Promise> { - const data = await db + const [data] = await db .select() .from(schema.chiiEpStatus) .where( op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), ); - for (const d of data) { - return convert.toSubjectEpStatus(d); + if (!data) { + return {}; } - return {}; + return convert.toSubjectEpStatus(data); } /** Cached */ diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 82512e1df..8896c85e0 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -164,11 +164,7 @@ export async function setup(app: App) { op.eq(schema.chiiEpisodes.ban, 0), ), ) - .orderBy( - op.asc(schema.chiiEpisodes.disc), - op.asc(schema.chiiEpisodes.type), - op.asc(schema.chiiEpisodes.sort), - ) + .orderBy(op.asc(schema.chiiEpisodes.type), op.asc(schema.chiiEpisodes.sort)) .limit(epStatus); const episodeIDs = episodes.map((e) => e.id); if (episodeIDs.length === 0) { From d8d9f42d3cd080ee4ecf761981f51f4931dd0e9a Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:21:44 +0800 Subject: [PATCH 43/65] z --- lib/subject/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index b353f706c..ebb9c398c 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -81,6 +81,7 @@ export async function updateSubjectRating( .limit(1); } +/** 标记条目剧集为已观看,需要在事务中执行 */ export async function markEpisodesAsWatched( t: Txn, uid: number, From 4aecd825f5610f6743aa08b602a8917e15d01970 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:47:50 +0800 Subject: [PATCH 44/65] z --- lib/subject/utils.ts | 39 ++++++++++++++++++++++++++++++-- lib/types/convert.ts | 24 +------------------- lib/types/fetcher.ts | 17 -------------- routes/private/routes/episode.ts | 3 ++- routes/private/routes/subject.ts | 3 ++- 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index ebb9c398c..8c86edc81 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -1,11 +1,11 @@ import * as php from '@trim21/php-serialize'; import { DateTime } from 'luxon'; -import { decr, incr, op, type Txn } from '@app/drizzle/db.ts'; +import { db, decr, incr, op, type Txn } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; -import type { CollectionType } from './type'; +import type { CollectionType, UserEpisodeCollection } from './type'; import { EpisodeCollectionStatus, getCollectionTypeField } from './type'; /** 更新条目收藏计数,需要在事务中执行 */ @@ -128,3 +128,38 @@ export async function markEpisodesAsWatched( }); } } + +export function parseSubjectEpStatus( + status: orm.ISubjectEpStatus, +): Record { + const result: Record = {}; + if (!status.status) { + return result; + } + const epStatusList = php.parse(status.status) as Record; + for (const [eid, x] of Object.entries(epStatusList)) { + const episodeId = Number.parseInt(eid); + if (Number.isNaN(episodeId)) { + continue; + } + result[episodeId] = { id: episodeId, type: x.type }; + } + return result; +} + +export async function getEpStatus( + userID: number, + subjectID: number, +): Promise> { + const [data] = await db + .select() + .from(schema.chiiEpStatus) + .where( + op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), + ) + .limit(1); + if (!data) { + return {}; + } + return parseSubjectEpStatus(data); +} diff --git a/lib/types/convert.ts b/lib/types/convert.ts index b20a3a289..b5b4c896c 100644 --- a/lib/types/convert.ts +++ b/lib/types/convert.ts @@ -5,11 +5,7 @@ import * as php from '@trim21/php-serialize'; import type * as orm from '@app/drizzle/orm.ts'; import { avatar, blogIcon, groupIcon, personImages, subjectCover } from '@app/lib/images'; import { getInfoboxSummary } from '@app/lib/subject/infobox.ts'; -import { - CollectionPrivacy, - CollectionType, - type UserEpisodeCollection, -} from '@app/lib/subject/type.ts'; +import { CollectionPrivacy, CollectionType } from '@app/lib/subject/type.ts'; import type * as res from '@app/lib/types/res.ts'; import { findNetworkService, @@ -449,24 +445,6 @@ export function toSlimEpisode(episode: orm.IEpisode): res.IEpisode { }; } -export function toSubjectEpStatus( - status: orm.ISubjectEpStatus, -): Record { - const result: Record = {}; - if (!status.status) { - return result; - } - const epStatusList = php.parse(status.status) as Record; - for (const [eid, x] of Object.entries(epStatusList)) { - const episodeId = Number.parseInt(eid); - if (Number.isNaN(episodeId)) { - continue; - } - result[episodeId] = { id: episodeId, type: x.type }; - } - return result; -} - export function toSlimCharacter(character: orm.ICharacter): res.ISlimCharacter { const infobox = toInfobox(character.infobox); return { diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index da6b96c15..766095f34 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -22,7 +22,6 @@ import { SubjectSort, SubjectType, TagCat, - type UserEpisodeCollection, } from '@app/lib/subject/type.ts'; import { getSubjectTrendingKey } from '@app/lib/trending/subject.ts'; import { type TrendingItem, TrendingPeriod } from '@app/lib/trending/type.ts'; @@ -454,22 +453,6 @@ export async function fetchSubjectInterest( return convert.toSubjectInterest(data); } -export async function fetchSubjectEpStatus( - userID: number, - subjectID: number, -): Promise> { - const [data] = await db - .select() - .from(schema.chiiEpStatus) - .where( - op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), - ); - if (!data) { - return {}; - } - return convert.toSubjectEpStatus(data); -} - /** Cached */ export async function fetchSlimEpisodeByID(episodeID: number): Promise { const episode = await fetchEpisodeItemByID(episodeID); diff --git a/routes/private/routes/episode.ts b/routes/private/routes/episode.ts index ab3740c6e..478997039 100644 --- a/routes/private/routes/episode.ts +++ b/routes/private/routes/episode.ts @@ -3,6 +3,7 @@ import { Type as t } from '@sinclair/typebox'; import { Comment, CommentTarget } from '@app/lib/comment'; import { NotFoundError } from '@app/lib/error.ts'; import { Security, Tag } from '@app/lib/openapi/index.ts'; +import { getEpStatus } from '@app/lib/subject/utils'; 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'; @@ -35,7 +36,7 @@ export async function setup(app: App) { throw new NotFoundError(`episode ${episodeID}`); } if (auth.login) { - const epStatus = await fetcher.fetchSubjectEpStatus(auth.userID, ep.subjectID); + const epStatus = await getEpStatus(auth.userID, ep.subjectID); ep.status = epStatus[episodeID]?.type; } return ep; diff --git a/routes/private/routes/subject.ts b/routes/private/routes/subject.ts index c0cc3350f..4ceac9ce5 100644 --- a/routes/private/routes/subject.ts +++ b/routes/private/routes/subject.ts @@ -12,6 +12,7 @@ import { Notify, NotifyType } from '@app/lib/notify.ts'; import { Security, Tag } from '@app/lib/openapi/index.ts'; import type { SubjectFilter, SubjectSort } from '@app/lib/subject/type.ts'; import { CollectionPrivacy } from '@app/lib/subject/type.ts'; +import { getEpStatus } from '@app/lib/subject/utils'; import { CanViewTopicContent, CanViewTopicReply } from '@app/lib/topic/display.ts'; import { canEditTopic, canReplyPost } from '@app/lib/topic/state'; import { CommentState, TopicDisplay } from '@app/lib/topic/type.ts'; @@ -219,7 +220,7 @@ export async function setup(app: App) { .offset(offset); const episodes = data.map((d) => convert.toSlimEpisode(d)); if (auth.login) { - const epStatus = await fetcher.fetchSubjectEpStatus(auth.userID, subjectID); + const epStatus = await getEpStatus(auth.userID, subjectID); for (const ep of episodes) { ep.status = epStatus[ep.id]?.type; } From 569fa5a3bf6129ee94011019dc4240f17e7cccd7 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:48:24 +0800 Subject: [PATCH 45/65] z --- routes/private/routes/collection.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 8896c85e0..e09d83914 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -13,7 +13,11 @@ import { PersonType, SubjectType, } from '@app/lib/subject/type.ts'; -import { updateSubjectCollection, updateSubjectRating } from '@app/lib/subject/utils.ts'; +import { + markEpisodesAsWatched, + updateSubjectCollection, + updateSubjectRating, +} from '@app/lib/subject/utils.ts'; import { AsyncTimelineWriter } from '@app/lib/timeline/writer.ts'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -170,8 +174,7 @@ export async function setup(app: App) { if (episodeIDs.length === 0) { break; } - // TODO: mark episodes as watched - + await markEpisodesAsWatched(t, auth.userID, subjectID, episodeIDs); break; } case SubjectType.Book: { From fda002d9301a8240b4782852391970a7ea56ff58 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:50:48 +0800 Subject: [PATCH 46/65] z --- lib/subject/utils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 8c86edc81..3e6dc0fcf 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -84,8 +84,8 @@ export async function updateSubjectRating( /** 标记条目剧集为已观看,需要在事务中执行 */ export async function markEpisodesAsWatched( t: Txn, - uid: number, - sid: number, + userID: number, + subjectID: number, episodeIDs: number[], ) { const epStatusList: Record = {}; @@ -98,7 +98,9 @@ export async function markEpisodesAsWatched( const [current] = await t .select() .from(schema.chiiEpStatus) - .where(op.and(op.eq(schema.chiiEpStatus.uid, uid), op.eq(schema.chiiEpStatus.sid, sid))); + .where( + op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), + ); if (current) { if (current.status) { const oldList = php.parse(current.status) as Record; @@ -121,8 +123,8 @@ export async function markEpisodesAsWatched( } else { const newStatus = php.stringify(epStatusList); await t.insert(schema.chiiEpStatus).values({ - uid, - sid, + uid: userID, + sid: subjectID, status: newStatus, updatedAt: DateTime.now().toUnixInteger(), }); From 6279ce8b18d62a32d94abebd97adaea8d688efce Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:52:13 +0800 Subject: [PATCH 47/65] z --- lib/subject/utils.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 3e6dc0fcf..6d938fb53 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -131,14 +131,12 @@ export async function markEpisodesAsWatched( } } -export function parseSubjectEpStatus( - status: orm.ISubjectEpStatus, -): Record { +export function parseSubjectEpStatus(status: string): Record { const result: Record = {}; - if (!status.status) { + if (!status) { return result; } - const epStatusList = php.parse(status.status) as Record; + const epStatusList = php.parse(status) as Record; for (const [eid, x] of Object.entries(epStatusList)) { const episodeId = Number.parseInt(eid); if (Number.isNaN(episodeId)) { @@ -163,5 +161,5 @@ export async function getEpStatus( if (!data) { return {}; } - return parseSubjectEpStatus(data); + return parseSubjectEpStatus(data.status); } From 268ad83cfbe33b95548cda18d5881a5739d4450d Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:56:44 +0800 Subject: [PATCH 48/65] z --- lib/subject/type.ts | 2 +- lib/subject/utils.ts | 30 ++++++++++-------------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/subject/type.ts b/lib/subject/type.ts index ef32da89d..6b87e5fd0 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -65,7 +65,7 @@ export enum EpisodeCollectionStatus { } export interface UserEpisodeCollection { - id: number; + eid: number; type: EpisodeCollectionStatus; } diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 6d938fb53..cdf567631 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -88,10 +88,10 @@ export async function markEpisodesAsWatched( subjectID: number, episodeIDs: number[], ) { - const epStatusList: Record = {}; + const epStatusList: Record = {}; for (const episodeID of episodeIDs) { epStatusList[episodeID] = { - eid: episodeID.toString(), + eid: episodeID, type: EpisodeCollectionStatus.Done, }; } @@ -101,17 +101,11 @@ export async function markEpisodesAsWatched( .where( op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), ); - if (current) { - if (current.status) { - const oldList = php.parse(current.status) as Record; - for (const [eid, x] of Object.entries(oldList)) { - const episodeID = Number.parseInt(eid); - if (Number.isNaN(episodeID)) { - continue; - } - if (!episodeIDs.includes(episodeID)) { - epStatusList[episodeID] = x; - } + if (current?.status) { + const oldList = parseSubjectEpStatus(current.status); + for (const x of Object.values(oldList)) { + if (!episodeIDs.includes(x.eid)) { + epStatusList[x.eid] = x; } } const newStatus = php.stringify(epStatusList); @@ -136,13 +130,9 @@ export function parseSubjectEpStatus(status: string): Record; - for (const [eid, x] of Object.entries(epStatusList)) { - const episodeId = Number.parseInt(eid); - if (Number.isNaN(episodeId)) { - continue; - } - result[episodeId] = { id: episodeId, type: x.type }; + const epStatusList = php.parse(status) as Record; + for (const x of Object.values(epStatusList)) { + result[x.eid] = x; } return result; } From 7480f95a6946c4d6db88ac7235a31fd41563c7de Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 09:57:36 +0800 Subject: [PATCH 49/65] z --- lib/subject/type.ts | 2 +- lib/subject/utils.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/subject/type.ts b/lib/subject/type.ts index 6b87e5fd0..83dcdef1b 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -64,7 +64,7 @@ export enum EpisodeCollectionStatus { Dropped = 3, // 抛弃 } -export interface UserEpisodeCollection { +export interface UserEpisodeStatusItem { eid: number; type: EpisodeCollectionStatus; } diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index cdf567631..51ff2e344 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -5,7 +5,7 @@ import { db, decr, incr, op, type Txn } from '@app/drizzle/db.ts'; import type * as orm from '@app/drizzle/orm.ts'; import * as schema from '@app/drizzle/schema'; -import type { CollectionType, UserEpisodeCollection } from './type'; +import type { CollectionType, UserEpisodeStatusItem } from './type'; import { EpisodeCollectionStatus, getCollectionTypeField } from './type'; /** 更新条目收藏计数,需要在事务中执行 */ @@ -88,7 +88,7 @@ export async function markEpisodesAsWatched( subjectID: number, episodeIDs: number[], ) { - const epStatusList: Record = {}; + const epStatusList: Record = {}; for (const episodeID of episodeIDs) { epStatusList[episodeID] = { eid: episodeID, @@ -125,12 +125,12 @@ export async function markEpisodesAsWatched( } } -export function parseSubjectEpStatus(status: string): Record { - const result: Record = {}; +export function parseSubjectEpStatus(status: string): Record { + const result: Record = {}; if (!status) { return result; } - const epStatusList = php.parse(status) as Record; + const epStatusList = php.parse(status) as Record; for (const x of Object.values(epStatusList)) { result[x.eid] = x; } @@ -140,7 +140,7 @@ export function parseSubjectEpStatus(status: string): Record> { +): Promise> { const [data] = await db .select() .from(schema.chiiEpStatus) From 82d19d4819dfdc86034339b2e031f3fcde5221a6 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:38:04 +0800 Subject: [PATCH 50/65] z --- drizzle/schema.ts | 6 +- lib/tag.ts | 119 ++++++++++++++++++++++++++++ routes/private/routes/collection.ts | 22 ++--- 3 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 lib/tag.ts diff --git a/drizzle/schema.ts b/drizzle/schema.ts index b141ac788..a5bf918de 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -559,7 +559,7 @@ export const chiiSubjectImgs = mysqlTable('chii_subject_imgs', { }); export const chiiSubjectInterests = mysqlTable('chii_subject_interests', { - id: int('interest_id').autoincrement().notNull(), + id: int('interest_id').autoincrement().notNull().primaryKey(), uid: mediumint('interest_uid').notNull(), subjectID: mediumint('interest_subject_id').notNull(), subjectType: smallint('interest_subject_type').notNull(), @@ -650,11 +650,11 @@ export const chiiSubjectPosts = mysqlTable('chii_subject_posts', { }); export const chiiTagIndex = mysqlTable('chii_tag_neue_index', { - id: mediumint('tag_id').autoincrement().notNull(), + id: mediumint('tag_id').autoincrement().notNull().primaryKey(), name: varchar('tag_name', { length: 30 }).notNull(), cat: tinyint('tag_cat').notNull(), type: tinyint('tag_type').notNull(), - totalCount: mediumint('tag_results').notNull(), + count: mediumint('tag_results').notNull(), createdAt: int('tag_dateline').notNull(), updatedAt: int('tag_lasttouch').notNull(), }); diff --git a/lib/tag.ts b/lib/tag.ts new file mode 100644 index 000000000..2adf3f3e3 --- /dev/null +++ b/lib/tag.ts @@ -0,0 +1,119 @@ +import * as lo from 'lodash-es'; +import { DateTime } from 'luxon'; + +import type { Txn } from '@app/drizzle/db.ts'; +import { op } from '@app/drizzle/db.ts'; +import * as schema from '@app/drizzle/schema.ts'; +import { dam } from '@app/lib/dam'; +import type { SubjectType } from '@app/lib/subject/type.ts'; +import { TagCat } from '@app/lib/subject/type.ts'; + +export function validateTags(tags: string[]): string[] { + let count = 0; + const result: string[] = []; + for (const tag of tags) { + const t = tag.trim().normalize('NFKC'); + if (t.length < 2) { + continue; + } + if (dam.needReview(t)) { + continue; + } + result.push(t); + count++; + if (count >= 10) { + break; + } + } + return lo.uniq(result).sort(); +} + +/** + * 插入用户收藏标签,需要在事务中执行 + * + * @param t - 事务 + * @param uid - 用户ID + * @param sid - 条目ID + * @param stype - 条目类型 + * @param tags - 输入的标签 + * @returns 清理后的标签 + */ +export async function insertUserSubjectTags( + t: Txn, + uid: number, + sid: number, + stype: SubjectType, + tags: string[], +): Promise { + tags = validateTags(tags); + await t + .delete(schema.chiiTagList) + .where( + op.and( + op.eq(schema.chiiTagList.userID, uid), + op.eq(schema.chiiTagList.cat, TagCat.Subject), + op.eq(schema.chiiTagList.type, stype), + op.eq(schema.chiiTagList.mainID, sid), + ), + ); + const existTags = await t + .select() + .from(schema.chiiTagIndex) + .where( + op.and( + op.eq(schema.chiiTagIndex.cat, TagCat.Subject), + op.eq(schema.chiiTagIndex.type, stype), + op.inArray(schema.chiiTagIndex.name, tags), + ), + ); + const tagIDs: number[] = []; + const tagMap = new Map(); + for (const tag of existTags) { + tagMap.set(tag.name, tag.id); + tagIDs.push(tag.id); + } + const insertTags = tags.filter((tag) => !tagMap.has(tag)); + const now = DateTime.now().toUnixInteger(); + const insertResult = await t + .insert(schema.chiiTagIndex) + .values( + insertTags.map((tag) => ({ + name: tag, + cat: TagCat.Subject, + type: stype, + count: 0, + createdAt: now, + updatedAt: now, + })), + ) + .$returningId(); + tagIDs.push(...insertResult.map((r) => r.id)); + await t.insert(schema.chiiTagList).values( + tagIDs.map((id) => ({ + tagID: id, + userID: uid, + cat: TagCat.Subject, + type: stype, + mainID: sid, + createdAt: now, + })), + ); + const counts = await t + .select({ + tagID: schema.chiiTagList.tagID, + count: op.count(schema.chiiTagList.tagID), + }) + .from(schema.chiiTagList) + .where(op.inArray(schema.chiiTagList.tagID, tagIDs)) + .groupBy(schema.chiiTagList.tagID); + for (const count of counts) { + await t + .update(schema.chiiTagIndex) + .set({ + count: count.count, + }) + .where(op.eq(schema.chiiTagIndex.id, count.tagID)) + .limit(1); + } + return tags; +} diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index e09d83914..0a48acf5e 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -18,6 +18,7 @@ import { updateSubjectCollection, updateSubjectRating, } from '@app/lib/subject/utils.ts'; +import { insertUserSubjectTags } from '@app/lib/tag'; import { AsyncTimelineWriter } from '@app/lib/timeline/writer.ts'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -270,23 +271,6 @@ export async function setup(app: App) { privacy = CollectionPrivacy.Ban; } } - tags = tags?.map((t) => t.trim().normalize('NFKC')); - if (tags !== undefined) { - if (tags.length > 10) { - throw new BadRequestError('too many tags'); - } - if (dam.needReview(tags.join(' '))) { - tags = undefined; - } else { - for (const tag of tags) { - if (tag.length < 2) { - throw new BadRequestError('tag too short'); - } - } - // 插入 tag 并生成 tag 字符串 - // tags = TagCore::insertTagsNeue($uid, $_POST['tags'], TagCore::TAG_CAT_SUBJECT, $subject['subject_type_id'], $subject['subject_id']); - } - } await rateLimit(LimitAction.Subject, auth.userID); @@ -295,6 +279,10 @@ export async function setup(app: App) { await db.transaction(async (t) => { let needUpdateRate = false; + if (tags !== undefined) { + tags = await insertUserSubjectTags(t, auth.userID, subjectID, slimSubject.type, tags); + } + const [subject] = await t .select() .from(schema.chiiSubjects) From 2fc30ddcb42a7178ce8cc70d981da7e96f9984af Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:47:20 +0800 Subject: [PATCH 51/65] z --- lib/tag.ts | 63 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index 2adf3f3e3..d39ac9ca6 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -45,6 +45,7 @@ export async function insertUserSubjectTags( stype: SubjectType, tags: string[], ): Promise { + const now = DateTime.now().toUnixInteger(); tags = validateTags(tags); await t .delete(schema.chiiTagList) @@ -56,38 +57,44 @@ export async function insertUserSubjectTags( op.eq(schema.chiiTagList.mainID, sid), ), ); - const existTags = await t - .select() - .from(schema.chiiTagIndex) - .where( - op.and( - op.eq(schema.chiiTagIndex.cat, TagCat.Subject), - op.eq(schema.chiiTagIndex.type, stype), - op.inArray(schema.chiiTagIndex.name, tags), - ), - ); const tagIDs: number[] = []; const tagMap = new Map(); - for (const tag of existTags) { - tagMap.set(tag.name, tag.id); - tagIDs.push(tag.id); + + if (tags.length > 0) { + const existTags = await t + .select() + .from(schema.chiiTagIndex) + .where( + op.and( + op.eq(schema.chiiTagIndex.cat, TagCat.Subject), + op.eq(schema.chiiTagIndex.type, stype), + op.inArray(schema.chiiTagIndex.name, tags), + ), + ); + for (const tag of existTags) { + tagMap.set(tag.name, tag.id); + tagIDs.push(tag.id); + } } + const insertTags = tags.filter((tag) => !tagMap.has(tag)); - const now = DateTime.now().toUnixInteger(); - const insertResult = await t - .insert(schema.chiiTagIndex) - .values( - insertTags.map((tag) => ({ - name: tag, - cat: TagCat.Subject, - type: stype, - count: 0, - createdAt: now, - updatedAt: now, - })), - ) - .$returningId(); - tagIDs.push(...insertResult.map((r) => r.id)); + if (insertTags.length > 0) { + const insertResult = await t + .insert(schema.chiiTagIndex) + .values( + insertTags.map((tag) => ({ + name: tag, + cat: TagCat.Subject, + type: stype, + count: 0, + createdAt: now, + updatedAt: now, + })), + ) + .$returningId(); + tagIDs.push(...insertResult.map((r) => r.id)); + } + await t.insert(schema.chiiTagList).values( tagIDs.map((id) => ({ tagID: id, From b8a4440f33c337253ae6face672c83c75e0f7dc3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:47:58 +0800 Subject: [PATCH 52/65] z --- lib/tag.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index d39ac9ca6..623068efd 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -95,16 +95,19 @@ export async function insertUserSubjectTags( tagIDs.push(...insertResult.map((r) => r.id)); } - await t.insert(schema.chiiTagList).values( - tagIDs.map((id) => ({ - tagID: id, - userID: uid, - cat: TagCat.Subject, - type: stype, - mainID: sid, - createdAt: now, - })), - ); + if (tagIDs.length > 0) { + await t.insert(schema.chiiTagList).values( + tagIDs.map((id) => ({ + tagID: id, + userID: uid, + cat: TagCat.Subject, + type: stype, + mainID: sid, + createdAt: now, + })), + ); + } + const counts = await t .select({ tagID: schema.chiiTagList.tagID, From d413bd69637883ff58195a441952eccf1ac603b3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:57:04 +0800 Subject: [PATCH 53/65] z --- lib/tag.ts | 105 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index 623068efd..e9ea0a132 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -57,47 +57,13 @@ export async function insertUserSubjectTags( op.eq(schema.chiiTagList.mainID, sid), ), ); - const tagIDs: number[] = []; - const tagMap = new Map(); - if (tags.length > 0) { - const existTags = await t - .select() - .from(schema.chiiTagIndex) - .where( - op.and( - op.eq(schema.chiiTagIndex.cat, TagCat.Subject), - op.eq(schema.chiiTagIndex.type, stype), - op.inArray(schema.chiiTagIndex.name, tags), - ), - ); - for (const tag of existTags) { - tagMap.set(tag.name, tag.id); - tagIDs.push(tag.id); - } - } - - const insertTags = tags.filter((tag) => !tagMap.has(tag)); - if (insertTags.length > 0) { - const insertResult = await t - .insert(schema.chiiTagIndex) - .values( - insertTags.map((tag) => ({ - name: tag, - cat: TagCat.Subject, - type: stype, - count: 0, - createdAt: now, - updatedAt: now, - })), - ) - .$returningId(); - tagIDs.push(...insertResult.map((r) => r.id)); - } + const tagIDs = await ensureTags(t, TagCat.Subject, stype, tags); + const tids = Object.values(tagIDs).sort(); - if (tagIDs.length > 0) { + if (tids.length > 0) { await t.insert(schema.chiiTagList).values( - tagIDs.map((id) => ({ + tids.map((id) => ({ tagID: id, userID: uid, cat: TagCat.Subject, @@ -108,6 +74,11 @@ export async function insertUserSubjectTags( ); } + await updateTagResult(t, tids); + return tags; +} + +export async function updateTagResult(t: Txn, tagIDs: number[]) { const counts = await t .select({ tagID: schema.chiiTagList.tagID, @@ -116,14 +87,64 @@ export async function insertUserSubjectTags( .from(schema.chiiTagList) .where(op.inArray(schema.chiiTagList.tagID, tagIDs)) .groupBy(schema.chiiTagList.tagID); - for (const count of counts) { + for (const item of counts) { await t .update(schema.chiiTagIndex) .set({ - count: count.count, + count: item.count, }) - .where(op.eq(schema.chiiTagIndex.id, count.tagID)) + .where(op.eq(schema.chiiTagIndex.id, item.tagID)) .limit(1); } - return tags; +} + +export async function ensureTags( + t: Txn, + cat: TagCat, + type: number, + tags: string[], +): Promise> { + const tagIDs: Record = {}; + if (tags.length === 0) { + return tagIDs; + } + + const existTags = await t + .select() + .from(schema.chiiTagIndex) + .where( + op.and( + op.eq(schema.chiiTagIndex.cat, cat), + op.eq(schema.chiiTagIndex.type, type), + op.inArray(schema.chiiTagIndex.name, tags), + ), + ); + for (const tag of existTags) { + tagIDs[tag.name] = tag.id; + } + + const now = DateTime.now().toUnixInteger(); + const insertTags = tags.filter((tag) => !tagIDs[tag]); + if (insertTags.length > 0) { + const insertResult = await t + .insert(schema.chiiTagIndex) + .values( + insertTags.map((tag) => ({ + name: tag, + cat: cat, + type: type, + count: 0, + createdAt: now, + updatedAt: now, + })), + ) + .$returningId(); + for (const [idx, r] of insertResult.entries()) { + const tag = insertTags[idx]; + if (tag) { + tagIDs[tag] = r.id; + } + } + } + return tagIDs; } From 0934f9f83d91fd785c0468356c87a1d073a6816a Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:57:45 +0800 Subject: [PATCH 54/65] z --- lib/tag.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index e9ea0a132..621524e40 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -72,9 +72,8 @@ export async function insertUserSubjectTags( createdAt: now, })), ); + await updateTagResult(t, tids); } - - await updateTagResult(t, tids); return tags; } From 06a39a94cc8a7b6e9246b2a4ec167e063d22cbbd Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:58:12 +0800 Subject: [PATCH 55/65] z --- lib/tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tag.ts b/lib/tag.ts index 621524e40..571199c0b 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -45,7 +45,6 @@ export async function insertUserSubjectTags( stype: SubjectType, tags: string[], ): Promise { - const now = DateTime.now().toUnixInteger(); tags = validateTags(tags); await t .delete(schema.chiiTagList) @@ -62,6 +61,7 @@ export async function insertUserSubjectTags( const tids = Object.values(tagIDs).sort(); if (tids.length > 0) { + const now = DateTime.now().toUnixInteger(); await t.insert(schema.chiiTagList).values( tids.map((id) => ({ tagID: id, From 9e7a930cd8e1261d5bb189ebbbfc35b01c5a2aa8 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 21:58:57 +0800 Subject: [PATCH 56/65] z --- lib/tag.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tag.ts b/lib/tag.ts index 571199c0b..f0b6b75d5 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -78,6 +78,7 @@ export async function insertUserSubjectTags( } export async function updateTagResult(t: Txn, tagIDs: number[]) { + const now = DateTime.now().toUnixInteger(); const counts = await t .select({ tagID: schema.chiiTagList.tagID, @@ -91,6 +92,7 @@ export async function updateTagResult(t: Txn, tagIDs: number[]) { .update(schema.chiiTagIndex) .set({ count: item.count, + updatedAt: now, }) .where(op.eq(schema.chiiTagIndex.id, item.tagID)) .limit(1); From 3db5db4051db0f5609ab9ef2d1476f95427ef4a6 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:04:34 +0800 Subject: [PATCH 57/65] z --- lib/tag.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index f0b6b75d5..0f82abd88 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -9,7 +9,6 @@ import type { SubjectType } from '@app/lib/subject/type.ts'; import { TagCat } from '@app/lib/subject/type.ts'; export function validateTags(tags: string[]): string[] { - let count = 0; const result: string[] = []; for (const tag of tags) { const t = tag.trim().normalize('NFKC'); @@ -20,8 +19,7 @@ export function validateTags(tags: string[]): string[] { continue; } result.push(t); - count++; - if (count >= 10) { + if (result.length >= 10) { break; } } From e6dd994b7f2726b86a5dc255f52aaf42946f8f8c Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:05:23 +0800 Subject: [PATCH 58/65] z --- lib/tag.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index 0f82abd88..526210c1f 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -1,4 +1,3 @@ -import * as lo from 'lodash-es'; import { DateTime } from 'luxon'; import type { Txn } from '@app/drizzle/db.ts'; @@ -9,7 +8,7 @@ import type { SubjectType } from '@app/lib/subject/type.ts'; import { TagCat } from '@app/lib/subject/type.ts'; export function validateTags(tags: string[]): string[] { - const result: string[] = []; + const result = new Set(); for (const tag of tags) { const t = tag.trim().normalize('NFKC'); if (t.length < 2) { @@ -18,12 +17,12 @@ export function validateTags(tags: string[]): string[] { if (dam.needReview(t)) { continue; } - result.push(t); - if (result.length >= 10) { + result.add(t); + if (result.size >= 10) { break; } } - return lo.uniq(result).sort(); + return [...result].sort(); } /** From 98a57b6decabed4ca19c900190d1dee0b919f764 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:15:29 +0800 Subject: [PATCH 59/65] z --- lib/subject/type.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/subject/type.ts b/lib/subject/type.ts index 83dcdef1b..713d96f00 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -76,6 +76,8 @@ export enum PersonType { export enum TagCat { Subject = 0, + Blog = 1, + Doujin = 2, Meta = 3, } From 6233572a371c18f00b341ffcef4bc5a68d39f47e Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:19:10 +0800 Subject: [PATCH 60/65] z --- lib/subject/type.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/subject/type.ts b/lib/subject/type.ts index 713d96f00..95d3916b4 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -75,9 +75,16 @@ export enum PersonType { } export enum TagCat { + /** 条目, 对应的 type: 条目类型 */ Subject = 0, - Blog = 1, + + /** 入口, 对应的 type: blog = 1 */ + Entry = 1, + + /** 同人, 对应的 type: dounin = 1 和 club = 2 */ Doujin = 2, + + /** 条目 wiki, 对应的 type: 条目类型 */ Meta = 3, } From b8242635580d274664a9caecd5fb7dbadb53ecfa Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:20:56 +0800 Subject: [PATCH 61/65] z --- lib/subject/index.ts | 3 ++- lib/subject/type.ts | 14 -------------- lib/tag.ts | 15 ++++++++++++++- lib/types/fetcher.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/subject/index.ts b/lib/subject/index.ts index bbc94d991..769a55781 100644 --- a/lib/subject/index.ts +++ b/lib/subject/index.ts @@ -18,12 +18,13 @@ import { RevType } from '@app/lib/orm/entity/index.ts'; import * as ormold from '@app/lib/orm/index.ts'; import { SubjectImageRepo, SubjectRepo } from '@app/lib/orm/index.ts'; import { extractDate } from '@app/lib/subject/date.ts'; +import { TagCat } from '@app/lib/tag.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; import { DATE } from '@app/lib/utils/date.ts'; import { matchExpected } from '@app/lib/wiki.ts'; import { getSubjectPlatforms } from '@app/vendor'; -import { SubjectType, TagCat } from './type.ts'; +import { SubjectType } from './type.ts'; export const InvalidWikiSyntaxError = createError( 'INVALID_SYNTAX_ERROR', diff --git a/lib/subject/type.ts b/lib/subject/type.ts index 95d3916b4..e2bf2998a 100644 --- a/lib/subject/type.ts +++ b/lib/subject/type.ts @@ -74,20 +74,6 @@ export enum PersonType { Person = 'prsn', } -export enum TagCat { - /** 条目, 对应的 type: 条目类型 */ - Subject = 0, - - /** 入口, 对应的 type: blog = 1 */ - Entry = 1, - - /** 同人, 对应的 type: dounin = 1 和 club = 2 */ - Doujin = 2, - - /** 条目 wiki, 对应的 type: 条目类型 */ - Meta = 3, -} - export enum SubjectSort { Rank = 'rank', Trends = 'trends', diff --git a/lib/tag.ts b/lib/tag.ts index 526210c1f..14ca60a0c 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -5,7 +5,20 @@ import { op } from '@app/drizzle/db.ts'; import * as schema from '@app/drizzle/schema.ts'; import { dam } from '@app/lib/dam'; import type { SubjectType } from '@app/lib/subject/type.ts'; -import { TagCat } from '@app/lib/subject/type.ts'; + +export enum TagCat { + /** 条目, 对应的 type: 条目类型 */ + Subject = 0, + + /** 入口, 对应的 type: blog = 1 */ + Entry = 1, + + /** 同人, 对应的 type: dounin = 1 和 club = 2 */ + Doujin = 2, + + /** 条目 wiki, 对应的 type: 条目类型 */ + Meta = 3, +} export function validateTags(tags: string[]): string[] { const result = new Set(); diff --git a/lib/types/fetcher.ts b/lib/types/fetcher.ts index 766095f34..d7f717fc2 100644 --- a/lib/types/fetcher.ts +++ b/lib/types/fetcher.ts @@ -21,8 +21,8 @@ import { type SubjectFilter, SubjectSort, SubjectType, - TagCat, } from '@app/lib/subject/type.ts'; +import { TagCat } from '@app/lib/tag.ts'; import { getSubjectTrendingKey } from '@app/lib/trending/subject.ts'; import { type TrendingItem, TrendingPeriod } from '@app/lib/trending/type.ts'; import { getSlimCacheKey as getUserSlimCacheKey } from '@app/lib/user/cache.ts'; From 38a2f3bd77591ac2c5cf0921b2ad8984f1f585fe Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:24:57 +0800 Subject: [PATCH 62/65] z --- lib/tag.ts | 27 ++++++++++++++------------- routes/private/routes/collection.ts | 11 +++++++++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/tag.ts b/lib/tag.ts index 14ca60a0c..9e50d364d 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -4,7 +4,6 @@ import type { Txn } from '@app/drizzle/db.ts'; import { op } from '@app/drizzle/db.ts'; import * as schema from '@app/drizzle/schema.ts'; import { dam } from '@app/lib/dam'; -import type { SubjectType } from '@app/lib/subject/type.ts'; export enum TagCat { /** 条目, 对应的 type: 条目类型 */ @@ -43,16 +42,18 @@ export function validateTags(tags: string[]): string[] { * * @param t - 事务 * @param uid - 用户ID - * @param sid - 条目ID - * @param stype - 条目类型 + * @param cat - 标签分类 + * @param type - 标签类型 + * @param mid - 条目 ID 等 * @param tags - 输入的标签 * @returns 清理后的标签 */ -export async function insertUserSubjectTags( +export async function insertUserTags( t: Txn, uid: number, - sid: number, - stype: SubjectType, + cat: TagCat, + type: number, + mid: number, tags: string[], ): Promise { tags = validateTags(tags); @@ -61,13 +62,13 @@ export async function insertUserSubjectTags( .where( op.and( op.eq(schema.chiiTagList.userID, uid), - op.eq(schema.chiiTagList.cat, TagCat.Subject), - op.eq(schema.chiiTagList.type, stype), - op.eq(schema.chiiTagList.mainID, sid), + op.eq(schema.chiiTagList.cat, cat), + op.eq(schema.chiiTagList.type, type), + op.eq(schema.chiiTagList.mainID, mid), ), ); - const tagIDs = await ensureTags(t, TagCat.Subject, stype, tags); + const tagIDs = await ensureTags(t, cat, type, tags); const tids = Object.values(tagIDs).sort(); if (tids.length > 0) { @@ -76,9 +77,9 @@ export async function insertUserSubjectTags( tids.map((id) => ({ tagID: id, userID: uid, - cat: TagCat.Subject, - type: stype, - mainID: sid, + cat, + type, + mainID: mid, createdAt: now, })), ); diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index 0a48acf5e..e2af9ad8a 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -18,7 +18,7 @@ import { updateSubjectCollection, updateSubjectRating, } from '@app/lib/subject/utils.ts'; -import { insertUserSubjectTags } from '@app/lib/tag'; +import { insertUserTags, TagCat } from '@app/lib/tag'; import { AsyncTimelineWriter } from '@app/lib/timeline/writer.ts'; import * as convert from '@app/lib/types/convert.ts'; import * as fetcher from '@app/lib/types/fetcher.ts'; @@ -280,7 +280,14 @@ export async function setup(app: App) { let needUpdateRate = false; if (tags !== undefined) { - tags = await insertUserSubjectTags(t, auth.userID, subjectID, slimSubject.type, tags); + tags = await insertUserTags( + t, + auth.userID, + TagCat.Subject, + slimSubject.type, + subjectID, + tags, + ); } const [subject] = await t From 58a9b7ed5c6ce1a9cbcd852d16ced8d10aa96c6b Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:26:20 +0800 Subject: [PATCH 63/65] z --- lib/tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tag.ts b/lib/tag.ts index 9e50d364d..d2fd45bd7 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -12,7 +12,7 @@ export enum TagCat { /** 入口, 对应的 type: blog = 1 */ Entry = 1, - /** 同人, 对应的 type: dounin = 1 和 club = 2 */ + /** 同人, 对应的 type: doujin = 1 和 club = 2 */ Doujin = 2, /** 条目 wiki, 对应的 type: 条目类型 */ From 29dbedb6a72f5be7e13d900edd1b93da4b43659c Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 10 Feb 2025 22:26:41 +0800 Subject: [PATCH 64/65] z --- lib/tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tag.ts b/lib/tag.ts index d2fd45bd7..053356212 100644 --- a/lib/tag.ts +++ b/lib/tag.ts @@ -15,7 +15,7 @@ export enum TagCat { /** 同人, 对应的 type: doujin = 1 和 club = 2 */ Doujin = 2, - /** 条目 wiki, 对应的 type: 条目类型 */ + /** Wiki, 对应的 type: 条目类型 */ Meta = 3, } From c281ef8680ab5300821dc5da0d962b3036dbcd10 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Tue, 11 Feb 2025 19:14:24 +0800 Subject: [PATCH 65/65] z --- lib/subject/utils.ts | 11 ++++++++++- routes/private/routes/collection.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/subject/utils.ts b/lib/subject/utils.ts index 51ff2e344..93e085981 100644 --- a/lib/subject/utils.ts +++ b/lib/subject/utils.ts @@ -87,6 +87,7 @@ export async function markEpisodesAsWatched( userID: number, subjectID: number, episodeIDs: number[], + revertOthers = false, ) { const epStatusList: Record = {}; for (const episodeID of episodeIDs) { @@ -104,7 +105,15 @@ export async function markEpisodesAsWatched( if (current?.status) { const oldList = parseSubjectEpStatus(current.status); for (const x of Object.values(oldList)) { - if (!episodeIDs.includes(x.eid)) { + if (episodeIDs.includes(x.eid)) { + continue; + } + if (revertOthers && x.type === EpisodeCollectionStatus.Done) { + epStatusList[x.eid] = { + eid: x.eid, + type: EpisodeCollectionStatus.None, + }; + } else { epStatusList[x.eid] = x; } } diff --git a/routes/private/routes/collection.ts b/routes/private/routes/collection.ts index e2af9ad8a..dfa01951f 100644 --- a/routes/private/routes/collection.ts +++ b/routes/private/routes/collection.ts @@ -175,7 +175,7 @@ export async function setup(app: App) { if (episodeIDs.length === 0) { break; } - await markEpisodesAsWatched(t, auth.userID, subjectID, episodeIDs); + await markEpisodesAsWatched(t, auth.userID, subjectID, episodeIDs, true); break; } case SubjectType.Book: {