From 082ec1c3857f7d6e3d2a8e574167ed79010e7e92 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Thu, 22 Oct 2026 10:35:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(order):=20=E7=A6=81=E6=AD=A2=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E6=8E=A5=E8=87=AA=E5=B7=B1=E5=8F=91=E5=B8=83=E7=9A=84?= =?UTF-8?q?=E8=AE=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - claimOrder 校验 buyer_id 与大神登录用户 id,相同直接 403「不能接自己发布的订单」 - 派单池支持 excludeBuyerId:已登录大神不会看到自己发布的待接单 - 115 套件/753 用例全绿;线上实测自接 403、他人可抢、自己的单不出现在自己的池子 --- server/src/db/repository.ts | 17 ++++++++++++++--- server/src/index.ts | 6 ++++-- server/tests/order-activity-integrity.spec.ts | 2 +- server/tests/platform-dispatch.spec.ts | 7 +++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index ac53c40..c1fffa3 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -1940,7 +1940,8 @@ export class MysqlRepository { } } - async getOrdersPool(opts: { fromLat?: number | null; fromLng?: number | null; radiusKm?: number | null } = {}): Promise { + async getOrdersPool(opts: { fromLat?: number | null; fromLng?: number | null; radiusKm?: number | null; excludeBuyerId?: number | null } = {}): Promise { + const excludeBuyerId = Number(opts.excludeBuyerId) || 0; const [rows]: any = await pool.query(` SELECT id, order_no as orderNo, client_name as clientName, @@ -1951,9 +1952,10 @@ export class MysqlRepository { FROM orders WHERE status = '待接单' AND (claimed_by IS NULL OR claimed_by = 0) + ${excludeBuyerId ? 'AND buyer_id <> ?' : ''} ORDER BY created_at DESC LIMIT 50 - `); + `, excludeBuyerId ? [excludeBuyerId] : []); // 默认池保障 const maskClientName = (n: any): string => { @@ -2043,7 +2045,7 @@ export class MysqlRepository { // orders.expert_id 统一存「大神档案 id」;claimed_by 存登录用户 id const companionIdForOrder = comp ? Number(comp.id) || expertId : expertId; - const [curRows]: any = await pool.query('SELECT id, status, claimed_by FROM orders WHERE id = ? OR order_no = ? LIMIT 1', [orderId, orderId]); + const [curRows]: any = await pool.query('SELECT id, buyer_id, status, claimed_by FROM orders WHERE id = ? OR order_no = ? LIMIT 1', [orderId, orderId]); const cur = curRows && curRows[0]; if (!cur) { const err: any = new Error('订单不存在'); @@ -2051,6 +2053,15 @@ export class MysqlRepository { err.code = 40400; throw err; } + // 禁止自己接自己发布的订单(刷单/套券风险) + const buyerId = Number(cur.buyer_id) || 0; + const actorUserId = Number(comp && comp.user_id) || expertId; + if (buyerId > 0 && buyerId === actorUserId) { + const err: any = new Error('不能接自己发布的订单'); + err.status = 403; + err.code = 40300; + throw err; + } // 状态闸门:仅「待接单」可被抢单,严禁抢已在履约/已服务/已核销的订单 if (String(cur.status) !== '待接单') { const err: any = new Error(`当前订单状态(${cur.status})不可接单(仅待接单可抢)`); diff --git a/server/src/index.ts b/server/src/index.ts index eb6b1dd..2336235 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1490,9 +1490,11 @@ app.get('/api/orders/pool', async (req: Request, res: Response) => { const lng = Number(req.query.lng); const radius = Number(req.query.radius); const hasFrom = Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0); + // 已登录的大神不展示自己发布的订单(禁止自接自单) + const excludeBuyerId = resolveRequestUserId(req) || 0; // 先取不带半径的全量池:为历史无坐标订单做地理编码兜底(每轮最多 5 条,结果回写入库) - let pre = await repo.getOrdersPool(hasFrom ? { fromLat: lat, fromLng: lng } : {}); + let pre = await repo.getOrdersPool({ fromLat: hasFrom ? lat : null, fromLng: hasFrom ? lng : null, excludeBuyerId }); if (hasFrom) { const missing = pre.filter((o: any) => !o.hasGeo && o.location).slice(0, 5); let changed = false; @@ -1503,7 +1505,7 @@ app.get('/api/orders/pool', async (req: Request, res: Response) => { changed = true; } } - if (changed) pre = await repo.getOrdersPool({ fromLat: lat, fromLng: lng }); + if (changed) pre = await repo.getOrdersPool({ fromLat: lat, fromLng: lng, excludeBuyerId }); } const list = hasFrom && Number.isFinite(radius) && radius > 0 diff --git a/server/tests/order-activity-integrity.spec.ts b/server/tests/order-activity-integrity.spec.ts index bb772ca..4646443 100644 --- a/server/tests/order-activity-integrity.spec.ts +++ b/server/tests/order-activity-integrity.spec.ts @@ -50,7 +50,7 @@ describe('订单与活动资金一致性护栏 (报名超卖 / 抢单 CAS / 评 const code = fs.readFileSync(serverRepoPath, 'utf-8'); const start = code.indexOf('async claimOrder('); expect(start).toBeGreaterThan(-1); - const block = code.slice(start, start + 2400); + const block = code.slice(start, start + 3600); expect(block).toContain("String(cur.status) !== '待接单'"); expect(block).toContain("AND status = '待接单'"); }); diff --git a/server/tests/platform-dispatch.spec.ts b/server/tests/platform-dispatch.spec.ts index 8447df2..c0f6769 100644 --- a/server/tests/platform-dispatch.spec.ts +++ b/server/tests/platform-dispatch.spec.ts @@ -60,4 +60,11 @@ describe('第九十五批平台派单闭环', () => { expect(columns).toBe(24); expect(placeholders + 2).toBe(columns); }); + + it('禁止自己接自己发布的订单,派单池也不展示自己的单', () => { + expect(repo).toContain('不能接自己发布的订单'); + expect(repo).toContain('const actorUserId = Number(comp && comp.user_id) || expertId;'); + expect(repo).toContain('AND buyer_id <> ?'); + expect(indexCode).toContain('excludeBuyerId'); + }); });