-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add api for subject collect (#944)
- Loading branch information
Showing
13 changed files
with
800 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import * as php from '@trim21/php-serialize'; | ||
import { DateTime } from 'luxon'; | ||
|
||
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, UserEpisodeStatusItem } from './type'; | ||
import { EpisodeCollectionStatus, getCollectionTypeField } from './type'; | ||
|
||
/** 更新条目收藏计数,需要在事务中执行 */ | ||
export async function updateSubjectCollection( | ||
t: Txn, | ||
subjectID: number, | ||
newType: CollectionType, | ||
oldType?: CollectionType, | ||
) { | ||
if (oldType && oldType === newType) { | ||
return; | ||
} | ||
const toUpdate: Record<string, op.SQL> = {}; | ||
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', | ||
'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 = getRatingField(newRate); | ||
const oldField = getRatingField(oldRate); | ||
const toUpdate: Record<string, op.SQL> = {}; | ||
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); | ||
} | ||
|
||
/** 标记条目剧集为已观看,需要在事务中执行 */ | ||
export async function markEpisodesAsWatched( | ||
t: Txn, | ||
userID: number, | ||
subjectID: number, | ||
episodeIDs: number[], | ||
revertOthers = false, | ||
) { | ||
const epStatusList: Record<number, UserEpisodeStatusItem> = {}; | ||
for (const episodeID of episodeIDs) { | ||
epStatusList[episodeID] = { | ||
eid: episodeID, | ||
type: EpisodeCollectionStatus.Done, | ||
}; | ||
} | ||
const [current] = await t | ||
.select() | ||
.from(schema.chiiEpStatus) | ||
.where( | ||
op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)), | ||
); | ||
if (current?.status) { | ||
const oldList = parseSubjectEpStatus(current.status); | ||
for (const x of Object.values(oldList)) { | ||
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; | ||
} | ||
} | ||
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: userID, | ||
sid: subjectID, | ||
status: newStatus, | ||
updatedAt: DateTime.now().toUnixInteger(), | ||
}); | ||
} | ||
} | ||
|
||
export function parseSubjectEpStatus(status: string): Record<number, UserEpisodeStatusItem> { | ||
const result: Record<number, UserEpisodeStatusItem> = {}; | ||
if (!status) { | ||
return result; | ||
} | ||
const epStatusList = php.parse(status) as Record<number, UserEpisodeStatusItem>; | ||
for (const x of Object.values(epStatusList)) { | ||
result[x.eid] = x; | ||
} | ||
return result; | ||
} | ||
|
||
export async function getEpStatus( | ||
userID: number, | ||
subjectID: number, | ||
): Promise<Record<number, UserEpisodeStatusItem>> { | ||
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.status); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
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'; | ||
|
||
export enum TagCat { | ||
/** 条目, 对应的 type: 条目类型 */ | ||
Subject = 0, | ||
|
||
/** 入口, 对应的 type: blog = 1 */ | ||
Entry = 1, | ||
|
||
/** 同人, 对应的 type: doujin = 1 和 club = 2 */ | ||
Doujin = 2, | ||
|
||
/** Wiki, 对应的 type: 条目类型 */ | ||
Meta = 3, | ||
} | ||
|
||
export function validateTags(tags: string[]): string[] { | ||
const result = new Set<string>(); | ||
for (const tag of tags) { | ||
const t = tag.trim().normalize('NFKC'); | ||
if (t.length < 2) { | ||
continue; | ||
} | ||
if (dam.needReview(t)) { | ||
continue; | ||
} | ||
result.add(t); | ||
if (result.size >= 10) { | ||
break; | ||
} | ||
} | ||
return [...result].sort(); | ||
} | ||
|
||
/** | ||
* 插入用户收藏标签,需要在事务中执行 | ||
* | ||
* @param t - 事务 | ||
* @param uid - 用户ID | ||
* @param cat - 标签分类 | ||
* @param type - 标签类型 | ||
* @param mid - 条目 ID 等 | ||
* @param tags - 输入的标签 | ||
* @returns 清理后的标签 | ||
*/ | ||
export async function insertUserTags( | ||
t: Txn, | ||
uid: number, | ||
cat: TagCat, | ||
type: number, | ||
mid: number, | ||
tags: string[], | ||
): Promise<string[]> { | ||
tags = validateTags(tags); | ||
await t | ||
.delete(schema.chiiTagList) | ||
.where( | ||
op.and( | ||
op.eq(schema.chiiTagList.userID, uid), | ||
op.eq(schema.chiiTagList.cat, cat), | ||
op.eq(schema.chiiTagList.type, type), | ||
op.eq(schema.chiiTagList.mainID, mid), | ||
), | ||
); | ||
|
||
const tagIDs = await ensureTags(t, cat, type, tags); | ||
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, | ||
userID: uid, | ||
cat, | ||
type, | ||
mainID: mid, | ||
createdAt: now, | ||
})), | ||
); | ||
await updateTagResult(t, tids); | ||
} | ||
return tags; | ||
} | ||
|
||
export async function updateTagResult(t: Txn, tagIDs: number[]) { | ||
const now = DateTime.now().toUnixInteger(); | ||
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 item of counts) { | ||
await t | ||
.update(schema.chiiTagIndex) | ||
.set({ | ||
count: item.count, | ||
updatedAt: now, | ||
}) | ||
.where(op.eq(schema.chiiTagIndex.id, item.tagID)) | ||
.limit(1); | ||
} | ||
} | ||
|
||
export async function ensureTags( | ||
t: Txn, | ||
cat: TagCat, | ||
type: number, | ||
tags: string[], | ||
): Promise<Record<string, number>> { | ||
const tagIDs: Record<string, number> = {}; | ||
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; | ||
} |
Oops, something went wrong.