feat(safety): support full CRUD for approved commercial places and dynamic sync to uniapp

这个提交包含在:
xcbyy
2026-10-10 09:52:42 +08:00
父节点 dc19189bc6
当前提交 e36e80c399
共有 6 个文件被更改,包括 266 次插入 和 6 次删除

查看文件

@@ -1439,6 +1439,40 @@ export class MysqlRepository {
return { id, ...data };
}
async updateSafetyPlace(id: string, data: Partial<{ name: string; area: string; category: string; status: string }>): Promise<any> {
const fields: string[] = [];
const values: any[] = [];
if (data.name !== undefined) {
fields.push('name = ?');
values.push(data.name);
}
if (data.area !== undefined) {
fields.push('area = ?');
values.push(data.area);
}
if (data.category !== undefined) {
fields.push('category = ?');
values.push(data.category);
}
if (data.status !== undefined) {
fields.push('status = ?');
values.push(data.status);
}
if (fields.length > 0) {
values.push(id);
await pool.query(`UPDATE safety_places SET ${fields.join(', ')} WHERE id = ?`, values);
}
const [rows]: any = await pool.query('SELECT * FROM safety_places WHERE id = ?', [id]);
return rows[0] || null;
}
async deleteSafetyPlace(id: string): Promise<boolean> {
const [res]: any = await pool.query('DELETE FROM safety_places WHERE id = ?', [id]);
return res.affectedRows > 0;
}
async getSafetyAuditLogs(): Promise<any[]> {
const [rows]: any = await pool.query(`
SELECT

查看文件

@@ -1260,6 +1260,14 @@ app.get('/api/safety/places', async (_req: Request, res: Response) => {
app.post('/api/safety/places', async (req: Request, res: Response) => {
try {
const { name, area, category } = req.body;
if (!name || !area) {
return res.status(400).json({ code: 40000, message: '场所名称和商圈不能为空' });
}
const sec = contentSecurityService.checkText(`${name} ${area}`);
if (!sec.pass) {
return res.status(400).json({ code: sec.code, message: sec.message });
}
const place = await repo.createSafetyPlace(req.body);
res.json({
code: 20000,
@@ -1271,6 +1279,41 @@ app.post('/api/safety/places', async (req: Request, res: Response) => {
}
});
// 管理后台: 修改场所信息
app.put('/api/safety/places/:id', async (req: Request, res: Response) => {
try {
const placeId = String(req.params.id);
const { name, area, category, status } = req.body;
if (name || area) {
const sec = contentSecurityService.checkText(`${name || ''} ${area || ''}`);
if (!sec.pass) {
return res.status(400).json({ code: sec.code, message: sec.message });
}
}
const updated = await repo.updateSafetyPlace(placeId, { name, area, category, status });
if (!updated) {
return res.status(404).json({ code: 40400, message: '场所不存在' });
}
res.json({ code: 20000, data: updated, message: '场所信息已更新' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 管理后台: 删除场所
app.delete('/api/safety/places/:id', async (req: Request, res: Response) => {
try {
const placeId = String(req.params.id);
const ok = await repo.deleteSafetyPlace(placeId);
if (!ok) {
return res.status(404).json({ code: 40400, message: '场所不存在或已被移除' });
}
res.json({ code: 20000, data: null, message: '场所已移除' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
app.get('/api/safety/audit-logs', async (_req: Request, res: Response) => {
try {
const logs = await repo.getSafetyAuditLogs();

查看文件

@@ -0,0 +1,64 @@
// 已读取AGENTS.md
import { ContentSecurityService } from '../src/core/security/content-security.service';
describe('场所白名单准入与敏感词内容安全审查 TDD 自动化测试', () => {
const secService = new ContentSecurityService();
it('1. 合规公开商圈场所白名单准入', () => {
const validPlace = {
name: '三里屯太二酸菜鱼',
area: '朝阳区三里屯商圈',
category: '餐饮美食',
safetyLevel: 'AAA级合规认证',
status: '正常营业'
};
expect(validPlace.name).toBe('三里屯太二酸菜鱼');
expect(validPlace.safetyLevel).toBe('AAA级合规认证');
});
it('2. 录入合规商业场所时内容安全审查放行', () => {
const check = secService.checkText('工体纯KTV 朝阳区工人体育场商圈 量贩KTV');
expect(check.pass).toBe(true);
expect(check.suggest).toBe('pass');
});
it('3. 录入涉黄低俗敏感词场所时系统自动拦截', () => {
const check = secService.checkText('某某丝袜会所 上门特殊服务');
expect(check.pass).toBe(false);
expect(check.code).toBe(87014);
expect(check.label).toBe('VULGAR_PORN');
});
it('4. 场所更新支持正常营业与暂停营业状态变更', () => {
const place = {
id: 'place_001',
name: '三里屯聚宝源铜锅涮肉',
status: '正常营业'
};
const updateStatus = (p: typeof place, newStatus: string) => {
return { ...p, status: newStatus };
};
const updated = updateStatus(place, '暂停营业');
expect(updated.status).toBe('暂停营业');
});
it('5. 移动端动态拉取合规商圈场所列表过滤正常营业项', () => {
const rawPlaces = [
{ id: '1', name: '太二酸菜鱼', area: '朝阳大悦城', status: '正常营业' },
{ id: '2', name: '纯KTV', area: '工体商圈', status: '暂停营业' },
{ id: '3', name: '跳海酒馆', area: '望京麒麟社', status: '正常营业' }
];
const activeZones = rawPlaces
.filter((p) => p.status === '正常营业')
.map((p) => `${p.area} · ${p.name}`);
expect(activeZones.length).toBe(2);
expect(activeZones).toContain('朝阳大悦城 · 太二酸菜鱼');
expect(activeZones).toContain('望京麒麟社 · 跳海酒馆');
expect(activeZones).not.toContain('工体商圈 · 纯KTV');
});
});