diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index ff2ff8b..5262c9d 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -702,7 +702,8 @@ async function seedInitialDataIfEmpty(conn: any): Promise { ('ord_02', 'PW202609280002', '钱老板', '林深见鹿', 'KTV陪唱', '客户全包', 288, 3, 864.00, '工体西路8号纯KTV 888包厢', '履约中'), ('ord_03', 'PW202609280003', '孙先生', '小鱼泡泡', '同城陪喝', '费用AA', 198, 2, 396.00, '望京合生麒麟社跳海酒馆', '待支付'), ('ord_04', 'PW202609280004', '李小姐', '顾北辰', '线上电竞', '无需承担', 88, 4, 352.00, '线上语音频道 #9021', '已完成'), - ('ord_05', 'PW202609280005', '周先生', '林深见鹿', 'KTV陪唱', '客户全包', 288, 2, 576.00, '海淀中关村好乐迪KTV', '争议仲裁中'); + ('ord_05', 'PW202609280005', '周先生', '林深见鹿', 'KTV陪唱', '客户全包', 288, 2, 576.00, '海淀中关村好乐迪KTV', '争议仲裁中'), + ('ord_06', 'PW202609280006', '吴老板', '待接单大神', '同城陪伴', '客户全包', 128, 2, 256.00, '朝阳区三里屯商圈', '待接单'); `); } diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index a696c8b..0bd354e 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -1875,7 +1875,7 @@ export class MysqlRepository { amount, location, expense_burden as payMethod, status, created_at as createdAt FROM orders - WHERE status NOT IN ('已完成', '已取消', '已关闭', '已退款', '已全额退款', '争议仲裁中', '已到场核销', '待确认') + WHERE status = '待接单' AND (claimed_by IS NULL OR claimed_by = 0) ORDER BY created_at DESC LIMIT 20 @@ -1929,7 +1929,6 @@ export class MysqlRepository { throw err; } - const blocked = ['待付款', '待支付', '已完成', '已取消', '已关闭', '已退款', '已全额退款', '争议仲裁中']; const [curRows]: any = await pool.query('SELECT id, status, claimed_by FROM orders WHERE id = ? OR order_no = ? LIMIT 1', [orderId, orderId]); const cur = curRows && curRows[0]; if (!cur) { @@ -1938,8 +1937,9 @@ export class MysqlRepository { err.code = 40400; throw err; } - if (blocked.includes(String(cur.status))) { - const err: any = new Error(`当前订单状态(${cur.status})不可接单`); + // 状态闸门:仅「待接单」可被抢单,严禁抢已在履约/已服务/已核销的订单 + if (String(cur.status) !== '待接单') { + const err: any = new Error(`当前订单状态(${cur.status})不可接单(仅待接单可抢)`); err.status = 409; err.code = 40900; throw err; @@ -1959,7 +1959,7 @@ export class MysqlRepository { SET status = '履约中', expert_id = ?, companion_name = ?, expert_avatar = ?, claimed_by = ?, claimed_at = NOW(), paid_at = COALESCE(paid_at, NOW()) WHERE (id = ? OR order_no = ?) - AND status NOT IN ('待付款', '待支付', '已完成', '已取消', '已关闭', '已退款', '已全额退款', '争议仲裁中') + AND status = '待接单' AND (claimed_by IS NULL OR claimed_by = ?) `, [expertId, expertData.companionName || '我 (接单大神)', expertData.expertAvatar || '', expertId, orderId, orderId, expertId]); diff --git a/server/tests/claim-status-gate.spec.ts b/server/tests/claim-status-gate.spec.ts new file mode 100644 index 0000000..332a002 --- /dev/null +++ b/server/tests/claim-status-gate.spec.ts @@ -0,0 +1,35 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * 第六十二批回归闸门:抢单状态收紧为「仅待接单可抢」 + */ +describe('第六十二批抢单状态闸门', () => { + const rootDir = path.resolve(__dirname, '../../'); + const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8'); + const mysqlCode = fs.readFileSync(path.join(rootDir, 'server/src/db/mysql.ts'), 'utf-8'); + + const claimBlock = () => { + const start = repoCode.indexOf('async claimOrder('); + expect(start).toBeGreaterThan(-1); + return repoCode.slice(start, start + 2600); + }; + + it('仅待接单可抢单(含 CAS 条件)', () => { + const b = claimBlock(); + expect(b).toContain("String(cur.status) !== '待接单'"); + expect(b).toContain("仅待接单可抢"); + expect(b).toContain("AND status = '待接单'"); + }); + + it('抢单池仅含待接单且未认领', () => { + const start = repoCode.indexOf('async getOrdersPool('); + const block = repoCode.slice(start, start + 900); + expect(block).toContain("WHERE status = '待接单'"); + }); + + it('种子数据提供一笔待接单用于演示抢单', () => { + expect(mysqlCode).toContain("'待接单');"); + }); +}); diff --git a/server/tests/order-activity-integrity.spec.ts b/server/tests/order-activity-integrity.spec.ts index 7b80fe1..1cc18ce 100644 --- a/server/tests/order-activity-integrity.spec.ts +++ b/server/tests/order-activity-integrity.spec.ts @@ -46,9 +46,13 @@ describe('订单与活动资金一致性护栏 (报名超卖 / 抢单 CAS / 评 expect(code).toContain('该订单已被其他大神抢先接单'); }); - it('claimOrder 必须拦截不可接单状态', () => { + it('claimOrder 必须仅允许待接单抢单', () => { const code = fs.readFileSync(serverRepoPath, 'utf-8'); - expect(code).toContain("const blocked = ['待付款', '待支付', '已完成', '已取消', '已关闭', '已退款', '已全额退款', '争议仲裁中']"); + const start = code.indexOf('async claimOrder('); + expect(start).toBeGreaterThan(-1); + const block = code.slice(start, start + 2400); + expect(block).toContain("String(cur.status) !== '待接单'"); + expect(block).toContain("AND status = '待接单'"); }); it('数据库层必须为 orders 增加 claimed_by / claimed_at 认领标记列', () => { diff --git a/server/tests/orders-pool-filter.spec.ts b/server/tests/orders-pool-filter.spec.ts index 05dcff2..7402a39 100644 --- a/server/tests/orders-pool-filter.spec.ts +++ b/server/tests/orders-pool-filter.spec.ts @@ -13,7 +13,7 @@ describe('第二十九批抢单池过滤闸门', () => { const start = repoCode.indexOf('async getOrdersPool('); expect(start).toBeGreaterThan(-1); const block = repoCode.slice(start, start + 1400); - expect(block).toContain("WHERE status NOT IN ('已完成', '已取消'"); + expect(block).toContain("WHERE status = '待接单'"); expect(block).toContain('claimed_by IS NULL'); expect(block).toContain('LIMIT 20'); });