fix(order): 抢单收紧为仅「待接单」可抢,抢单池同步过滤
- claimOrder 状态闸门改为严格只允许 待接单(含 CAS 条件 status='待接单'),杜绝抢已在履约/已服务/已核销的订单 - getOrdersPool 仅返回未认领的 待接单 订单 - 种子数据新增一笔待接单订单用于演示抢单 - 更新/新增回归护栏 claim-status-gate.spec.ts(78 套件/608 用例全绿)
这个提交包含在:
@@ -702,7 +702,8 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
('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, '朝阳区三里屯商圈', '待接单');
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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("'待接单');");
|
||||
});
|
||||
});
|
||||
@@ -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 认领标记列', () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户