Files
competition-website/app/service/news.js
qubiaobiao 459f1adf6d feat(admin): R176-R180 公示表与新闻撤回 + 管理端多管理员权限(C 层)+ 交卷冻结
- R176 成绩公示表引擎(lib/publicitySheet.js + lib/assets/publicity-template.xlsx 官方模板基底)
  与指导教师回填(lib/teacherBackfill.js);赛项名称/编号改为配置项
- R177 新闻「发布 ↔ 撤回」互斥:撤回只置 is_draft=1,不删除数据
- R178 C 层 RBAC:lib/adminPermissions.js(13 权限点 / 70 条路由规则,唯一 SoT)
  + middleware/auth.js 三段判定(默认拒绝 · is_super 单点 · token_version)
  + controller/service/adminUser.js + lib/auditLog.js + model/adminAuditLog.js
- R178 B1 交卷冻结:service.exam.assertNotSubmitted(bizCode 4006 单一判据源)
- R178 B④ 提交口径拆分:formalSubmitted / submitted / answeredCount 三字段
- R178 成绩总览补小组编号:resolveTeamLabels(与选手管理同源,含兜底与三态)
- R181 生产迁移配套:admins +7 列、admin_audit_logs(scripts/migrate_r178_admin_rbac.py)
2026-10-20 11:06:29 +08:00

139 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const Service = require('egg').Service;
/**
* 新闻服务 - NewsService
*
* 封装新闻相关的数据库查询操作。
* 支持按组别过滤、按置顶过滤、分页查询。
*/
class NewsService extends Service {
/**
* 查询新闻列表(支持分页和过滤)
*
* 过滤参数:
* - group: 组别筛选(如 zhongzhi, gaozhi, benke, jiaoshi),同时包含 group='all' 的通用新闻
* - pinned: 是否仅查询置顶新闻(true/false)
*
* 排序规则:pinned DESC, publish_time DESC
*
* @param {Object} params - 查询参数
* @param {string} [params.group] - 目标组别
* @param {string|boolean} [params.pinned] - 是否只查置顶
* @param {number} [params.page=1] - 页码
* @param {number} [params.pageSize=20] - 每页数量
* @returns {Promise<{list: Array, total: number, page: number, pageSize: number}>} 分页结果
*/
async list(params = {}) {
const { ctx } = this;
const { Sequelize } = ctx.app;
const { group, pinned, page, pageSize } = params;
// 分页参数,默认第 1 页,每页 20 条
const currentPage = parseInt(page) || 1;
const currentPageSize = parseInt(pageSize) || 20;
const offset = (currentPage - 1) * currentPageSize;
// 构建查询条件
const where = {
// 🔴 R176:草稿只允许后台可见。成绩发布时会自动生成一条「成绩公示」预览新闻,
// 人工编辑确认前绝不能出现在前台列表(否则等于提前公示未定稿成绩)。
is_draft: 0,
};
// 组别过滤:同时包含指定组别和通用(all)新闻
if (group) {
where.group = {
[Sequelize.Op.in]: [group, 'all'],
};
}
// 置顶过滤
if (pinned !== undefined && pinned !== null && pinned !== '') {
where.pinned = pinned === 'true' || pinned === true;
}
// 构建查询
const queryOptions = {
where,
order: [
['pinned', 'DESC'],
['sort_order', 'ASC'],
['publish_time', 'DESC'],
],
attributes: ['id', 'title', 'content', 'publish_time', 'pinned', 'group', 'tags', 'sort_order', 'created_at', 'updated_at'],
offset,
limit: currentPageSize,
};
// 查询新闻列表和总数
const [list, total] = await Promise.all([
ctx.model.News.findAll(queryOptions),
ctx.model.News.count({ where }),
]);
return {
list,
total,
page: currentPage,
pageSize: currentPageSize,
};
}
/**
* 获取各分组新闻计数(用于首页 Tab 显示数量)
* @returns {Promise<Object>} { all: number, zhongzhi: number, gaozhi: number, benke: number, jiaoshi: number }
*/
async getGroupCounts() {
const { ctx } = this;
const { Sequelize } = ctx.app;
const groups = ['all', 'zhongzhi', 'gaozhi', 'benke', 'jiaoshi'];
const counts = { all: 0, zhongzhi: 0, gaozhi: 0, benke: 0, jiaoshi: 0 };
// 并行查询各分组计数
await Promise.all(groups.map(async group => {
// 🔴 R176:计数口径必须与 list 一致,否则首页 Tab 显示「3 条」点进去只有 2 条
const where = { is_draft: 0 };
// 对于具体组别,同时包含该组别和 all 通用新闻
if (group !== 'all') {
where.group = { [Sequelize.Op.in]: [group, 'all'] };
}
counts[group] = await ctx.model.News.count({ where });
}));
return counts;
}
/**
* 根据 ID 查询新闻详情
*
* @param {number} id - 新闻 ID
* @returns {Promise<Model|null>} 新闻模型对象或 null
*/
async findById(id) {
const { ctx } = this;
// 🔴 R176:草稿不可通过 id 直接访问(前台详情页按 id 拉取 ⇒ 只过滤列表等于没防住)
return await ctx.model.News.findOne({
where: { id, is_draft: 0 },
attributes: ['id', 'title', 'content', 'publish_time', 'pinned', 'group', 'tags', 'sort_order', 'attachment_url', 'attachment_name', 'created_at', 'updated_at'],
});
}
/**
* 批量更新新闻排序
* @param {Array<{id: number, sort_order: number}>} orders - 排序信息数组
* @returns {Promise<void>}
*/
async updateSortOrder(orders) {
const { ctx } = this;
await Promise.all(orders.map(async ({ id, sort_order }) => {
await ctx.model.News.update({ sort_order }, { where: { id } });
}));
}
}
module.exports = NewsService;