776b470457
feat(reaction): 评价一次定终身,提交后不可修改
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 06:15:48 -06:00
94be6d997f
refactor(reaction): 后台统计去掉排序,增加最近评价时间
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 06:15:48 -06:00
e84b25634c
docs: 记录题目点评重写的遗留项
...
评审判定可带着合并的次要项,避免日后重复发现。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 04:15:46 -06:00
f485ff6aac
fix(reaction): materialize paginated queryset before JSON response
...
ReactionStatsAPI called the sync paginate_data() without an
object_serializer, leaving results as a lazy QuerySet. json.dumps()
in JSONResponse.response has no custom encoder for QuerySet, so every
request 500'd. list() the results before wrapping in success().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 04:12:44 -06:00
b679f8eb06
refactor(reaction): 下线 comment app
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 03:55:50 -06:00
700488a36b
feat(reaction): 后台表情统计接口
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 01:45:33 -06:00
203a9a48d0
feat(reaction): 用户端表情接口
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 01:40:11 -06:00
b80145f2b6
feat(reaction): 新增 reaction app 与数据模型
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 01:36:06 -06:00
ff593f0b60
docs: 题目点评重写实现计划
...
七个任务,覆盖后端 reaction app、前端组件与后台统计页,
含服务器人工验证清单。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 01:01:03 -06:00
47c9c8554f
docs: 整理题目点评设计文档
...
正文只讲实现,取舍理由集中到附录。
修正若干处讨论过程遗留的不一致。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 00:55:39 -06:00
133b504743
docs: 题目点评重写设计(评分表单改为表情 reaction)
...
三维评分 + 文字评价改为一排七个表情按钮,点击即表态,
最多选 3 个。新建 reaction app 替换 comment app,老数据丢弃。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 00:53:10 -06:00
e42cfac3ce
feat: 班级号位数不合法直接报错,规则收敛到一处
...
原先班级号位数不对是静默处理的:get_class_name 猜前几位,class_list
根本不校验,填错只表现为登录页班级下拉查不到人。现在两个入口都报错。
- get_class_name 先用 \d+ 抓全再判位数,不合法抛 APIError。不能直接用
CLASS_NAME_RE 匹配,那样 ks251001 会"匹配成功"并悄悄取前 4 位,正是
要避免的猜测
- CreateEditWebsiteConfigSerializer 加 validate_class_list
- 位数规则收敛到 utils/shortcuts 的 CLASS_NAME_MIN/MAX_DIGITS 和
is_valid_class_name,两个调用方和错误文案都从常量拼
不以 ks+数字 开头的用户名(管理员、教师账号、kstest)仍返回 None 不报错,
只校验"看起来想当班级用户名"的。
批量导入的构造循环在 transaction.atomic() 之前,报错时一条都不入库。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 00:15:47 -06:00
625f2466e5
refactor: 统一用户名的 ks 班级前缀处理
...
同一个剥前缀函数在 submission 和 flowchart 的管理端各有一份逐字相同的
拷贝,conf 里还有一份内联写法,合并到 utils/shortcuts.strip_class_prefix。
改名是因为原名 get_real_name 和 UserProfile.real_name 字段、以及三个
serializer 里的同名方法都容易混。
行为上修了两处:
- 剥前缀改用 removeprefix,不再按长度硬切。班级号对不上时原样返回,
旧写法会从中间截出乱码(ks999王五 配 class_name=251 会切成「王五」)
- get_class_name 的正则从 \d+ 收紧到 \d{3,4},与前端
ButtonWithSearch 的 /^ks\d{3,4}/ 对齐。旧的贪婪匹配在姓名部分是纯
数字时会吃掉整串(ks251001 返回 251001 而不是 251)。顺带 re.search
换成 re.match,外层的 startswith 判断并进正则
正常数据(ks251张三、ks2510李四)新旧结果一致,已逐例对拍。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:58:13 -06:00
fda7515263
refactor: 移除注册验证码,删掉 pillow
...
验证码只剩注册一处调用方,一并去掉。
- UserRegisterAPI 的校验、UserRegisterSerializer 的 captcha 字段、
发图的 captcha 路由
- utils/captcha/ 整个包删除,含 Menlo.ttc 和 timesbi.ttf 两个字体
- utils/shortcuts.py 的 img2base64(),唯一调用方是验证码视图
- 依赖删除 pillow(本地实测 PIL/ 5.9M + pillow.libs/ 14M)
注意:注册接口现在只有 SysOptions.allow_register 一个开关,没有限流,
throttling 目前只用在 SubmissionAPI 上。内网自用可以接受,站点若暴露到
公网需要补 IP 维度限流。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:48:00 -06:00
12c12b1840
refactor(submission): 删掉提交接口里走不到的验证码分支
...
captcha 在 CreateSubmissionSerializer 里是 required=False,视图里只有
data.get("captcha") 为真才校验,而前端只在注册表单发验证码,提交代码这
条路径从来不传,分支永远进不去。
验证码现在只服务注册流程。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:35:24 -06:00
927f235537
refactor: 移除邮件功能
...
发信能力整体下线,唯一的调用方(忘记密码)前端也从来没接过。
- utils/shortcuts.py 删 send_email,连带四个 django.core.mail 相关 import
- account/tasks.py 整个删除,里面只有 send_email_async 一个 actor
- 删 ApplyResetPasswordAPI / ResetPasswordAPI 及其路由、serializer,
User.reset_password_token 和 reset_password_token_expire_time 两个
字段(account/0010)
- 删 SMTPAPI / SMTPTestAPI 及其路由、三个 SMTP serializer、
SysOptions.smtp_config
- account/templates/reset_password_email.html
options/0003 数据迁移删掉库里的 smtp_config 行:_init_option 会为每个
OptionKey 建行,key 从代码里去掉后它会变成孤儿,而且配过 SMTP 的话
value 里存着明文密码。
保留 ResetUserPasswordAPI(管理端直接重置密码,不发信,前端在用)和
User.email(注册要填)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:17:48 -06:00
8d35d5c2de
chore: psycopg 改用 binary extra,精简 Dockerfile 的 apt 依赖
...
psycopg[binary] 替代并列声明的 psycopg + psycopg-binary。上游的
binary extra 是 psycopg-binary==<同版本> 的精确 pin,而原来两条各自
带范围,没有东西保证它们同步——单独升级其中一个就可能凑出上游没测过
的组合。解析结果不变,requirements.txt 只多了 extra 自带的
implementation_name != 'pypy' 标记。
apt 依赖从 10 个降到 4 个:
- curl / unzip / openssl:全仓库零引用。解压走 Python zipfile,
健康检查走 xmlrpc,openssl 装的是 CLI 而 Python 要的是 libssl
- libpq5:psycopg_binary.libs 自带 libpq.so.5.18,容器内实测
psycopg.pq.__impl__ == binary
- libjpeg62-turbo:pillow.libs 自带 libjpeg.so.62
- zlib1g:base 镜像的 CPython 已经带着
保留 ca-certificates(smtplib 走 TLS 时读系统信任库,不是 certifi)、
clang-format(submission/utils.py 调用)、passwd(entrypoint 建账号)、
supervisor。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:11:33 -06:00
8557f8a5f5
refactor: python-dateutil 换成标准库
...
- contest 后台创建/编辑:dateutil.parser.parse 换成
datetime.fromisoformat。serializer 里 start_time/end_time 声明的是
DateTimeField,而 validate_serializer 做的是 request.data = s.data,
即重新序列化一遍,所以视图拿到的必定是 DRF 规范化后的 ISO 8601,
dateutil 的宽容解析能力在这里从来没用上
- AI 学情统计:relativedelta 换成 shift_months 辅助函数,复刻月末
收缩语义(1月31日 + 1个月 = 2月28/29日)。_parse_duration 的配置
由 delta 对象改为 rewind/advance 可调用,周档和月档统一
删除依赖:python-dateutil、six
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:05:50 -06:00
989b33d827
refactor: 移除 2FA 功能与三个冗余依赖
...
2FA 早已是死代码:TOTP 相关端点标注为前端未调用,前端唯一的
two_factor_auth 是硬编码 false,没有任何入口能打开它。
- 删除 TwoFactorAuthAPI、CheckTFARequiredAPI、_totp* 辅助函数、
TwoFactorAuthCodeSerializer 及各 serializer 的 tfa_code 字段
- 删除管理端写 two_factor_auth/tfa_token 的分支。登录已不再校验
TOTP,若保留写入路径,置 True 会变成静默的安全降级
- 删除 User.two_factor_auth / tfa_token 字段(迁移 0009)
- 移除 django-dbconn-retry:仅挂在 INSTALLED_APPS,代码零引用。
DATABASES 未配置 CONN_MAX_AGE(默认 0),本就没有持久连接需要
重连修复。要开持久连接用 Django 自带的 CONN_HEALTH_CHECKS
- requests 换成 httpx(openai 已经引入):删除废弃的
ReleaseNotesAPI,judge dispatcher 显式传 timeout=None 以保持
requests 原本的无超时行为,判题请求是同步等结果的
依赖净减 5 个:otpauth、qrcode、requests、charset-normalizer、
django-dbconn-retry
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 23:00:45 -06:00
7be5975d88
feat(problemset): 用户奖章接口带出所属题单
...
成就页的奖章列表只有奖章本身,看不出来自哪个题单。UserBadgeSerializer
补一个 problemset 字段,查询改 select_related("badge__problemset") —— 这是
异步视图,不预加载会在异步上下文里发同步查询。
2026-08-05 13:53:03 -06:00
8f5fee1719
docs(achievement): 部署手册补充阈值调法与实际配置状态
...
计划文档里 17 条的参考表已经跟线上对不上(现 38 条),标明以后台为准。
补上"阈值只能从严往松调"这条硬约束和分布查询命令——contest_joined
最高只有 7、min_ac_code_chars 最小值 8,都是拍脑袋定阈值踩出来的坑。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 11:10:33 -06:00
e9c7793714
feat(achievement): 添加按难度的 AC 题数指标,删除 min_ac_code_chars
...
新增 mid_ac_count / hard_ac_count,去重统计中等和困难难度的 AC 题数。
在此之前所有成就对水题和难题一视同仁,这是最缺的一个维度。
增量走 build_ctx 新增的 problem_difficulty,它只在首次 AC 时才查库——
绝大多数提交都不是首次 AC,无条件预查等于给每次判题白加一条 SQL。
删除 min_ac_code_chars:线上实测 1314 个用户的分布,最小值 8、p5=10,
有道题 8 个字符就能通过,这个指标测的是"谁做过那道水题"而不是
"谁写得简洁",配不出有意义的成就。
自检里写死 min_ac_code_chars 的两处改成按 lte 成就自动发现:
_check_registry 不再要求某个具体指标存在,_check_min_metric_absent
改为遍历所有上架的 lte 成就检查其指标。没有 lte 成就时 SKIP,
将来配了自动开始检查。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 11:02:36 -06:00
c50f7b7269
feat(achievement): 添加早起提交次数指标
...
5:00–7:00 的提交计数,与 midnight_submissions 对称,5 点为分界不重叠。
纯计数指标,不依赖 _ 前缀辅助键,因此无需 recompute_state。
部署后需跑 recompute_achievements,否则存量用户该指标为空。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 10:58:30 -06:00
e1ed78e7d9
feat: 成就摘要最近解锁从 5 个放宽到 10 个
...
个人主页删掉徽章卡片后腾出空间,多展示几个。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 10:27:46 -06:00
92e1c2b75d
refactor(deploy): 容器内 nginx 换成 Caddy
...
TLS 和域名由前面的 Nginx Proxy Manager 负责,容器里这层只做静态文件
和反代,用 Caddy 配置更短,也消掉了两个坑:add_header 不继承、
entrypoint 里 __IP_HEADER__ 的 sed 替换。
- deploy/nginx/ 四个配置文件合并成一个 deploy/caddy/Caddyfile
- Dockerfile 去掉 nginx 包,改从官方镜像拷 Caddy 静态二进制
- supervisord 的 nginx 程序换成 caddy,跑在非特权的 server 账号下
- entrypoint 删掉自签证书、http_locations.conf 软链、IP header sed
行为对齐:body 上限写 200MiB 才等于 nginx 的 200M;心跳和 /assets/
继续不记访问日志;安全头、缓存策略、gzip 覆盖范围保持不变。
两处行为变化:
- 访问日志变成 JSON,路径 /data/log/caddy_access.log
- X-Accel-Buffering 是 nginx 私有头,AI 的 SSE 改由 flush_interval -1
保证不缓冲,上线需实测
FORCE_HTTPS 和 LOWER_IP_HEADER 两个环境变量随之作废,真实 IP 改由
trusted_proxies + {client_ip} 从 NPM 的 XFF 解析。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 09:50:46 -06:00
8d6f4616b9
perf(nginx): 修复 JS 未压缩,静态资源加不可变缓存
...
nginx 1.21.1 起 mime.types 把 .js 标成 text/javascript,而 gzip_types
只写了 application/javascript,导致前端 JS 实际一直在裸传。补全类型列表,
并加上 application/json 让 API 响应也走压缩。
gzip_comp_level 此前用的是默认值 1,改为 6,实测 js+css 总量 3.85M → 3.44M。
/assets/ 下的产物文件名带内容 hash,加 immutable 永久缓存,省掉每次进页面
对 32 个 chunk 的条件请求;index.html 反过来设 no-cache,避免发版后刷不到新版。
location 里出现 add_header 会中断 http 级 add_header 的继承,
两个 block 都重新声明了原有的三个安全头。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 09:11:48 -06:00
502078b51b
update
2026-08-05 08:18:02 -06:00
fbbbe11388
feat: 后台题目列表支持按标签筛选
2026-08-05 06:00:03 -06:00
f37ad5823b
docs: 标签管理点击查看对应题目的实现计划
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 05:55:26 -06:00
a8399ad201
docs: 标签管理点击查看对应题目的设计文档
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-05 05:51:04 -06:00
3a9ab83ba5
style: ruff format 全仓库
...
行宽 180 下把历史遗留的折行表达式合并,无语义改动。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:45:26 -06:00
db104ac091
fix(problem): resolve_tags 的 create 包一层 atomic
...
外层若在事务里,IntegrityError 会中止整个事务,导致回查抛
TransactionManagementError。当前未开 ATOMIC_REQUESTS 所以还没触发,
但仓库多处用了 transaction.atomic(),属于随时会踩的坑。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:29:48 -06:00
5b393a3b4c
feat(problem): 新增批量给题目添加/移除标签接口
...
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:21:08 -06:00
c775ae4b22
feat(problem): 新增标签管理接口
...
列表带题目数,重命名撞名时自动合并并返回受影响题数,删除后清标签缓存。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:18:41 -06:00
ec7cf71ea7
refactor(problem): 标签创建逻辑收敛到 services.resolve_tags
...
原本 4 处题目保存逻辑各自复制了一份 get-or-create,且大小写敏感、有竞态。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:15:36 -06:00
e698ef8bfa
docs: 约束 ruff format 只作用于任务改动文件
...
ruff format . 会重排 50+ 个历史遗留格式的无关文件。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:09:21 -06:00
f0da3de9c0
feat(problem): 标签名去空格并加大小写不敏感唯一约束
...
迁移时先合并已存在的重复标签,题目关系转移到保留的那个。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:07:53 -06:00
6ab336ee8f
docs: 计划改为纯代码实现,跳过需要数据库和浏览器的验证步骤
...
dev_settings 指向的是远程库,本次不跑迁移,运行时验证由用户择机进行。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 04:06:36 -06:00
bffda7aa95
docs: 修正计划中 Task2/Task3 的 import 顺序,避免 ruff F401
...
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 01:23:01 -06:00
196e8a4532
docs: 题目标签重构实施计划
...
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 00:42:56 -06:00
9e0fb40f6c
docs: 题目标签重构设计文档
...
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com >
2026-08-05 00:24:16 -06:00
b5702147b8
feat(achievement): 成就图标从 emoji 换成 iconify 图标名
...
机房的老浏览器缺 emoji 字体,成就图标会渲染成方块。改存 iconify
图标名(noto 集,Noto Color Emoji 的 SVG 版),前端渲染成 SVG,
和浏览器字体无关,视觉上与原 emoji 基本一致。
隐藏成就的遮罩图标一并改成 noto:red-question-mark。
迁移 0002 带 77 条 emoji → noto 映射,覆盖首批成就用到的全部图标;
表里没有的保持原值,前端按纯文本兜底,之后在后台手工补。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 19:20:02 -06:00
cf5c3c5d2f
fix(achievement): 管理接口序列化 create_time 为字符串
...
_serialize 是手写 dict 而非 DRF 序列化器,原始 datetime 交给
JSON 编码器抛 TypeError,导致整个管理接口 500。
表为空时列表接口看着正常(不进循环),一旦有数据就全挂——
生产环境表现为:新建成就实际写库成功,但返回响应时炸掉,
前端弹窗不关、列表刷不出来,看起来像保存失败,于是重复点击建出多条。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 09:56:16 -06:00
1504164ae8
fix(achievement): distinct() 前必须 order_by() 清掉默认排序
...
Submission.Meta 有 ordering=("-create_time",),Django 会把排序字段
并入 DISTINCT,导致 _languages 每条提交各成一行——生产环境实测
某用户 659 条而不是 5 种语言。
recompute 侥幸正确只因 .count() 会清排序,两个实现同一个量却不一致。
三处 distinct() 全部显式加 order_by()。
由 check_achievement_deploy 的自检项发现。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 09:50:19 -06:00
f95c3a8bae
docs: 成就系统上线与运维文档
...
部署步骤、日常运维(加成就/加指标/定期检查)、
出问题的处置、已知未验证部分与遗留问题。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 07:30:51 -06:00
8ada9f72ed
feat(achievement): 添加部署后自检命令
...
只读,覆盖那些错了也不报错的地方:极小值指标对零 AC 用户是否返回 None、
JSONB 阈值比较是否按整数、重算是否重建了增量辅助键、
unlock_count 是否与实际解锁人数一致、是否有疑似配错阈值的成就。
这套系统开发全程无数据库,后端逻辑只经过纸面审查,这些是静态审查抓不到的部分。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 07:27:29 -06:00
8ecfeb5e36
fix(problemset): 奖章判定修复 N+1 与并发冲突并接入通知
2026-08-04 04:40:17 -06:00
1ef681d5e9
fix(achievement): 重算时重建增量辅助键,避免指标回退
2026-08-04 01:39:29 -06:00
72fec2ca61
fix(plan): 重算必须重建增量辅助键
...
原计划断言四个依赖辅助键的指标都受 max 语义保护、重算后不会回退,
实测只有 max_ac_streak_days 的最大值成立。另外三个的 on_submission
完全从辅助结构推导,辅助键一丢下次提交就打回 1,上线当天即触发。
新增 BaseMetric.recompute_state 钩子由指标各自重建。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 01:36:41 -06:00
13e2f6df08
feat(achievement): 添加全量重算管理命令
...
新增 recompute_achievements 管理命令:全量重算所有指标并按补发语义
解锁成就,用于首次上线补发存量用户、指标口径变更后重算,以及增量
逻辑漂移时的安全网。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-04 01:33:20 -06:00