feat(M08-B): 接入保洁微信转账适配器

This commit is contained in:
Codex
2026-06-27 16:07:41 +08:00
parent 05907a6bea
commit b07c451700
22 changed files with 663 additions and 27 deletions
@@ -7,6 +7,7 @@ export const cleaningTaskStatuses = [
'REJECTED', 'EXEMPT', 'SETTLED', 'CANCELLED'
] as const;
export type CleaningTaskStatus = typeof cleaningTaskStatuses[number];
export type CleaningSettlementStatus = 'DRAFT' | 'CONFIRMED' | 'PAID' | 'CANCELLED';
export class CleaningTaskError extends Error {
constructor(public readonly code: string) { super(code); }
@@ -79,7 +80,7 @@ interface SettlementRow extends RowDataPacket {
cleanerName: string;
storeId: string | null;
storeName: string | null;
status: 'DRAFT' | 'CONFIRMED' | 'PAID' | 'CANCELLED';
status: CleaningSettlementStatus;
taskCount: number;
totalRewardCents: number;
periodStart: Date | null;
@@ -89,6 +90,8 @@ interface SettlementRow extends RowDataPacket {
paidAt: Date | null;
payoutChannel: string;
payoutReference: string;
payoutState: string;
payoutPackageInfo: string;
payoutError: string;
note: string;
createdAt: Date;
@@ -321,7 +324,7 @@ export class CleaningTaskRepository {
}
async listSettlements(input: CleaningActor & {
page: number; pageSize: number; status?: 'DRAFT' | 'CONFIRMED' | 'PAID' | 'CANCELLED';
page: number; pageSize: number; status?: CleaningSettlementStatus;
}) {
this.assertSettlement(input.access, 'read');
const where = [
@@ -348,6 +351,7 @@ export class CleaningTaskRepository {
s.period_start AS periodStart, s.period_end AS periodEnd,
s.paid_by AS paidBy, s.confirmed_at AS confirmedAt, s.paid_at AS paidAt,
s.payout_channel AS payoutChannel, s.payout_reference AS payoutReference,
s.payout_state AS payoutState, s.payout_package_info AS payoutPackageInfo,
s.payout_error AS payoutError,
s.note, s.created_at AS createdAt
FROM qipai_cleaning_settlements s
@@ -478,7 +482,8 @@ export class CleaningTaskRepository {
const [result] = await connection.execute<ResultSetHeader>(
`UPDATE qipai_cleaning_settlements s
SET s.status = 'PAID', s.paid_by = ?, s.paid_at = UTC_TIMESTAMP(3),
s.payout_channel = ?, s.payout_reference = ?, s.payout_error = '',
s.payout_channel = ?, s.payout_reference = ?, s.payout_state = 'SUCCESS',
s.payout_package_info = '', s.payout_error = '',
s.note = CASE WHEN ? = '' THEN s.note ELSE ? END
WHERE s.tenant_id = ? AND s.id = ? AND s.status = 'CONFIRMED' AND s.deleted_at IS NULL
AND ${storeScopeSql(input.access, 'COALESCE(s.store_id, 0)')}`,
@@ -501,7 +506,7 @@ export class CleaningTaskRepository {
`UPDATE qipai_cleaning_settlements s
SET s.payout_channel = CASE WHEN ? = '' THEN s.payout_channel ELSE ? END,
s.payout_reference = CASE WHEN ? = '' THEN s.payout_reference ELSE ? END,
s.payout_error = ?,
s.payout_state = 'FAIL', s.payout_package_info = '', s.payout_error = ?,
s.note = CASE WHEN ? = '' THEN s.note ELSE ? END
WHERE s.tenant_id = ? AND s.id = ? AND s.status = 'CONFIRMED' AND s.deleted_at IS NULL
AND ${storeScopeSql(input.access, 'COALESCE(s.store_id, 0)')}`,
@@ -517,6 +522,30 @@ export class CleaningTaskRepository {
});
}
async recordSettlementPayoutPending(input: CleaningActor & {
settlementId: string; payoutChannel: string; payoutReference: string;
payoutState: string; payoutPackageInfo?: string; note?: string;
}) {
this.assertSettlement(input.access, 'write');
return this.transaction(async (connection) => {
const [result] = await connection.execute<ResultSetHeader>(
`UPDATE qipai_cleaning_settlements s
SET s.payout_channel = ?, s.payout_reference = ?, s.payout_state = ?,
s.payout_package_info = ?, s.payout_error = '',
s.note = CASE WHEN ? = '' THEN s.note ELSE ? END
WHERE s.tenant_id = ? AND s.id = ? AND s.status = 'CONFIRMED' AND s.deleted_at IS NULL
AND ${storeScopeSql(input.access, 'COALESCE(s.store_id, 0)')}`,
[
input.payoutChannel, input.payoutReference, input.payoutState.slice(0, 32),
(input.payoutPackageInfo ?? '').slice(0, 1024),
input.note ?? '', input.note ?? '', input.tenantId, input.settlementId
]
);
if (result.affectedRows !== 1) throw new CleaningTaskError('CLEANING_SETTLEMENT_STATUS_CONFLICT');
return this.getSettlement(connection, input.tenantId, input.settlementId);
});
}
async reclaimTimeouts(input: CleaningActor & { olderThanMinutes: number; limit: number }) {
this.assertCleaner(input.access, 'write');
const [rows] = await this.pool.execute<RowDataPacket[]>(
@@ -921,6 +950,7 @@ export class CleaningTaskRepository {
s.period_start AS periodStart, s.period_end AS periodEnd,
s.paid_by AS paidBy, s.confirmed_at AS confirmedAt, s.paid_at AS paidAt,
s.payout_channel AS payoutChannel, s.payout_reference AS payoutReference,
s.payout_state AS payoutState, s.payout_package_info AS payoutPackageInfo,
s.payout_error AS payoutError,
s.note, s.created_at AS createdAt
FROM qipai_cleaning_settlements s
@@ -1017,6 +1047,8 @@ function publicSettlement(row: SettlementRow) {
paidAt: row.paidAt,
payoutChannel: row.payoutChannel,
payoutReference: row.payoutReference,
payoutState: row.payoutState,
payoutPackageInfo: row.payoutPackageInfo,
payoutError: row.payoutError,
note: row.note,
createdAt: row.createdAt