背景
- 这两个域把 SQL 与处理器放在同一文件,是全仓仅有的两个零接口测试的后端域。
直接拆会改动 SQL 与参数顺序,而硬约束要求不得变更统计口径 —— 因此按
"可注入化 -> 补契约测试 -> 再提取" 的顺序做。
改动
- ele:拆为 routes.ts(校验与组装)/ repository.ts(全部 SQL)/ model.ts(xlsx 解析、
取值清洗、筛选片段、插入值组装等纯逻辑),并改为 registerEleRoutes(app, deps) 可注入。
- feedback:拆为 routes.ts / repository.ts,同样可注入;建表与截图上传也作为依赖注入。
- 两域各补测试:ele 7 个接口契约 + 6 个模型用例,feedback 9 个接口契约。
mock pool 逐条断言 SQL 文本与参数顺序(含分页、状态白名单、批量插入的 30 列顺序)。
- 架构测试新增一条:已完整分层的三个域(vehicles / ele / feedback)必须有 repository.ts
且 routes.ts 不得出现 SQL。
等价性验证(关键)
- 把改造前的实现从 git 取出,与改造后的实现跑同一批请求(含真实 xlsx 解析路径),
对比每一步落库 SQL、参数、HTTP 状态与响应体:
feedback:6 个场景,SQL + 参数 + 状态完全一致(差异仅 DDL 已移交 db/schema 层)
ele :5 个场景,SQL + 参数 + 状态 + 响应体完全一致(仅随机 batchId/时间戳做掩码)
- 期间的修正:曾把 /mine 与 /list 的列集合统一,二者实际不同(管理列表多 user_id/user_name),
已按原样保留;测试同时锁定了这一差异。
lint / test(161) / build 全绿,可达性 0 未引用文件。
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
import type { ResultSetHeader, RowDataPacket } from 'mysql2';
|
|
|
|
/**
|
|
* 反馈表的全部 SQL 出口。
|
|
*
|
|
* SQL 与参数顺序在此固定:路由只做校验与组装,不再拼 SQL。
|
|
* 接口由 mock pool 的契约测试锁定(见 routes.test.ts)。
|
|
*/
|
|
|
|
/** 只需要 query 能力的最小依赖,便于在测试中注入 mock。 */
|
|
export interface Database {
|
|
query<T = any>(sql: string, values?: unknown[]): Promise<[T, ...any[]]>;
|
|
}
|
|
|
|
// 写入时间戳一律用东八区 CST,避免依赖 MySQL/容器时区设置。
|
|
const CST_NOW = `DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR)`;
|
|
|
|
// 两个列表接口的列集合刻意不同:管理列表额外返回 user_id / user_name。
|
|
const USER_LIST_COLUMNS = `id, type, module, content, contact, screenshots, status,
|
|
reply_content, reply_user, reply_at, created_at`;
|
|
const ADMIN_LIST_COLUMNS = `id, type, module, content, contact, screenshots, user_id, user_name, status,
|
|
reply_content, reply_user, reply_at, created_at`;
|
|
|
|
export interface NewFeedback {
|
|
type: string;
|
|
module: string | null;
|
|
content: string;
|
|
contact: string | null;
|
|
screenshotsJson: string;
|
|
userId: string | null;
|
|
userName: string | null;
|
|
userAgent: string | null;
|
|
}
|
|
|
|
export async function insertFeedback(db: Database, row: NewFeedback): Promise<number> {
|
|
const [result] = await db.query<ResultSetHeader>(
|
|
`INSERT INTO bi_user_feedback (type, module, content, contact, screenshots, user_id, user_name, user_agent, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${CST_NOW})`,
|
|
[row.type, row.module, row.content, row.contact, row.screenshotsJson, row.userId, row.userName, row.userAgent],
|
|
);
|
|
return result.insertId;
|
|
}
|
|
|
|
/** 当前用户的反馈历史(最多 100 条)。 */
|
|
export async function listFeedbackForUser(db: Database, userId: string): Promise<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(
|
|
`SELECT ${USER_LIST_COLUMNS}
|
|
FROM bi_user_feedback
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 100`,
|
|
[userId],
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
/** 管理列表:可选状态过滤 + 条数上限。 */
|
|
export async function listFeedbackForAdmin(
|
|
db: Database,
|
|
options: { status: string | null; limit: number },
|
|
): Promise<RowDataPacket[]> {
|
|
const where: string[] = ['1=1'];
|
|
const params: (string | number)[] = [];
|
|
if (options.status) {
|
|
where.push('status = ?');
|
|
params.push(options.status);
|
|
}
|
|
const [rows] = await db.query<RowDataPacket[]>(
|
|
`SELECT ${ADMIN_LIST_COLUMNS}
|
|
FROM bi_user_feedback
|
|
WHERE ${where.join(' AND ')}
|
|
ORDER BY created_at DESC
|
|
LIMIT ?`,
|
|
[...params, options.limit],
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
export interface FeedbackUpdate {
|
|
status?: string;
|
|
/** 传 undefined 表示不更新回复;传 null/'' 表示清空。 */
|
|
reply?: string | null;
|
|
replyUser?: string | null;
|
|
}
|
|
|
|
/** 管理更新。返回 false 表示没有任何可更新字段。 */
|
|
export async function updateFeedback(db: Database, id: number, update: FeedbackUpdate): Promise<boolean> {
|
|
const fields: string[] = [];
|
|
const params: (string | number | null)[] = [];
|
|
if (update.status !== undefined) {
|
|
fields.push('status = ?');
|
|
params.push(update.status);
|
|
}
|
|
if (update.reply !== undefined) {
|
|
fields.push('reply_content = ?', 'reply_user = ?', `reply_at = ${CST_NOW}`);
|
|
params.push(update.reply || null, update.replyUser ?? null);
|
|
}
|
|
if (fields.length === 0) return false;
|
|
await db.query(
|
|
`UPDATE bi_user_feedback SET ${fields.join(', ')} WHERE id = ?`,
|
|
[...params, id],
|
|
);
|
|
return true;
|
|
}
|