| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- /**
- * data.ts —— mock 数据源(academic-mock-plan §6 数据策略 + §4 行为契约)。
- *
- * 定位:dev 会话内单例内存数据(刷新即重置,不落 localStorage)。承载:
- * - 固定作用域实体(3 校区 / 10 用户,与原型一致);
- * - 账户余额(全局 + 校区,分整数);
- * - 资金流水(每用户 320 条 → 覆盖分页边界:total>200 / pageSize 上限 200 / 越界页空集);
- * - 活动 / 课程排班 / 分账树 / 课酬方案 / 资金工单(工单驱动资金操作);
- * - 幂等存储(同 requestId 重复提交 → repeated=true)+ 余额/流水的可变状态(充值/退费/清零/转结改余额并追加流水)。
- *
- * 契约对齐(academic-frontend-contract-v1.md,冻结):
- * - 流水 type 编码 = §10.2 编码表(100 充值 + / 101,201 消费 − / 207 退费 − / 208 清零 − / 106,206 转结 等);
- * - 金额恒**分**整数存、对外输出**字符串两位小数**(money.ts);
- * - 时间恒 RFC3339 +08:00(time.ts);用户 ID 字符串、业务主键 int64、单号 bizNo 字符串。
- *
- * 本文件仅在 dev + VITE_API_MOCK 下被加载(http.ts 动态 import 门控),不进 dist。
- */
- import { fmt, toCents } from '@/api/mock/money';
- import { rfc3339 } from '@/api/mock/time';
- /* ================= 种子随机(确定性,保证每次会话数据一致) ================= */
- function mulberry32(seed: number): () => number {
- let a = seed | 0;
- return () => {
- a = (a + 0x6d2b79f5) | 0;
- let t = Math.imul(a ^ (a >>> 15), 1 | a);
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
- }
- const rand = mulberry32(20261109);
- const randInt = (min: number, max: number): number => Math.floor(rand() * (max - min + 1)) + min;
- const pick = <T>(arr: readonly T[]): T => arr[Math.floor(rand() * arr.length)];
- /** 基准时刻:2026-11-09T12:00:00+08:00(数据在其前 60 天分布,确定性) */
- const BASE_MS = Date.parse('2026-11-09T12:00:00+08:00');
- /* ================= 作用域实体 ================= */
- export interface MockCampus {
- campusId: number;
- institutionId: number;
- name: string;
- }
- export interface MockUser {
- /** canonical_user_id(ULID 26 位字符串,契约 §1.5) */
- userId: string;
- realName: string;
- personType: string;
- /** 脱敏手机号 */
- phone: string;
- /** 归属校区(mock 简化:每用户主校区) */
- campusId: number;
- role: string;
- }
- export const campuses: MockCampus[] = [
- { campusId: 11, institutionId: 1, name: '滨江校区' },
- { campusId: 12, institutionId: 1, name: '城西校区' },
- { campusId: 13, institutionId: 1, name: '高新校区' },
- ];
- // 契约 §1.5:canonical_user_id 恒 26 位 ULID(E8:原生成 23~24 位,补到 26)。
- const ulid = (n: number): string => `01J2M${String(n).padStart(21, '0')}`;
- export const users: MockUser[] = [
- { userId: ulid(101), realName: '张婷', personType: 'staff', phone: '135****3322', campusId: 11, role: 'teacher' },
- { userId: ulid(102), realName: '李强', personType: 'staff', phone: '135****4477', campusId: 12, role: 'teacher' },
- { userId: ulid(201), realName: '王梓涵', personType: 'student', phone: '139****2211', campusId: 11, role: 'student' },
- { userId: ulid(202), realName: '陈一诺', personType: 'student', phone: '137****6644', campusId: 11, role: 'student' },
- { userId: ulid(203), realName: '李沐宸', personType: 'student', phone: '138****9900', campusId: 12, role: 'student' },
- { userId: ulid(204), realName: '赵雨桐', personType: 'student', phone: '136****5511', campusId: 11, role: 'student' },
- { userId: ulid(205), realName: '刘子墨', personType: 'student', phone: '133****7788', campusId: 12, role: 'student' },
- { userId: ulid(301), realName: '周敏', personType: 'staff', phone: '132****8899', campusId: 11, role: 'academic' },
- { userId: ulid(302), realName: '财务管理员', personType: 'staff', phone: '131****6633', campusId: 11, role: 'finance' },
- { userId: ulid(303), realName: '超级管理员', personType: 'staff', phone: '130****1122', campusId: 11, role: 'super_admin' },
- ];
- /** 合法作用域校验(mock 侧 1004 越界判定:campusId 不在集合或 institutionId 不在 {1,0} → 越界)。 */
- export function inScope(institutionId: number, campusId: number | undefined | null): boolean {
- if (institutionId !== 1 && institutionId !== 0) return false;
- if (campusId === undefined || campusId === null || campusId === 0) return true;
- return campuses.some((c) => c.campusId === campusId);
- }
- export function findUser(userId: string): MockUser | undefined {
- return users.find((u) => u.userId === userId);
- }
- export function findCampus(campusId: number): MockCampus | undefined {
- return campuses.find((c) => c.campusId === campusId);
- }
- /* ================= 账户余额(分整数;全局 + 校区) ================= */
- interface Account {
- totalBalance: number;
- campusBalance: number;
- }
- const accounts = new Map<string, Account>(); // key = `${userId}@${campusId}`
- function accountKey(userId: string, campusId: number): string {
- return `${userId}@${campusId}`;
- }
- function ensureAccount(userId: string, campusId: number, total: number, campus: number): Account {
- const k = accountKey(userId, campusId);
- let a = accounts.get(k);
- if (!a) {
- a = { totalBalance: total, campusBalance: campus };
- accounts.set(k, a);
- }
- return a;
- }
- function getUserTotal(userId: string): number {
- let sum = 0;
- for (const c of campuses) sum += accounts.get(accountKey(userId, c.campusId))?.totalBalance ?? 0;
- return sum;
- }
- /** 初始化账户:学生/老师有正余额(可退/可清零),1 名 0 余额(3102 触发),1 名负余额。 */
- (function seedAccounts() {
- const students = users.filter((u) => u.personType === 'student');
- students.forEach((u, i) => {
- // 王梓涵(201) 滨江:正余额;陈一诺(202) 滨江:0 余额(3102);其余正
- const campusTotal = i === 1 ? 0 : randInt(800, 4000) * 100; // 800~4000 元
- ensureAccount(u.userId, u.campusId, campusTotal, i === 1 ? 0 : Math.floor(campusTotal * 0.6));
- // 部分学生在另一校区也有账户
- if (i % 2 === 0) {
- const other = campuses.find((c) => c.campusId !== u.campusId)!;
- ensureAccount(u.userId, other.campusId, Math.floor(campusTotal * 0.4), 0);
- }
- });
- // 李沐宸(203) 额外给一个负余额校区(3120 清零前置:校区余额>全局 或 余额≤0 场景)
- ensureAccount(ulid(203), 13, -5000, 30000);
- })();
- export function getAccount(userId: string, campusId: number): { total: number; campus: number } {
- const a = accounts.get(accountKey(userId, campusId));
- return { total: a?.totalBalance ?? 0, campus: a?.campusBalance ?? 0 };
- }
- /** 当前作用域内的全局余额(campusId=0 表示机构级聚合)。 */
- export function getGlobalBalance(userId: string, campusId: number): number {
- if (campusId && campusId > 0) return getAccount(userId, campusId).total;
- return getUserTotal(userId);
- }
- /* ================= 资金流水(t_user_expense_log 形态) ================= */
- export interface ExpenseLog {
- id: number;
- userId: string;
- institutionId: number;
- campusId: number;
- /** 流水 type(§10.2 编码表) */
- type: number;
- /** 金额(分,含方向符号:充值/坏账 +,消费/退费/清零 −) */
- money: number;
- balanceAfter: number;
- /** 业务时间(契约 C1:`createAt` 是第三种写法,正确名为 `createTime` ← legacy `create_time`) */
- createTime: string;
- remark: string;
- requestId?: string;
- }
- const LOG_TYPE_REMARK: Record<number, string> = {
- 100: '充值',
- 101: '课程消费(校区)',
- 201: '课程消费(校区)',
- 104: '课程消费(学校)',
- 204: '课程消费(学校)',
- 207: '余额退费',
- 208: '余额清零',
- 106: '转结出',
- 206: '转结入',
- };
- const SEED_TYPES = [100, 101, 101, 201, 104, 207, 208] as const;
- const PER_USER_LOGS = 320; // > 200,覆盖分页边界
- let logSeq = 90000;
- const logs: ExpenseLog[] = [];
- (function seedLogs() {
- for (const u of users) {
- if (u.personType !== 'student') continue; // 仅学生造大量流水
- let running = getAccount(u.userId, u.campusId).total;
- for (let i = 0; i < PER_USER_LOGS; i++) {
- const type = pick(SEED_TYPES);
- // 金额 20~3000 元;充值/转结入 为正,其余为负
- const yuan = randInt(20, 3000);
- // 仅 100(充值)为正;其余消费/退费/清零为负(106/206 转结不入 SEED_TYPES,故不判)
- const positive = type === 100;
- const money = positive ? yuan * 100 : -yuan * 100;
- running = Math.max(running + money, -200000);
- const createTime = rfc3339(BASE_MS - i * 3600_000 * randInt(1, 30));
- logs.push({
- id: ++logSeq,
- userId: u.userId,
- institutionId: 1,
- campusId: u.campusId,
- type,
- money,
- balanceAfter: running,
- createTime,
- remark: LOG_TYPE_REMARK[type] ?? '流水',
- });
- }
- }
- })();
- /** 按筛选条件取**完整**流水集(倒序;分页由 envelope.page 负责)。 */
- export interface LogFilter {
- userId?: string;
- type?: number;
- from?: string;
- to?: string;
- institutionId?: number;
- campusId?: number;
- }
- export function queryLogs(f: LogFilter): ExpenseLog[] {
- const from = f.from ? Date.parse(`${f.from}T00:00:00+08:00`) : null;
- const to = f.to ? Date.parse(`${f.to}T23:59:59.999+08:00`) : null;
- return logs
- .filter((l) => {
- if (f.userId && l.userId !== f.userId) return false;
- if (f.type !== undefined && l.type !== f.type) return false;
- if (f.institutionId && f.institutionId !== 1) return false;
- if (f.campusId && f.campusId !== 0 && l.campusId !== f.campusId) return false;
- const t = Date.parse(l.createTime);
- if (from !== null && t < from) return false;
- if (to !== null && t > to) return false;
- return true;
- })
- .sort((a, b) => (a.createTime < b.createTime ? 1 : a.createTime > b.createTime ? -1 : b.id - a.id));
- }
- /** 追加一条流水(写端点共用;返回新 logId)。 */
- export function appendLog(userId: string, campusId: number, type: number, money: number, remark: string, requestId?: string): ExpenseLog {
- const a = ensureAccount(userId, campusId, 0, 0);
- a.campusBalance += money;
- a.totalBalance += money;
- const log: ExpenseLog = {
- id: ++logSeq,
- userId,
- institutionId: 1,
- campusId,
- type,
- money,
- balanceAfter: a.totalBalance,
- createTime: rfc3339(BASE_MS),
- remark,
- };
- if (requestId) log.requestId = requestId;
- logs.unshift(log); // 新流水置顶(倒序查询天然靠前)
- return log;
- }
- /* ================= 资金工单(t_user_balance_request) ================= */
- export interface BalanceRequest {
- id: number;
- bizNo: string;
- type: number; // 100/101/202/203/300
- userId: string;
- institutionId: number;
- campusId: number;
- /** 仅 type=300(转结)有值,其余 0 */
- toInstitutionId: number;
- toCampusId: number;
- money: number; // 分
- approved: 0 | 1 | -1 | -2; // 0 待审 / 1 通过 / -1 驳回 / -2 撤销
- applicantId: string;
- approverId: string | null;
- remark: string;
- /** 审批意见(驳回必填;端点 15 responseMsg) */
- responseMsg: string;
- /** 申请时间(业务时间 ← request_time;v1.6 B2:`createdAt` 是迁移时生成的新列,不下发) */
- requestTime: string;
- /** 审批时间(业务时间 ← response_time;未审批为 null) */
- responseTime: string | null;
- }
- const requestTypes = [100, 101, 202, 203, 300] as const;
- let reqSeq = 5000;
- const requests: BalanceRequest[] = [];
- (function seedRequests() {
- for (let i = 0; i < 60; i++) {
- const u = pick(users.filter((x) => x.personType === 'student'));
- const approved = pick([0, 0, 1, -1, -2] as const);
- const type = pick(requestTypes);
- const requestTime = rfc3339(BASE_MS - i * 7200_000);
- // type=300(转结)有目标校区,其余无
- const isTransfer = type === 300;
- requests.push({
- id: ++reqSeq,
- bizNo: `REQ${(700000 + i).toString()}`,
- type,
- userId: u.userId,
- institutionId: 1,
- campusId: u.campusId,
- toInstitutionId: isTransfer ? 1 : 0,
- toCampusId: isTransfer ? pick(campuses.filter((c) => c.campusId !== u.campusId)).campusId : 0,
- money: randInt(300, 5000) * 100,
- approved,
- applicantId: u.userId,
- approverId: approved === 0 ? null : ulid(302),
- remark: '工单',
- responseMsg: approved === -1 ? '材料不齐,请补充凭证后重新提交' : '',
- requestTime,
- responseTime: approved === 0 ? null : rfc3339(Date.parse(requestTime) + 3600_000),
- });
- }
- requests.sort((a, b) => (a.requestTime < b.requestTime ? 1 : -1));
- })();
- export function queryRequests(f: { type?: number; approved?: number; userId?: string; institutionId?: number; campusId?: number }): BalanceRequest[] {
- return requests.filter((r) => {
- if (f.type !== undefined && r.type !== f.type) return false;
- if (f.approved !== undefined && r.approved !== f.approved) return false;
- if (f.userId && r.userId !== f.userId) return false;
- if (f.institutionId && f.institutionId !== 1) return false;
- if (f.campusId && f.campusId !== 0 && r.campusId !== f.campusId) return false;
- return true;
- });
- }
- export function findRequest(id: number): BalanceRequest | undefined {
- return requests.find((r) => r.id === id);
- }
- export function createRequest(
- type: number,
- userId: string,
- campusId: number,
- money: number,
- remark: string,
- applicantId: string,
- toInstitutionId = 0,
- toCampusId = 0,
- ): BalanceRequest {
- const r: BalanceRequest = {
- id: ++reqSeq,
- bizNo: `REQ${(700000 + reqSeq).toString()}`,
- type,
- userId,
- institutionId: 1,
- campusId,
- toInstitutionId,
- toCampusId,
- money,
- approved: 0,
- applicantId,
- approverId: null,
- remark,
- responseMsg: '',
- requestTime: rfc3339(BASE_MS),
- responseTime: null,
- };
- requests.unshift(r);
- return r;
- }
- /* ================= 活动(t_courses_activity) ================= */
- export interface ActivitySign {
- id: number;
- activityId: number;
- userId: string;
- status: number; // 0 取消 / 1 报名 / 2 参加 / 3 报课 / 4 过期(Go models_activity.go:31 ActivitySignedCancelled=0)
- recommendUserId: string | null;
- money: number; // 分
- /** 报名业务时间(契约 C1:`createAt`→`createTime`;端点 24/28 signeds 行) */
- createTime: string;
- }
- export interface Activity {
- id: number;
- institutionId: number;
- campusId: number;
- type: 2 | 3 | 4; // 2 充值赠送 / 3 代金券 / 4 老带新(Go models_activity.go:16-20 ActivityTypeRecharge/Voucher/Recommend)
- name: string;
- status: 0 | 1; // 0 停用 / 1 启用(注意:status=0 仍允许报名,契约 §2.2)
- numberSign: number;
- total: number;
- signed: number;
- /** 活动金额(分;v1.6 E6:活动级 `money`,充值赠送额度/老带新奖励额/特殊费用额,区别于 signed 行 money) */
- money: number;
- signable: boolean;
- /** 开始时间(契约 C2:源列 start_time,活动与排班统一 startTime,撤回 startAt 方案) */
- startTime: string;
- /** 结束时间(契约 C2:源列 end_time,统一 endTime) */
- endTime: string;
- signeds: ActivitySign[];
- }
- let signSeq = 3000;
- function mkSign(activityId: number, userId: string, status: number, recommendUserId: string | null, money: number): ActivitySign {
- return { id: ++signSeq, activityId, userId, status, recommendUserId, money, createTime: rfc3339(BASE_MS - randInt(1, 30) * 86400_000) };
- }
- const activities: Activity[] = [
- {
- // type=4 老带新(Go ActivityTypeRecommend);status 覆盖 0取消/1报名/2参加/3报课
- id: 1, institutionId: 1, campusId: 11, type: 4, name: '老带新推荐(数学)', status: 1, numberSign: 5,
- total: 100, signed: 4, money: 20000, signable: true, startTime: rfc3339(BASE_MS - 40 * 86400_000), endTime: rfc3339(BASE_MS + 20 * 86400_000),
- signeds: [
- mkSign(1, ulid(201), 1, null, 20000),
- mkSign(1, ulid(202), 1, ulid(201), 20000),
- mkSign(1, ulid(204), 0, ulid(201), 20000),
- mkSign(1, ulid(203), 2, null, 20000),
- mkSign(1, ulid(205), 3, null, 20000),
- ],
- },
- {
- // type=2 充值赠送(Go ActivityTypeRecharge);status 覆盖 1报名/4过期/0取消
- id: 2, institutionId: 1, campusId: 12, type: 2, name: '充值赠送 10%', status: 1, numberSign: 3,
- total: 200, signed: 2, money: 10000, signable: true, startTime: rfc3339(BASE_MS - 20 * 86400_000), endTime: rfc3339(BASE_MS + 40 * 86400_000),
- signeds: [mkSign(2, ulid(203), 1, null, 10000), mkSign(2, ulid(205), 4, null, 15000), mkSign(2, ulid(204), 0, null, 8000)],
- },
- {
- // type=3 代金券(Go ActivityTypeVoucher);活动 status=0 停用(仍允许报名,契约 §2.2)
- id: 3, institutionId: 1, campusId: 11, type: 3, name: '代金券减免', status: 0, numberSign: 1,
- total: 50, signed: 1, money: 5000, signable: false, startTime: rfc3339(BASE_MS - 60 * 86400_000), endTime: rfc3339(BASE_MS - 5 * 86400_000),
- signeds: [mkSign(3, ulid(202), 1, null, 5000)],
- },
- {
- // type=2 充值赠送;暂无报名
- id: 4, institutionId: 1, campusId: 13, type: 2, name: '新学期充值有礼', status: 1, numberSign: 0,
- total: 150, signed: 0, money: 15000, signable: true, startTime: rfc3339(BASE_MS - 5 * 86400_000), endTime: rfc3339(BASE_MS + 35 * 86400_000),
- signeds: [],
- },
- {
- // type=4 老带新;2 条有效报名
- id: 5, institutionId: 1, campusId: 12, type: 4, name: '老带新推荐(物理)', status: 1, numberSign: 2,
- total: 80, signed: 2, money: 30000, signable: true, startTime: rfc3339(BASE_MS - 15 * 86400_000), endTime: rfc3339(BASE_MS + 25 * 86400_000),
- signeds: [mkSign(5, ulid(203), 1, null, 30000), mkSign(5, ulid(205), 1, ulid(203), 30000)],
- },
- ];
- export function queryActivities(f: { type?: number; status?: number; keyword?: string; institutionId?: number; campusId?: number }): Activity[] {
- return activities.filter((a) => {
- if (f.type !== undefined && a.type !== f.type) return false;
- if (f.status !== undefined && a.status !== f.status) return false;
- if (f.institutionId && f.institutionId !== 1) return false;
- if (f.campusId && f.campusId !== 0 && a.campusId !== f.campusId) return false;
- if (f.keyword && !a.name.includes(f.keyword)) return false;
- return true;
- });
- }
- export function findActivity(id: number): Activity | undefined {
- return activities.find((a) => a.id === id);
- }
- /* ================= 课程 / 排班 / 打卡 ================= */
- export interface CourseRecord {
- id: number;
- courseId: number;
- teacherId: string;
- campusId: number;
- /** 地址(v1.8:源列 `address`;**无 room 列**——地址≠教室号,不得顶替) */
- address: string;
- /** 周(排班规则源列 week VARCHAR(50) DEFAULT '-1';"-1"=全部周 / "0".."6" / "1,3,5" 多值) */
- week: string;
- startTime: string; // RFC3339
- endTime: string;
- status: 0 | 1 | 2; // 0 未开始 / 1 进行中 / 2 已结束
- /** 逐学生(C3:`attendance/payCost`→`type/pay/cost/giveCost`) */
- students: { userId: string; type: number; pay: number; cost: number; giveCost: number }[];
- }
- export interface Course {
- id: number;
- institutionId: number;
- name: string;
- classify: string;
- status: 0 | 1;
- teacherIds: string[];
- records: CourseRecord[];
- }
- let recordSeq = 6000;
- function mkRecord(courseId: number, teacherId: string, campusId: number, address: string, week: string, offsetDays: number, status: 0 | 1 | 2): CourseRecord {
- const start = BASE_MS + offsetDays * 86400_000;
- const present = (on: boolean): number => (status === 2 && on ? 1 : 0); // 已结束课才有出勤 type=1(正常)
- return {
- id: ++recordSeq,
- courseId,
- teacherId,
- campusId,
- address,
- week,
- startTime: rfc3339(start),
- endTime: rfc3339(start + 90 * 60_000),
- status,
- students: [
- { userId: ulid(201), type: present(true), pay: 10000, cost: 10000, giveCost: 0 },
- { userId: ulid(202), type: present(true), pay: 10000, cost: 10000, giveCost: 0 },
- { userId: ulid(204), type: present(false), pay: 10000, cost: 10000, giveCost: 0 },
- ],
- };
- }
- const courses: Course[] = [
- {
- id: 1, institutionId: 1, name: '数学 8 班', classify: '数学', status: 1, teacherIds: [ulid(101)],
- records: [
- mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', -3, 2),
- mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', 0, 0),
- mkRecord(1, ulid(101), 11, '滨江校区 5 楼 502 教室', '2,4', 2, 0),
- mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '-1', 4, 0),
- ],
- },
- {
- id: 2, institutionId: 1, name: '物理 3 班', classify: '物理', status: 1, teacherIds: [ulid(102)],
- records: [mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', -1, 1), mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', 1, 0)],
- },
- {
- id: 3, institutionId: 1, name: '英语 1 班', classify: '英语', status: 0, teacherIds: [ulid(101)],
- records: [mkRecord(3, ulid(101), 11, '滨江校区 4 楼 401 教室', '6', -7, 2)],
- },
- ];
- export function queryCourses(f: { classify?: string; status?: number; keyword?: string; institutionId?: number; campusId?: number }): Course[] {
- return courses.filter((c) => {
- if (f.classify && c.classify !== f.classify) return false;
- if (f.status !== undefined && c.status !== f.status) return false;
- if (f.institutionId && f.institutionId !== 1) return false;
- if (f.keyword && !c.name.includes(f.keyword)) return false;
- if (f.campusId && f.campusId !== 0 && !c.records.some((r) => r.campusId === f.campusId)) return false;
- return true;
- });
- }
- export function findCourse(id: number): Course | undefined {
- return courses.find((c) => c.id === id);
- }
- export function findRecord(courseId: number, recordId: number): CourseRecord | undefined {
- return findCourse(courseId)?.records.find((r) => r.id === recordId);
- }
- /* ================= 分账(Nested Set 树) ================= */
- export interface DivideNode {
- userId: string;
- realName: string;
- parentUserId: string | null;
- /** 层级(根=1;Go MaxDivideDepth=100,业务约束 3 层) */
- layer: number;
- /** 每次分成金额(元;A2:`quota` 是金额非 0~1 比率,源列 DECIMAL(12,2),与 total_income 同族;0.00 = 该链不分成) */
- quota: number;
- cumulativeIncome: number; // 分
- children: DivideNode[];
- }
- const divideTree: DivideNode[] = [
- {
- userId: ulid(303), realName: '超级管理员', parentUserId: null, layer: 1, quota: 500, cumulativeIncome: 0,
- children: [
- {
- userId: ulid(101), realName: '张婷', parentUserId: ulid(303), layer: 2, quota: 150, cumulativeIncome: 1200000,
- children: [
- { userId: ulid(201), realName: '王梓涵', parentUserId: ulid(101), layer: 3, quota: 50, cumulativeIncome: 300000, children: [] },
- { userId: ulid(202), realName: '陈一诺', parentUserId: ulid(101), layer: 3, quota: 0, cumulativeIncome: 0, children: [] }, // quota=0(新节点默认)
- ],
- },
- {
- userId: ulid(102), realName: '李强', parentUserId: ulid(303), layer: 2, quota: 100, cumulativeIncome: 800000,
- children: [{ userId: ulid(203), realName: '李沐宸', parentUserId: ulid(102), layer: 3, quota: 30, cumulativeIncome: 200000, children: [] }],
- },
- ],
- },
- ];
- export function getDivideTree(): DivideNode[] {
- return divideTree;
- }
- export function findDivideNode(userId: string): DivideNode | undefined {
- let found: DivideNode | undefined;
- const walk = (nodes: DivideNode[]): void => {
- for (const n of nodes) {
- if (n.userId === userId) found = n;
- walk(n.children);
- }
- };
- walk(divideTree);
- return found;
- }
- /* ================= 课酬方案(v1.9 两级结构:方案 + 明细) ================= */
- /** 课酬明细行(t_course_payment_detail;金额存分,教龄/比例为 number,percentage 为百分比数 0~100)。 */
- export interface PayrollDetail {
- detailId: number;
- /** 老师(可空;null = 机构级通用档) */
- teacherId: string | null;
- classifyId: number;
- classType: number;
- gradeId: number;
- subjectId: number;
- teacherCert: number;
- /** 教龄区间(年,3 位小数;源列两列,不得合并为单值) */
- teacherExpMin: number;
- teacherExpMax: number;
- /** 课酬比例(百分比数 0~100,1 位小数;A3:非 0~1 小数) */
- percentage: number;
- /** 金额上下限 / 奖励(分) */
- min: number;
- max: number;
- reward: number;
- /** 计算方式名(明细级 name;方案级为 schemeName) */
- displayName: string | null;
- /** 明细级启停(方案级为 actived) */
- detailActived: 0 | 1;
- }
- /** 课酬方案(t_course_payment;两级结构顶层)。 */
- export interface PayrollPlan {
- id: number;
- institutionId: number;
- campusId: number; // 0 = 机构级方案
- schemeName: string;
- effectDate: string;
- /** 方案级启停(整案停用 = 不参与匹配) */
- actived: 0 | 1;
- details: PayrollDetail[];
- }
- let detailSeq = 90000;
- const payrollPlans: PayrollPlan[] = [
- {
- id: 1, institutionId: 1, campusId: 11, schemeName: '滨江校区 2026 秋季课酬方案', effectDate: '2026-09-01', actived: 1,
- details: [
- { detailId: ++detailSeq, teacherId: ulid(101), classifyId: 1, classType: 1, gradeId: 1, subjectId: 1, teacherCert: 1, teacherExpMin: 5, teacherExpMax: 15, percentage: 85, min: 90000, max: 150000, reward: 10000, displayName: '数学 5~15 年教龄', detailActived: 1 },
- { detailId: ++detailSeq, teacherId: null, classifyId: 1, classType: 1, gradeId: -1, subjectId: -1, teacherCert: 0, teacherExpMin: 0, teacherExpMax: 5, percentage: 70, min: 60000, max: 90000, reward: 0, displayName: '数学 5 年内教龄(通用)', detailActived: 1 },
- ],
- },
- {
- id: 2, institutionId: 1, campusId: 12, schemeName: '城西校区 2026 秋季课酬方案', effectDate: '2026-09-01', actived: 1,
- details: [
- { detailId: ++detailSeq, teacherId: ulid(102), classifyId: 2, classType: 1, gradeId: 2, subjectId: 2, teacherCert: 0, teacherExpMin: 3, teacherExpMax: 10, percentage: 80, min: 80000, max: 120000, reward: 8000, displayName: '物理 3~10 年教龄', detailActived: 1 },
- { detailId: ++detailSeq, teacherId: null, classifyId: 2, classType: 1, gradeId: -1, subjectId: -1, teacherCert: 0, teacherExpMin: 0, teacherExpMax: 3, percentage: 60, min: 50000, max: 80000, reward: 0, displayName: '物理 3 年内教龄(通用)', detailActived: 0 },
- ],
- },
- ];
- export function queryPayrollPlans(f: { actived?: number; classifyId?: number }): PayrollPlan[] {
- return payrollPlans.filter((p) => {
- if (f.actived !== undefined && p.actived !== f.actived) return false;
- if (f.classifyId !== undefined && !p.details.some((d) => d.classifyId === f.classifyId)) return false;
- return true;
- });
- }
- export function findPayrollPlan(id: number): PayrollPlan | undefined {
- return payrollPlans.find((p) => p.id === id);
- }
- /* ================= 收支类型字典(v1.9:type 1=支出/2=收入,删 direction;补 id/parentId) ================= */
- export interface ExpenditureType {
- id: number;
- /** 收支方向编码:1=支出 / 2=收入(与流水 type 同事实,两表均无 direction 列) */
- type: number;
- name: string;
- /** 上级类型 id(0 = 顶级) */
- parentId: number;
- }
- export const expenditureTypes: ExpenditureType[] = [
- { id: 1, type: 1, name: '课程消费', parentId: 0 },
- { id: 2, type: 1, name: '课程消费(校区)', parentId: 1 },
- { id: 3, type: 1, name: '课程消费(学校)', parentId: 1 },
- { id: 4, type: 2, name: '线下收入', parentId: 0 },
- { id: 5, type: 2, name: '线上收入', parentId: 0 },
- ];
- export function expenditureTypeOf(id: number): ExpenditureType | undefined {
- return expenditureTypes.find((t) => t.id === id);
- }
- /* ================= 幂等存储(写端点重复提交 → repeated=true) ================= */
- const idempotent = new Map<string, Record<string, unknown>>();
- export function idempotentKey(method: string, requestId: string): string {
- return `${method}:${requestId}`;
- }
- export function takeIdempotent(key: string): Record<string, unknown> | undefined {
- return idempotent.get(key);
- }
- export function setIdempotent(key: string, result: Record<string, unknown>): void {
- idempotent.set(key, result);
- }
- /* ================= 金额/字段便捷校验(端点侧参数校验) ================= */
- export { fmt, toCents };
|