缴费
This commit is contained in:
1 parent
a30477bb58
commit
67863e7ebf
16 files changed
+211
-57
No files matched your search
@@ -20,7 +20,7 @@
|
|||||||
- 自主注册可由超级管理员随时开启或关闭;开启后系统直接生成固定报名号
|
- 自主注册可由超级管理员随时开启或关闭;开启后系统直接生成固定报名号
|
||||||
- 资料审核状态与管理员审核意见
|
- 资料审核状态与管理员审核意见
|
||||||
- 查看开放考试并自主选择多个报考科目
|
- 查看开放考试并自主选择多个报考科目
|
||||||
- 查看报名、缴费和审核状态
|
- 查看报名审核、应缴金额、缴费状态及班级负责人确认记录
|
||||||
- 准考证生成状态、开放时间与下载
|
- 准考证生成状态、开放时间与下载
|
||||||
- 已发布成绩查询,并可按科目提交成绩复议、查看审批进度与结论
|
- 已发布成绩查询,并可按科目提交成绩复议、查看审批进度与结论
|
||||||
- 通知公告中心
|
- 通知公告中心
|
||||||
@@ -30,7 +30,9 @@
|
|||||||
- 超级、校级、班级三级管理员,同一级支持多个账号
|
- 超级、校级、班级三级管理员,同一级支持多个账号
|
||||||
- 超级管理员管理全局事务,并可监督、修改、退回全部审批流程
|
- 超级管理员管理全局事务,并可监督、修改、退回全部审批流程
|
||||||
- 校级管理员管理本校班级、班级管理员、考生和报名流程,按班级批量申领报名号,并提交本校考点、考场档案变更
|
- 校级管理员管理本校班级、班级管理员、考生和报名流程,按班级批量申领报名号,并提交本校考点、考场档案变更
|
||||||
- 班级管理员可查看并审批本班考生、报名与成绩复议流程,成绩录入仍仅限超级管理员
|
- 班级管理员可查看并审批本班考生、报名与成绩复议流程,并在线下收款后确认考生已缴费;成绩录入仍仅限超级管理员
|
||||||
|
- 报名终审与缴费确认相互独立,系统不接入支付 SDK;仅考生所属班级负责人可确认缴费,并记录确认人和时间
|
||||||
|
- 超级、校级、班级管理员均可按各自数据范围查看及导出 Excel 缴费名单
|
||||||
- 考生信息修改、考试报名、成绩复议、批量报名号申领、考点考场变更使用可配置的多步骤审批流程
|
- 考生信息修改、考试报名、成绩复议、批量报名号申领、考点考场变更使用可配置的多步骤审批流程
|
||||||
- 班级和校级审批自动限定到考生所属班级、学校;同范围多名管理员按当前待办与历史分配量自动均分
|
- 班级和校级审批自动限定到考生所属班级、学校;同范围多名管理员按当前待办与历史分配量自动均分
|
||||||
- 当前处理人可将流程转交给同范围的同级管理员
|
- 当前处理人可将流程转交给同范围的同级管理员
|
||||||
@@ -85,7 +87,7 @@ npm start
|
|||||||
|
|
||||||
打开 <http://127.0.0.1:4173>。
|
打开 <http://127.0.0.1:4173>。
|
||||||
|
|
||||||
本地开发无需额外配置,首次运行会自动创建 `data/exam.sqlite` 和完整关系型数据库结构。当前数据库结构版本为 v14;v14 增加考试不可逆归档与成绩永久锁定。开发阶段可直接运行 `npm run reset-db` 重建完整数据库,不要求兼容旧数据。
|
本地开发无需额外配置,首次运行会自动创建 `data/exam.sqlite` 和完整关系型数据库结构。当前数据库结构版本为 v15;v15 将线下缴费确认从报名审核中拆分,并增加缴费确认人、确认时间及名单导出。开发阶段可直接运行 `npm run reset-db` 重建完整数据库,不要求兼容旧数据;低于 v15 的开发库会提示重建,不执行兼容迁移。
|
||||||
|
|
||||||
需要清空并重建本地演示库时运行 `npm run reset-db`。省市区县下拉数据位于 `src/data/china-regions.mjs`,当前版本为国家地名信息库截至 2025-12-31 的三级快照,并补入和康县(653228)与和安县(653229);从新版 CSV 更新时可运行 `node scripts/build-regions.mjs <CSV路径> src/data/china-regions.mjs`。
|
需要清空并重建本地演示库时运行 `npm run reset-db`。省市区县下拉数据位于 `src/data/china-regions.mjs`,当前版本为国家地名信息库截至 2025-12-31 的三级快照,并补入和康县(653228)与和安县(653229);从新版 CSV 更新时可运行 `node scripts/build-regions.mjs <CSV路径> src/data/china-regions.mjs`。
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ npm start
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
测试使用独立临时 SQLite 数据库,覆盖固定报名号跨考试复用、首次登录强制改密、完整资料补录、自主注册开关、三级管理员数据范围、本校班级与班级管理员管理、多级审批、同级转交、校级按班级批量申领与终审原子建号、结构化考点考场及变更审批、多资源 Excel 导入导出、多科目报名、独立科目及格规则、成绩 Excel 预览后原子提交、五级准考证混编、四种号码规则、多科目同考点、成绩复议、校班严格匹配和多人均分。
|
测试使用独立临时 SQLite 数据库,覆盖固定报名号跨考试复用、首次登录强制改密、完整资料补录、自主注册开关、三级管理员数据范围、本校班级与班级管理员管理、多级审批、同级转交、校级按班级批量申领与终审原子建号、班级负责人缴费确认、三级缴费名单导出、结构化考点考场及变更审批、多资源 Excel 导入导出、多科目报名、独立科目及格规则、成绩 Excel 预览后原子提交、五级准考证混编、四种号码规则、多科目同考点、成绩复议、校班严格匹配和多人均分。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from './src/client/api.mjs';
|
import { api } from './src/client/api.mjs';
|
||||||
import { createAdminViews } from './src/client/admin-views.mjs';
|
import { createAdminViews, numberSegmentMeta } from './src/client/admin-views.mjs';
|
||||||
import { createCandidateViews } from './src/client/candidate-views.mjs';
|
import { createCandidateViews } from './src/client/candidate-views.mjs';
|
||||||
import { createPublicViews } from './src/client/public-views.mjs';
|
import { createPublicViews } from './src/client/public-views.mjs';
|
||||||
import { state } from './src/client/state.mjs';
|
import { state } from './src/client/state.mjs';
|
||||||
@@ -182,6 +182,12 @@ document.addEventListener('click', async event => {
|
|||||||
toast(archived ? '账户已归档' : '账户已恢复', `${data.scopeLabel} · ${data.count} 个账户状态已更新`); return renderRoute();
|
toast(archived ? '账户已归档' : '账户已恢复', `${data.scopeLabel} · ${data.count} 个账户状态已更新`); return renderRoute();
|
||||||
}
|
}
|
||||||
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
|
if (action === 'review-registration') return openRegistrationReview(target.dataset.id);
|
||||||
|
if (action === 'confirm-payment') {
|
||||||
|
if (!window.confirm(`确认已收到 ${target.dataset.name} 的“${target.dataset.exam}”考试费用吗?`)) return;
|
||||||
|
await api(`/api/admin/payments/${target.dataset.id}`, { method: 'PATCH' });
|
||||||
|
toast('缴费已确认', '考生端状态、确认人和确认时间已同步记录');
|
||||||
|
return renderRoute();
|
||||||
|
}
|
||||||
if (action === 'excel-download') {
|
if (action === 'excel-download') {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (target.dataset.template === '1') query.set('template', '1');
|
if (target.dataset.template === '1') query.set('template', '1');
|
||||||
|
|||||||
+23
-9
@@ -57,7 +57,7 @@ function buildSeedOperations(state) {
|
|||||||
const nullable = value => value == null || value === '' ? null : value;
|
const nullable = value => value == null || value === '' ? null : value;
|
||||||
|
|
||||||
add(
|
add(
|
||||||
'UPDATE schema_metadata SET schema_version = 14, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
|
'UPDATE schema_metadata SET schema_version = 15, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
|
||||||
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
|
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
|
||||||
state.meta?.createdAt || new Date().toISOString()
|
state.meta?.createdAt || new Date().toISOString()
|
||||||
);
|
);
|
||||||
@@ -147,11 +147,12 @@ function buildSeedOperations(state) {
|
|||||||
for (const registration of state.registrations) {
|
for (const registration of state.registrations) {
|
||||||
add(
|
add(
|
||||||
`INSERT INTO registrations (
|
`INSERT INTO registrations (
|
||||||
id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note,
|
id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note,
|
||||||
registration_number, number_rule_id
|
registration_number, number_rule_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
registration.id, registration.userId, registration.examId, registration.status,
|
registration.id, registration.userId, registration.examId, registration.status,
|
||||||
registration.paymentStatus, registration.createdAt, nullable(registration.reviewedAt), nullable(registration.reviewNote),
|
registration.paymentStatus, nullable(registration.paidAt), nullable(registration.paidBy), registration.createdAt,
|
||||||
|
nullable(registration.reviewedAt), nullable(registration.reviewNote),
|
||||||
nullable(registration.registrationNumber), nullable(registration.numberRuleId)
|
nullable(registration.registrationNumber), nullable(registration.numberRuleId)
|
||||||
);
|
);
|
||||||
for (const subjectId of registration.subjectIds) {
|
for (const subjectId of registration.subjectIds) {
|
||||||
@@ -546,6 +547,8 @@ function stateFromRows(rows) {
|
|||||||
subjectIds: registrationSubjects.get(row.id) || [],
|
subjectIds: registrationSubjects.get(row.id) || [],
|
||||||
status: row.status,
|
status: row.status,
|
||||||
paymentStatus: row.payment_status,
|
paymentStatus: row.payment_status,
|
||||||
|
paidAt: row.paid_at || null,
|
||||||
|
paidBy: row.paid_by || null,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
reviewedAt: row.reviewed_at,
|
reviewedAt: row.reviewed_at,
|
||||||
reviewNote: row.review_note || '',
|
reviewNote: row.review_note || '',
|
||||||
@@ -957,11 +960,12 @@ function createRepository({ client, location, read, transaction, close }) {
|
|||||||
async createRegistration(registration, instance, action) {
|
async createRegistration(registration, instance, action) {
|
||||||
const operations = [operation(
|
const operations = [operation(
|
||||||
`INSERT INTO registrations (
|
`INSERT INTO registrations (
|
||||||
id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note,
|
id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note,
|
||||||
registration_number, number_rule_id
|
registration_number, number_rule_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
registration.id, registration.userId, registration.examId, registration.status,
|
registration.id, registration.userId, registration.examId, registration.status,
|
||||||
registration.paymentStatus, registration.createdAt, optional(registration.reviewedAt), optional(registration.reviewNote),
|
registration.paymentStatus, optional(registration.paidAt), optional(registration.paidBy), registration.createdAt,
|
||||||
|
optional(registration.reviewedAt), optional(registration.reviewNote),
|
||||||
optional(registration.registrationNumber), optional(registration.numberRuleId)
|
optional(registration.registrationNumber), optional(registration.numberRuleId)
|
||||||
)];
|
)];
|
||||||
for (const subjectId of registration.subjectIds) {
|
for (const subjectId of registration.subjectIds) {
|
||||||
@@ -992,6 +996,15 @@ function createRepository({ client, location, read, transaction, close }) {
|
|||||||
auditOperation(log)
|
auditOperation(log)
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
async updateRegistrationPayment(registration, log) {
|
||||||
|
await transaction([
|
||||||
|
operation(
|
||||||
|
'UPDATE registrations SET payment_status = ?, paid_at = ?, paid_by = ? WHERE id = ?',
|
||||||
|
registration.paymentStatus, optional(registration.paidAt), optional(registration.paidBy), registration.id
|
||||||
|
),
|
||||||
|
auditOperation(log)
|
||||||
|
]);
|
||||||
|
},
|
||||||
async processWorkflow(instance, action, business, log) {
|
async processWorkflow(instance, action, business, log) {
|
||||||
const operations = [
|
const operations = [
|
||||||
operation(
|
operation(
|
||||||
@@ -1012,9 +1025,10 @@ function createRepository({ client, location, read, transaction, close }) {
|
|||||||
} else if (instance.businessType === 'registration_review') {
|
} else if (instance.businessType === 'registration_review') {
|
||||||
operations.push(operation(
|
operations.push(operation(
|
||||||
`UPDATE registrations SET
|
`UPDATE registrations SET
|
||||||
status = ?, payment_status = ?, reviewed_at = ?, review_note = ?,
|
status = ?, payment_status = ?, paid_at = ?, paid_by = ?, reviewed_at = ?, review_note = ?,
|
||||||
registration_number = ?, number_rule_id = ? WHERE id = ?`,
|
registration_number = ?, number_rule_id = ? WHERE id = ?`,
|
||||||
business.status, business.paymentStatus, optional(business.reviewedAt), optional(business.reviewNote),
|
business.status, business.paymentStatus, optional(business.paidAt), optional(business.paidBy),
|
||||||
|
optional(business.reviewedAt), optional(business.reviewNote),
|
||||||
optional(business.registrationNumber), optional(business.numberRuleId), business.id
|
optional(business.registrationNumber), optional(business.numberRuleId), business.id
|
||||||
));
|
));
|
||||||
} else if (instance.businessType === 'center_change') {
|
} else if (instance.businessType === 'center_change') {
|
||||||
|
|||||||
Loaded 3 of 16 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user