merge: release daily Jushuitan fixed virtual inventory
This commit is contained in:
commit
77b3b3c800
@ -0,0 +1,76 @@
|
||||
export {}
|
||||
|
||||
const mockHandleRequest = jest.fn()
|
||||
const mockJsonError = jest.fn()
|
||||
const mockJsonSuccess = jest.fn()
|
||||
const mockProcessDue = jest.fn()
|
||||
|
||||
jest.mock('@/server/core/response', () => ({
|
||||
handleRequest: (...args: unknown[]) => mockHandleRequest(...args),
|
||||
jsonError: (...args: unknown[]) => mockJsonError(...args),
|
||||
jsonSuccess: (...args: unknown[]) => mockJsonSuccess(...args),
|
||||
}))
|
||||
|
||||
jest.mock('@/server/services/jushuitan-fixed-virtual-inventory.service', () => ({
|
||||
jushuitanFixedVirtualInventoryService: {
|
||||
processDue: (...args: unknown[]) => mockProcessDue(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
const { POST } = require('./route')
|
||||
|
||||
function createRequest(input: { secret?: string } = {}) {
|
||||
return {
|
||||
headers: {
|
||||
get: (name: string) =>
|
||||
name === 'x-jst-virtual-inventory-secret' ? input.secret ?? null : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('POST fixed Jushuitan virtual inventory push', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
process.env.JST_VIRTUAL_INVENTORY_CRON_SECRET = 'cron-secret'
|
||||
mockHandleRequest.mockImplementation(
|
||||
async (_request: unknown, handler: (ctx: unknown) => unknown) =>
|
||||
handler({ requestId: 'request-1', user: null }),
|
||||
)
|
||||
mockJsonError.mockImplementation((code, message, status, requestId) => ({
|
||||
code,
|
||||
message,
|
||||
status,
|
||||
requestId,
|
||||
}))
|
||||
mockJsonSuccess.mockImplementation((data, status, requestId) => ({
|
||||
data,
|
||||
status,
|
||||
requestId,
|
||||
}))
|
||||
mockProcessDue.mockResolvedValue({ processed: 1, pushed: 1, failed: 0 })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.JST_VIRTUAL_INVENTORY_CRON_SECRET
|
||||
})
|
||||
|
||||
it('rejects a request without the cron secret', async () => {
|
||||
const response = await POST(createRequest())
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(mockProcessDue).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs the daily batch for the authenticated 20:00 request', async () => {
|
||||
const response = await POST(createRequest({ secret: 'cron-secret' }))
|
||||
|
||||
expect(mockProcessDue).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ source: 'SYSTEM' }),
|
||||
)
|
||||
expect(response.data).toEqual({
|
||||
processed: 1,
|
||||
pushed: 1,
|
||||
failed: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,47 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import type { AuthUser } from '@/server/core/types'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service'
|
||||
|
||||
function getCronUser(request: NextRequest): AuthUser | null {
|
||||
const cronSecret = process.env.JST_VIRTUAL_INVENTORY_CRON_SECRET?.trim()
|
||||
if (!cronSecret) return null
|
||||
|
||||
const headerSecret =
|
||||
request.headers.get('x-jst-virtual-inventory-secret')?.trim() ?? ''
|
||||
if (headerSecret !== cronSecret) return null
|
||||
|
||||
return {
|
||||
id: 'jst-fixed-virtual-inventory-cron',
|
||||
email: 'jst-fixed-virtual-inventory-cron@local',
|
||||
name: '聚水潭固定虚拟库存定时任务',
|
||||
role: 'SUPER_ADMIN',
|
||||
status: 'ACTIVE',
|
||||
businessRoles: [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const user = ctx.user ?? getCronUser(request)
|
||||
if (!user) {
|
||||
return jsonError(
|
||||
ErrorCode.AUTH_INVALID_TOKEN,
|
||||
'请先登录或配置固定虚拟库存定时任务密钥',
|
||||
401,
|
||||
ctx.requestId,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await jushuitanFixedVirtualInventoryService.processDue({
|
||||
actor: user,
|
||||
source: ctx.user ? 'USER' : 'SYSTEM',
|
||||
actorId: ctx.user?.id,
|
||||
requestId: ctx.requestId,
|
||||
now: new Date(),
|
||||
})
|
||||
|
||||
return jsonSuccess(result, 200, ctx.requestId)
|
||||
}, { requireAuth: false })
|
||||
}
|
||||
@ -13,7 +13,6 @@ import {
|
||||
applyVirtualInventoryPushResultToRows,
|
||||
applyVirtualInventoryRunFailureToRows,
|
||||
} from '@/server/services/jushuitan-virtual-inventory-queue.service'
|
||||
import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service'
|
||||
|
||||
const DUE_PRODUCT_PAGE_SIZE = 20
|
||||
const MAX_DUE_PRODUCTS_PER_RUN = 100
|
||||
@ -425,16 +424,6 @@ export async function POST(request: NextRequest) {
|
||||
if (dueProductIds.length < requestedPageSize) break
|
||||
}
|
||||
|
||||
const fixedVirtualInventory = ctx.user
|
||||
? { processed: 0, pushed: 0, failed: 0 }
|
||||
: await jushuitanFixedVirtualInventoryService.processDue({
|
||||
actor: user,
|
||||
source: logSource,
|
||||
actorId,
|
||||
requestId: ctx.requestId,
|
||||
now,
|
||||
})
|
||||
|
||||
return jsonSuccess(
|
||||
{
|
||||
processedProductCount: processedProducts.length,
|
||||
@ -446,7 +435,6 @@ export async function POST(request: NextRequest) {
|
||||
0,
|
||||
),
|
||||
processedProducts,
|
||||
fixedVirtualInventory,
|
||||
},
|
||||
200,
|
||||
ctx.requestId,
|
||||
|
||||
@ -66,7 +66,7 @@ describe("Jushuitan fixed virtual inventory service", () => {
|
||||
it("builds the fixed warehouse and quantity payload from a barcode", () => {
|
||||
expect(buildFixedVirtualInventoryPayload("BARCODE-1")).toEqual({
|
||||
wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
items: [
|
||||
sku_and_qty: [
|
||||
{ sku_id: "BARCODE-1", qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY },
|
||||
],
|
||||
});
|
||||
@ -132,6 +132,36 @@ describe("Jushuitan fixed virtual inventory service", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("retries a code-zero response without an external request id", async () => {
|
||||
const repository = createRepository([createTask()]);
|
||||
const post = jest.fn(async () => ({
|
||||
path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
code: 0,
|
||||
message: "ok",
|
||||
raw: { code: 0, data: {} },
|
||||
}));
|
||||
jest.spyOn(operationLogService, "record").mockResolvedValue(undefined);
|
||||
const service = new JushuitanFixedVirtualInventoryService(repository, post);
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
|
||||
const result = await service.processDue({
|
||||
actor,
|
||||
source: "SYSTEM",
|
||||
requestId: "local-request-1",
|
||||
now,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ processed: 1, pushed: 0, failed: 1 });
|
||||
expect(repository.markSucceeded).not.toHaveBeenCalled();
|
||||
expect(repository.scheduleRetry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "task-1",
|
||||
attemptedAt: now,
|
||||
error: "Jushuitan fixed virtual inventory response is missing request_id",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("schedules every failed task exactly two hours later", async () => {
|
||||
const repository = createRepository([createTask()]);
|
||||
const post = jest.fn(async () => {
|
||||
@ -164,4 +194,5 @@ describe("Jushuitan fixed virtual inventory service", () => {
|
||||
expect.objectContaining({ status: OperationLogStatus.FAILED }),
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -20,6 +20,8 @@ export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH =
|
||||
export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID = 15994660;
|
||||
export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY = 100000;
|
||||
export const FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS = 2 * 60 * 60 * 1000;
|
||||
const FIXED_VIRTUAL_INVENTORY_BATCH_SIZE = 100;
|
||||
const MAX_FIXED_VIRTUAL_INVENTORY_TASKS_PER_RUN = 5000;
|
||||
|
||||
type FixedVirtualInventoryProduct = {
|
||||
id: string;
|
||||
@ -68,7 +70,7 @@ function normalizeError(error: unknown) {
|
||||
function buildFixedVirtualInventoryPayload(barcode: string) {
|
||||
return {
|
||||
wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
items: [
|
||||
sku_and_qty: [
|
||||
{
|
||||
sku_id: barcode,
|
||||
qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY,
|
||||
@ -105,16 +107,26 @@ class JushuitanFixedVirtualInventoryService {
|
||||
|
||||
async processDue(input: FixedVirtualInventoryRunInput) {
|
||||
const now = input.now ?? new Date();
|
||||
const tasks = await this.repository.findDue(now);
|
||||
const result = { processed: 0, pushed: 0, failed: 0 };
|
||||
|
||||
for (const task of tasks) {
|
||||
result.processed += 1;
|
||||
if (await this.processOne(task, input)) {
|
||||
result.pushed += 1;
|
||||
} else {
|
||||
result.failed += 1;
|
||||
while (result.processed < MAX_FIXED_VIRTUAL_INVENTORY_TASKS_PER_RUN) {
|
||||
const take = Math.min(
|
||||
FIXED_VIRTUAL_INVENTORY_BATCH_SIZE,
|
||||
MAX_FIXED_VIRTUAL_INVENTORY_TASKS_PER_RUN - result.processed,
|
||||
);
|
||||
const tasks = await this.repository.findDue(now, take);
|
||||
if (tasks.length === 0) break;
|
||||
|
||||
for (const task of tasks) {
|
||||
result.processed += 1;
|
||||
if (await this.processOne(task, input)) {
|
||||
result.pushed += 1;
|
||||
} else {
|
||||
result.failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.length < take) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -141,7 +153,12 @@ class JushuitanFixedVirtualInventoryService {
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
payload,
|
||||
);
|
||||
const requestId = readRequestId(response.raw) || input.requestId;
|
||||
const requestId = readRequestId(response.raw);
|
||||
if (!requestId) {
|
||||
throw new Error(
|
||||
"Jushuitan fixed virtual inventory response is missing request_id",
|
||||
);
|
||||
}
|
||||
await this.repository.markSucceeded({
|
||||
id: task.id,
|
||||
attemptedAt,
|
||||
@ -157,7 +174,7 @@ class JushuitanFixedVirtualInventoryService {
|
||||
productSpu: task.productSpu,
|
||||
skuId: task.skuItemId,
|
||||
skuCode: task.skuCode,
|
||||
message: "Jushuitan fixed virtual inventory push succeeded",
|
||||
message: "Jushuitan fixed virtual inventory request accepted",
|
||||
requestId,
|
||||
details: {
|
||||
flow: "fixed-wms-virtual-inventory",
|
||||
|
||||
@ -387,19 +387,27 @@ start_pm2_release() {
|
||||
install_virtual_inventory_push_cron() {
|
||||
local cron_file
|
||||
local cron_line
|
||||
local fixed_cron_line
|
||||
local lock_file="/tmp/furniture-3d-system-virtual-inventory-push.lock"
|
||||
local fixed_lock_file="/tmp/furniture-3d-system-fixed-virtual-inventory-push.lock"
|
||||
local endpoint="http://127.0.0.1:$APP_PORT/api/intake/non-integrated/jushuitan-virtual-inventory-push"
|
||||
local fixed_endpoint="http://127.0.0.1:$APP_PORT/api/intake/non-integrated/jushuitan-fixed-virtual-inventory-push"
|
||||
local log_file="$SHARED/logs/jushuitan-virtual-inventory-push.log"
|
||||
local fixed_log_file="$SHARED/logs/jushuitan-fixed-virtual-inventory-push.log"
|
||||
|
||||
mkdir -p "$SHARED/logs"
|
||||
cron_file="$(mktemp)"
|
||||
|
||||
(crontab -l 2>/dev/null || true) |
|
||||
sed '\|/api/intake/non-integrated/jushuitan-virtual-inventory-push|d' |
|
||||
sed '\|furniture-3d-system-virtual-inventory-push|d' > "$cron_file"
|
||||
sed '\|furniture-3d-system-virtual-inventory-push|d' |
|
||||
sed '\|/api/intake/non-integrated/jushuitan-fixed-virtual-inventory-push|d' |
|
||||
sed '\|furniture-3d-system-fixed-virtual-inventory-push|d' > "$cron_file"
|
||||
|
||||
cron_line="* * * * * /usr/bin/flock -n \"$lock_file\" /bin/bash -lc 'set -euo pipefail; set -a; source \"$SHARED/.env\"; set +a; : \"\${JST_VIRTUAL_INVENTORY_CRON_SECRET:?JST_VIRTUAL_INVENTORY_CRON_SECRET is required}\"; curl --fail --silent --show-error --max-time 55 --request POST \"$endpoint\" --header \"x-jst-virtual-inventory-secret: \${JST_VIRTUAL_INVENTORY_CRON_SECRET}\"' >> \"$log_file\" 2>&1 # furniture-3d-system-virtual-inventory-push"
|
||||
fixed_cron_line="0 20 * * * /usr/bin/flock -n \"$fixed_lock_file\" /bin/bash -lc 'set -euo pipefail; set -a; source \"$SHARED/.env\"; set +a; : \"\${JST_VIRTUAL_INVENTORY_CRON_SECRET:?JST_VIRTUAL_INVENTORY_CRON_SECRET is required}\"; curl --fail --silent --show-error --max-time 1800 --request POST \"$fixed_endpoint\" --header \"x-jst-virtual-inventory-secret: \${JST_VIRTUAL_INVENTORY_CRON_SECRET}\"' >> \"$fixed_log_file\" 2>&1 # furniture-3d-system-fixed-virtual-inventory-push"
|
||||
printf '%s\n' "$cron_line" >> "$cron_file"
|
||||
printf '%s\n' "$fixed_cron_line" >> "$cron_file"
|
||||
crontab "$cron_file"
|
||||
rm -f -- "$cron_file"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user