历史上一共 6 条站内信(最后一条 2025-06),发信接口两代前端都没接过,后台页面只有一句 「未完待续」,OJ2 顶栏也没有入口。 0033 是 DROP TABLE,部署要显式放行:OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh, 或 CI 手工触发时勾 allow_destructive。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
734f4aed05
commit
4240a451e4
13 files changed
+3838
-308
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
-- 站内信整条下线(2026-10-13):表里历史上一共 6 条(最后一条 2025-06),发信接口两代前端都
|
||||||
|
-- 没接过,后台页面只是一句「未完待续」,OJ2 的顶栏也没有入口。页面、接口、契约同一个提交里删掉。
|
||||||
|
DROP TABLE "message" CASCADE;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -232,6 +232,13 @@
|
|||||||
"when": 1791821976519,
|
"when": 1791821976519,
|
||||||
"tag": "0032_achievement_cleanup",
|
"tag": "0032_achievement_cleanup",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 33,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791823809856,
|
||||||
|
"tag": "0033_drop_message",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -273,58 +273,6 @@ export const flowchartSubmission = pgTable(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
export const message = pgTable(
|
|
||||||
"message",
|
|
||||||
{
|
|
||||||
id: integer().primaryKey().generatedByDefaultAsIdentity({
|
|
||||||
name: "message_id_seq",
|
|
||||||
startWith: 1,
|
|
||||||
increment: 1,
|
|
||||||
minValue: 1,
|
|
||||||
maxValue: 2147483647,
|
|
||||||
cache: 1,
|
|
||||||
}),
|
|
||||||
message: text().notNull(),
|
|
||||||
createTime: timestamp("create_time", {
|
|
||||||
withTimezone: true,
|
|
||||||
mode: "string",
|
|
||||||
}).notNull(),
|
|
||||||
recipientId: integer("recipient_id").notNull(),
|
|
||||||
senderId: integer("sender_id").notNull(),
|
|
||||||
submissionId: text("submission_id").notNull(),
|
|
||||||
},
|
|
||||||
(table) => [
|
|
||||||
index("message_recipient_time_idx").using(
|
|
||||||
"btree",
|
|
||||||
table.recipientId.asc().nullsLast().op("timestamptz_ops"),
|
|
||||||
table.createTime.asc().nullsLast().op("int4_ops"),
|
|
||||||
),
|
|
||||||
index("message_sender_id_a2a2e825").using(
|
|
||||||
"btree",
|
|
||||||
table.senderId.asc().nullsLast().op("int4_ops"),
|
|
||||||
),
|
|
||||||
index("message_submission_id_2fdf8a47").using(
|
|
||||||
"btree",
|
|
||||||
table.submissionId.asc().nullsLast().op("text_ops"),
|
|
||||||
),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.recipientId],
|
|
||||||
foreignColumns: [user.id],
|
|
||||||
name: "message_recipient_id_2aa5dd76_fk_user_id",
|
|
||||||
}),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.senderId],
|
|
||||||
foreignColumns: [user.id],
|
|
||||||
name: "message_sender_id_a2a2e825_fk_user_id",
|
|
||||||
}),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.submissionId],
|
|
||||||
foreignColumns: [submission.id],
|
|
||||||
name: "message_submission_id_2fdf8a47_fk_submission_id",
|
|
||||||
}).onDelete("cascade"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
export const judgeServer = pgTable("judge_server", {
|
export const judgeServer = pgTable("judge_server", {
|
||||||
id: serial().primaryKey().notNull(),
|
id: serial().primaryKey().notNull(),
|
||||||
hostname: text().notNull(),
|
hostname: text().notNull(),
|
||||||
@@ -903,7 +851,7 @@ export const submission = pgTable(
|
|||||||
* - `since_prev_ms` 是唯一由**服务端**算的一列(距同一用户同一道题上一次提交),
|
* - `since_prev_ms` 是唯一由**服务端**算的一列(距同一用户同一道题上一次提交),
|
||||||
* 客户端伪造不了;这道题的第一次提交为 null。
|
* 客户端伪造不了;这道题的第一次提交为 null。
|
||||||
* - CASCADE 挂在 submission 上、不挂 user:它是提交的附属,提交没了它没有意义,
|
* - CASCADE 挂在 submission 上、不挂 user:它是提交的附属,提交没了它没有意义,
|
||||||
* 人是谁顺着 submission 就能查到。同表的 message / problemset_submission 也是这一档。
|
* 人是谁顺着 submission 就能查到。同表的 problemset_submission 也是这一档。
|
||||||
*/
|
*/
|
||||||
export const submissionTrace = pgTable(
|
export const submissionTrace = pgTable(
|
||||||
"submission_trace",
|
"submission_trace",
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ adminAccountRoutes.delete("/users", requireSuperAdmin, async (c) => {
|
|||||||
// 撞外键说明该用户还有历史数据,应当禁用而不是删除。
|
// 撞外键说明该用户还有历史数据,应当禁用而不是删除。
|
||||||
//
|
//
|
||||||
// 所以 0010 那一批 CASCADE **有意跳过了 user 的绝大多数外键**:成就、表情、题单进度、
|
// 所以 0010 那一批 CASCADE **有意跳过了 user 的绝大多数外键**:成就、表情、题单进度、
|
||||||
// AI 分析、站内信全都继续拦着。只有 user_profile 和 user_stat 走 CASCADE ——
|
// AI 分析全都继续拦着。只有 user_profile 和 user_stat 走 CASCADE ——
|
||||||
// 一个是一对一附属、一个是可重算的统计缓存,都不构成「这人做过什么」的证据。
|
// 一个是一对一附属、一个是可重算的统计缓存,都不构成「这人做过什么」的证据。
|
||||||
// 别顺手把这里也改成全 CASCADE:submission.user_id 压根没有外键(Django 那边就是个
|
// 别顺手把这里也改成全 CASCADE:submission.user_id 压根没有外键(Django 那边就是个
|
||||||
// 裸 IntegerField),全连坐的结果是成就没了、提交却留成孤儿行,一半删一半留。
|
// 裸 IntegerField),全连坐的结果是成就没了、提交却留成孤儿行,一半删一半留。
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
createMessageRequestSchema,
|
|
||||||
exerciseAttemptRequestSchema,
|
exerciseAttemptRequestSchema,
|
||||||
reactionKeySchema,
|
reactionKeySchema,
|
||||||
setReactionRequestSchema,
|
setReactionRequestSchema,
|
||||||
@@ -7,10 +6,7 @@ import {
|
|||||||
type Announcement,
|
type Announcement,
|
||||||
type AnnouncementList,
|
type AnnouncementList,
|
||||||
type AnnouncementListItem,
|
type AnnouncementListItem,
|
||||||
type EmbeddedSubmission,
|
|
||||||
type Exercise,
|
type Exercise,
|
||||||
type Message,
|
|
||||||
type MessageList,
|
|
||||||
type ReactionCounts,
|
type ReactionCounts,
|
||||||
type ReactionState,
|
type ReactionState,
|
||||||
type Tutorial,
|
type Tutorial,
|
||||||
@@ -20,7 +16,7 @@ import {
|
|||||||
import { and, asc, count, desc, eq, inArray, sql } from "drizzle-orm"
|
import { and, asc, count, desc, eq, inArray, sql } from "drizzle-orm"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
|
||||||
import { optionalAuth, requireAuth, requireSuperAdmin, type AppEnv } from "../auth/middleware"
|
import { optionalAuth, requireAuth, type AppEnv } from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, parseBody, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { JudgeStatus } from "../judge/status"
|
||||||
@@ -95,98 +91,6 @@ contentRoutes.get("/announcements/:id", async (c) => {
|
|||||||
} satisfies Announcement)
|
} satisfies Announcement)
|
||||||
})
|
})
|
||||||
|
|
||||||
contentRoutes.get("/messages", requireAuth, async (c) => {
|
|
||||||
const user = c.get("user")!
|
|
||||||
const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 })
|
|
||||||
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
|
||||||
const [totalRows, rows] = await Promise.all([
|
|
||||||
db
|
|
||||||
.select({ value: count() })
|
|
||||||
.from(schema.message)
|
|
||||||
.where(eq(schema.message.recipientId, user.id)),
|
|
||||||
db
|
|
||||||
.select({
|
|
||||||
message: schema.message,
|
|
||||||
sender: schema.user,
|
|
||||||
realName: schema.userProfile.realName,
|
|
||||||
submission: schema.submission,
|
|
||||||
displayId: schema.problem.displayId,
|
|
||||||
})
|
|
||||||
.from(schema.message)
|
|
||||||
.innerJoin(schema.user, eq(schema.message.senderId, schema.user.id))
|
|
||||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
|
||||||
.innerJoin(schema.submission, eq(schema.message.submissionId, schema.submission.id))
|
|
||||||
.innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id))
|
|
||||||
.where(eq(schema.message.recipientId, user.id))
|
|
||||||
.orderBy(desc(schema.message.createTime))
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset),
|
|
||||||
])
|
|
||||||
return success(c, {
|
|
||||||
results: rows.map(
|
|
||||||
({ message, sender, realName, submission, displayId }) =>
|
|
||||||
({
|
|
||||||
id: message.id,
|
|
||||||
sender: sampleUser(sender, realName),
|
|
||||||
createTime: message.createTime,
|
|
||||||
message: message.message,
|
|
||||||
submission: {
|
|
||||||
id: submission.id,
|
|
||||||
createTime: submission.createTime,
|
|
||||||
userId: submission.userId,
|
|
||||||
username: submission.username,
|
|
||||||
code: submission.code,
|
|
||||||
result: submission.result,
|
|
||||||
// info / ip / contestId 三个字段不在 embeddedSubmissionSchema 里,故不传 ——
|
|
||||||
// 对齐旧后端 SubmissionSafeModelSerializer 的 exclude,这三个键不出现在响应中
|
|
||||||
language: submission.language,
|
|
||||||
statisticInfo: asRecord(submission.statisticInfo),
|
|
||||||
// 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号>
|
|
||||||
problemDisplayId: displayId,
|
|
||||||
showLink: true,
|
|
||||||
} satisfies EmbeddedSubmission,
|
|
||||||
}) satisfies Message,
|
|
||||||
),
|
|
||||||
total: totalRows[0]?.value ?? 0,
|
|
||||||
} satisfies MessageList)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发站内信。**目前没有任何前端在调用它** —— 后台那个页面
|
|
||||||
* (apps/web/src/admin/communication/messages.vue)两代前端都只是一句
|
|
||||||
* 「未完待续」的占位,ojnext 里定义过一个 createMessage 但同样零调用,
|
|
||||||
* 已在前端删掉。端点本身是完整实现的,要接 UI 从这里开始。
|
|
||||||
*/
|
|
||||||
contentRoutes.post("/messages", requireSuperAdmin, async (c) => {
|
|
||||||
const user = c.get("user")!
|
|
||||||
const parsed = await parseBody(c, createMessageRequestSchema, "Invalid message payload")
|
|
||||||
if (!parsed.success) return parsed.response
|
|
||||||
if (parsed.data.recipientId === user.id)
|
|
||||||
return failure(c, 400, "invalid-recipient", "Can not send a message to yourself")
|
|
||||||
const [[recipient], [submission]] = await Promise.all([
|
|
||||||
db
|
|
||||||
.select({ id: schema.user.id })
|
|
||||||
.from(schema.user)
|
|
||||||
.where(and(eq(schema.user.id, parsed.data.recipientId), eq(schema.user.isDisabled, false)))
|
|
||||||
.limit(1),
|
|
||||||
db
|
|
||||||
.select({ id: schema.submission.id })
|
|
||||||
.from(schema.submission)
|
|
||||||
.where(eq(schema.submission.id, parsed.data.submissionId))
|
|
||||||
.limit(1),
|
|
||||||
])
|
|
||||||
if (!recipient) return failure(c, 404, "user-not-found", "User does not exist")
|
|
||||||
if (!submission) return failure(c, 404, "submission-not-found", "Submission does not exist")
|
|
||||||
await db.insert(schema.message).values({
|
|
||||||
message: parsed.data.message,
|
|
||||||
createTime: new Date().toISOString(),
|
|
||||||
recipientId: recipient.id,
|
|
||||||
senderId: user.id,
|
|
||||||
submissionId: submission.id,
|
|
||||||
})
|
|
||||||
return success(c, null, 201)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function reactionState(problemId: number, userId: number) {
|
async function reactionState(problemId: number, userId: number) {
|
||||||
const [mine] = await db
|
const [mine] = await db
|
||||||
.select({ type: schema.reaction.type })
|
.select({ type: schema.reaction.type })
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
<template>未完待续</template>
|
|
||||||
<script lang="ts" setup></script>
|
|
||||||
@@ -57,7 +57,6 @@ import type {
|
|||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementListItem,
|
AnnouncementListItem,
|
||||||
Profile,
|
Profile,
|
||||||
Message,
|
|
||||||
SubmissionListItem,
|
SubmissionListItem,
|
||||||
Exercise,
|
Exercise,
|
||||||
ProblemDetail,
|
ProblemDetail,
|
||||||
@@ -380,13 +379,6 @@ export function getAnnouncement(id: number) {
|
|||||||
return api.get<Announcement>(`announcements/${id}`)
|
return api.get<Announcement>(`announcements/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessageList(offset = 0, limit = 10) {
|
|
||||||
// language 的收窄同 getSubmissions,见那里的说明
|
|
||||||
return api.get<{ results: Message[]; total: number }>("messages", {
|
|
||||||
params: { limit, offset },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReaction(problemID: number) {
|
export function getReaction(problemID: number) {
|
||||||
return api.get<ReactionState>(`problems/${problemID}/reaction`)
|
return api.get<ReactionState>(`problems/${problemID}/reaction`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="oj-page message-page">
|
|
||||||
<h2>消息</h2>
|
|
||||||
<n-list v-if="messages.length">
|
|
||||||
<n-list-item :style="{ overflow: 'auto' }" v-for="(item, index) in messages" :key="index">
|
|
||||||
<n-flex size="large" vertical>
|
|
||||||
<n-flex align="center">
|
|
||||||
<div>发送时间</div>
|
|
||||||
<div>{{ parseTime(item.createTime, "YYYY年M月D日 HH:mm:ss") }}</div>
|
|
||||||
<div>发送者</div>
|
|
||||||
<div>{{ item.sender.username }}</div>
|
|
||||||
</n-flex>
|
|
||||||
<n-flex align="center">
|
|
||||||
<div>题目序号</div>
|
|
||||||
<n-button
|
|
||||||
text
|
|
||||||
type="info"
|
|
||||||
@click="router.push('/problem/' + item.submission.problemDisplayId)"
|
|
||||||
>
|
|
||||||
{{ item.submission.problemDisplayId }}
|
|
||||||
</n-button>
|
|
||||||
<n-text :type="JUDGE_STATUS[item.submission.result]['type']">
|
|
||||||
{{ JUDGE_STATUS[item.submission.result]["name"] }}
|
|
||||||
</n-text>
|
|
||||||
<Copy :value="item.submission.code" />
|
|
||||||
</n-flex>
|
|
||||||
<n-code
|
|
||||||
:language="LANGUAGE_FORMAT_VALUE[item.submission.language]"
|
|
||||||
:code="item.submission.code"
|
|
||||||
show-line-numbers
|
|
||||||
/>
|
|
||||||
<div v-html="item.message"></div>
|
|
||||||
</n-flex>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
<n-empty v-else description="没有消息" style="margin: 48px 0"></n-empty>
|
|
||||||
<Pagination v-model:limit="query.limit" v-model:page="query.page" :total="total" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { getMessageList } from "oj/api"
|
|
||||||
import { JUDGE_STATUS, LANGUAGE_FORMAT_VALUE } from "utils/constants"
|
|
||||||
import Copy from "shared/components/Copy.vue"
|
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
|
||||||
import { parseTime } from "utils/functions"
|
|
||||||
import type { Message } from "utils/types"
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const messages = ref<Message[]>([])
|
|
||||||
const total = ref(0)
|
|
||||||
|
|
||||||
const query = reactive({
|
|
||||||
limit: 10,
|
|
||||||
page: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function listMessages() {
|
|
||||||
const offset = (query.page - 1) * query.limit
|
|
||||||
const res = await getMessageList(offset, query.limit)
|
|
||||||
total.value = res.total
|
|
||||||
messages.value = res.results
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(listMessages)
|
|
||||||
watch(query, listMessages, { deep: true })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.message-page h2 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: var(--oj-fs-title);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-page :deep(.n-list-item) {
|
|
||||||
padding: 20px 0;
|
|
||||||
font-size: var(--oj-fs-sec);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -111,11 +111,6 @@ export const ojs: RouteRecordRaw = {
|
|||||||
component: () => import("oj/achievement/index.vue"),
|
component: () => import("oj/achievement/index.vue"),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "message",
|
|
||||||
component: () => import("oj/user/message.vue"),
|
|
||||||
meta: { requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "learn",
|
path: "learn",
|
||||||
name: "learn home",
|
name: "learn home",
|
||||||
@@ -268,12 +263,6 @@ export const admins: RouteRecordRaw = {
|
|||||||
props: true,
|
props: true,
|
||||||
meta: { requiresSuperAdmin: true },
|
meta: { requiresSuperAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "message/list",
|
|
||||||
name: "admin message list",
|
|
||||||
component: () => import("admin/communication/messages.vue"),
|
|
||||||
meta: { requiresSuperAdmin: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "tutorial/list",
|
path: "tutorial/list",
|
||||||
name: "admin tutorial list",
|
name: "admin tutorial list",
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import type {
|
|||||||
RankProfile,
|
RankProfile,
|
||||||
SessionUser,
|
SessionUser,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
EmbeddedSubmission as ContractEmbeddedSubmission,
|
|
||||||
Message as ContractMessage,
|
|
||||||
ProblemDifficulty,
|
ProblemDifficulty,
|
||||||
JudgeStatus,
|
JudgeStatus,
|
||||||
AstRules,
|
AstRules,
|
||||||
@@ -245,12 +243,6 @@ export type Submission = Omit<SubmissionDetail, "result"> & {
|
|||||||
result: SUBMISSION_RESULT
|
result: SUBMISSION_RESULT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 站内信里嵌的提交:problem 是展示题号而非数字 id,且不含 info / ip / contestId。
|
|
||||||
* 契约里已经窄化好了(`embeddedSubmissionSchema`),这里不再重复 Omit + 增补。
|
|
||||||
*/
|
|
||||||
export type EmbeddedSubmission = ContractEmbeddedSubmission
|
|
||||||
|
|
||||||
export type SubmissionListItem = ContractSubmissionListItem
|
export type SubmissionListItem = ContractSubmissionListItem
|
||||||
|
|
||||||
export interface SubmissionListPayload {
|
export interface SubmissionListPayload {
|
||||||
@@ -358,14 +350,6 @@ export type {
|
|||||||
/** 后台编辑表单:请求体 + id(新建时填 0,提交前由 api 层剥掉) */
|
/** 后台编辑表单:请求体 + id(新建时填 0,提交前由 api 层剥掉) */
|
||||||
export type AnnouncementEdit = CreateAnnouncementRequest & { id: number }
|
export type AnnouncementEdit = CreateAnnouncementRequest & { id: number }
|
||||||
|
|
||||||
/**
|
|
||||||
* 站内信。**直接取契约** —— 原来这里把 `submission` 换成前端窄化过的
|
|
||||||
* `EmbeddedSubmission`,但那个窄化已经搬进 `embeddedSubmissionSchema`
|
|
||||||
* (statisticInfo / language 在契约里不再是 unknown),两者双向可赋值、
|
|
||||||
* 完全等价,这层 Omit 已经没有内容。
|
|
||||||
*/
|
|
||||||
export type Message = ContractMessage
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 题目表情。三个类型都直接取自契约 —— 语义 key 必须与后端 reaction/models.py
|
* 题目表情。三个类型都直接取自契约 —— 语义 key 必须与后端 reaction/models.py
|
||||||
* 的 ReactionType 一致(见根 CLAUDE.md),手抄一份迟早对不上。
|
* 的 ReactionType 一致(见根 CLAUDE.md),手抄一份迟早对不上。
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||||
import { embeddedSubmissionSchema } from "./submission"
|
|
||||||
|
|
||||||
export const announcementSchema = z.object({
|
export const announcementSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
@@ -26,25 +25,6 @@ export const announcementListItemSchema = announcementSchema.omit({
|
|||||||
|
|
||||||
export const announcementListSchema = paginatedSchema(announcementListItemSchema)
|
export const announcementListSchema = paginatedSchema(announcementListItemSchema)
|
||||||
|
|
||||||
export const messageSchema = z.object({
|
|
||||||
id: z.number().int(),
|
|
||||||
sender: sampleUserSchema,
|
|
||||||
createTime: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
submission: embeddedSubmissionSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const messageListSchema = paginatedSchema(messageSchema)
|
|
||||||
|
|
||||||
export const createMessageRequestSchema = z.object({
|
|
||||||
recipientId: z.number().int().positive(),
|
|
||||||
submissionId: z.string().min(1),
|
|
||||||
message: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(1024 * 1024),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const reactionKeySchema = z.enum([
|
export const reactionKeySchema = z.enum([
|
||||||
"too_easy",
|
"too_easy",
|
||||||
"too_hard",
|
"too_hard",
|
||||||
@@ -194,14 +174,11 @@ export const exerciseSchema = z.object({
|
|||||||
solved: z.boolean(),
|
solved: z.boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Message = z.infer<typeof messageSchema>
|
|
||||||
export type MessageList = z.infer<typeof messageListSchema>
|
|
||||||
export type Announcement = z.infer<typeof announcementSchema>
|
export type Announcement = z.infer<typeof announcementSchema>
|
||||||
export type AnnouncementListItem = z.infer<typeof announcementListItemSchema>
|
export type AnnouncementListItem = z.infer<typeof announcementListItemSchema>
|
||||||
export type TutorialSummary = z.infer<typeof tutorialSummarySchema>
|
export type TutorialSummary = z.infer<typeof tutorialSummarySchema>
|
||||||
|
|
||||||
export type AnnouncementList = z.infer<typeof announcementListSchema>
|
export type AnnouncementList = z.infer<typeof announcementListSchema>
|
||||||
export type CreateMessageRequest = z.infer<typeof createMessageRequestSchema>
|
|
||||||
export type ReactionKey = z.infer<typeof reactionKeySchema>
|
export type ReactionKey = z.infer<typeof reactionKeySchema>
|
||||||
export type ReactionCounts = z.infer<typeof reactionCountsSchema>
|
export type ReactionCounts = z.infer<typeof reactionCountsSchema>
|
||||||
export type ReactionState = z.infer<typeof reactionStateSchema>
|
export type ReactionState = z.infer<typeof reactionStateSchema>
|
||||||
|
|||||||
@@ -229,24 +229,6 @@ export const submissionDetailSchema = z.object({
|
|||||||
caseSummary: caseSummarySchema,
|
caseSummary: caseSummarySchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 内嵌在别处(目前只有站内信)的提交对象。对齐旧后端的
|
|
||||||
* `SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))` ——
|
|
||||||
* 这些键**根本不出现**,而不是出现但值为空。(`ip` 已随 IP 功能整体删除。)
|
|
||||||
*
|
|
||||||
* 独立成一个 schema 而不是复用 submissionDetailSchema 传空值:形状一致了,
|
|
||||||
* 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这些字段。
|
|
||||||
*/
|
|
||||||
export const embeddedSubmissionSchema = submissionDetailSchema
|
|
||||||
// 留下 problemDisplayId(展示用题号)而不是数字主键:站内信页面拿它拼
|
|
||||||
// `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。
|
|
||||||
.omit({
|
|
||||||
info: true,
|
|
||||||
contestId: true,
|
|
||||||
problemId: true,
|
|
||||||
caseSummary: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result /
|
* 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result /
|
||||||
* status 决定是继续等还是去拉详情。
|
* status 决定是继续等还是去拉详情。
|
||||||
@@ -623,7 +605,6 @@ export type AttemptedStudent = z.infer<typeof attemptedStudentSchema>
|
|||||||
|
|
||||||
export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
|
export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
|
||||||
export type SubmissionList = z.infer<typeof submissionListSchema>
|
export type SubmissionList = z.infer<typeof submissionListSchema>
|
||||||
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
|
|
||||||
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
|
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
|
||||||
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
|
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user