彻底重构-1

This commit is contained in:
yml
2026-04-09 09:06:08 +08:00
parent b509394b09
commit df2eb34478
92 changed files with 2226 additions and 1261 deletions
+33 -11
View File
@@ -59,8 +59,7 @@ function loadEnvFile(filePath) {
continue
}
const value = parseEnvValue(line.slice(separatorIndex + 1))
process.env[key] = value
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1))
}
}
@@ -127,14 +126,24 @@ function applyEnvOverrides(baseConfig) {
nextConfig.ocr.projectRoot = ocrProjectRoot
}
const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim()
if (proofMode) {
nextConfig.redeem.proofMode = proofMode
const dataRoot = String(process.env.DATA_ROOT || '').trim()
if (dataRoot) {
nextConfig.data.root = path.resolve(dataRoot)
}
const databaseFilePath = String(process.env.DATABASE_FILE_PATH || '').trim()
if (databaseFilePath) {
nextConfig.database.filePath = databaseFilePath
const databaseUrl = String(process.env.DATABASE_URL || '').trim()
if (databaseUrl) {
nextConfig.database.url = databaseUrl
}
const databaseSsl = parseBoolean(process.env.DATABASE_SSL)
if (databaseSsl !== null) {
nextConfig.database.ssl = databaseSsl
}
const databaseMaxConnections = parseInteger(process.env.DATABASE_MAX_CONNECTIONS)
if (databaseMaxConnections !== null) {
nextConfig.database.maxConnections = databaseMaxConnections
}
const claimBaseUrl = String(process.env.CLAIM_BASE_URL || '').trim()
@@ -142,11 +151,21 @@ function applyEnvOverrides(baseConfig) {
nextConfig.orders.claimBaseUrl = claimBaseUrl
}
const tokenTtlHours = parseInteger(process.env.CLAIM_TOKEN_TTL_HOURS)
if (tokenTtlHours !== null) {
nextConfig.orders.tokenTtlHours = tokenTtlHours
}
const skuMappings = parseJsonObject(process.env.ORDER_SKU_MAPPINGS_JSON)
if (skuMappings) {
nextConfig.orders.skuMappings = skuMappings
}
const fulfillmentBindings = parseJsonArray(process.env.ORDER_FULFILLMENT_BINDINGS_JSON)
if (fulfillmentBindings) {
nextConfig.orders.fulfillmentBindings = fulfillmentBindings
}
const adminSessionSecret = String(process.env.ADMIN_SESSION_SECRET || '').trim()
if (adminSessionSecret) {
nextConfig.admin.sessionSecret = adminSessionSecret
@@ -222,6 +241,11 @@ function applyEnvOverrides(baseConfig) {
nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops)
}
const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim()
if (proofMode) {
nextConfig.redeem.proofMode = proofMode
}
return nextConfig
}
@@ -269,9 +293,7 @@ function isPlainObject(value) {
}
function parseBoolean(rawValue) {
const normalized = String(rawValue || '')
.trim()
.toLowerCase()
const normalized = String(rawValue || '').trim().toLowerCase()
if (!normalized) {
return null
+28 -23
View File
@@ -1,43 +1,48 @@
import fs from 'node:fs'
import path from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { Pool } from 'pg'
import { runtimeConfig } from '../config/runtime.js'
let databaseInstance = null
let poolInstance = null
export function getDb() {
if (!databaseInstance) {
const filePath = resolveDatabasePath()
fs.mkdirSync(path.dirname(filePath), { recursive: true })
databaseInstance = new DatabaseSync(filePath)
databaseInstance.exec('PRAGMA foreign_keys = ON;')
databaseInstance.exec('PRAGMA journal_mode = WAL;')
if (!poolInstance) {
poolInstance = new Pool({
connectionString: String(runtimeConfig.database?.url || '').trim(),
ssl: runtimeConfig.database?.ssl ? { rejectUnauthorized: false } : false,
max: Number(runtimeConfig.database?.maxConnections || 10),
})
}
return databaseInstance
return poolInstance
}
export function runInTransaction(fn) {
const db = getDb()
db.exec('BEGIN')
export async function query(text, params = []) {
const pool = getDb()
return pool.query(text, params)
}
export async function withTransaction(fn) {
const client = await getDb().connect()
try {
const result = fn(db)
db.exec('COMMIT')
await client.query('BEGIN')
const result = await fn(client)
await client.query('COMMIT')
return result
} catch (error) {
db.exec('ROLLBACK')
await client.query('ROLLBACK')
throw error
} finally {
client.release()
}
}
function resolveDatabasePath() {
const configured = String(runtimeConfig.database.filePath || '').trim()
if (!configured) {
throw new Error('缺少数据库文件路径配置')
export async function closeDb() {
if (!poolInstance) {
return
}
return path.resolve(configured)
const pool = poolInstance
poolInstance = null
await pool.end()
}
+24 -16
View File
@@ -1,33 +1,28 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { getDb } from './client.js'
import { closeDb, query, withTransaction } from './client.js'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const MIGRATIONS_DIR = path.join(CURRENT_DIR, 'migrations')
export function runDatabaseMigrations() {
const db = getDb()
export async function runDatabaseMigrations() {
const files = fs.readdirSync(MIGRATIONS_DIR)
.filter((name) => name.endsWith('.sql'))
.sort()
db.exec(`
await query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id BIGSERIAL PRIMARY KEY,
filename TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL
);
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)
const appliedRows = db.prepare('SELECT filename FROM schema_migrations').all()
const applied = new Set(appliedRows.map((row) => row.filename))
const insertApplied = db.prepare(`
INSERT INTO schema_migrations (filename, applied_at)
VALUES (?, ?)
`)
const appliedRows = await query('SELECT filename FROM schema_migrations')
const applied = new Set(appliedRows.rows.map((row) => String(row.filename)))
for (const filename of files) {
if (applied.has(filename)) {
@@ -35,8 +30,13 @@ export function runDatabaseMigrations() {
}
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, filename), 'utf8')
db.exec(sql)
insertApplied.run(filename, new Date().toISOString())
await withTransaction(async (client) => {
await client.query(sql)
await client.query(
'INSERT INTO schema_migrations (filename, applied_at) VALUES ($1, NOW())',
[filename],
)
})
}
}
@@ -44,4 +44,12 @@ const currentFilePath = fileURLToPath(import.meta.url)
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
runDatabaseMigrations()
.then(async () => {
await closeDb()
})
.catch(async (error) => {
console.error(error)
await closeDb()
process.exit(1)
})
}
@@ -0,0 +1,272 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS admin_users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS admin_audit_logs (
id BIGSERIAL PRIMARY KEY,
actor_user_id BIGINT,
actor_username TEXT NOT NULL DEFAULT '',
actor_role TEXT NOT NULL DEFAULT '',
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at ON admin_audit_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_target ON admin_audit_logs(target_type, target_id);
CREATE TABLE IF NOT EXISTS fulfillment_profiles (
id BIGSERIAL PRIMARY KEY,
profile_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
executor_key TEXT NOT NULL,
requires_claim BOOLEAN NOT NULL DEFAULT FALSE,
auto_dispatch BOOLEAN NOT NULL DEFAULT FALSE,
inventory_strategy TEXT NOT NULL DEFAULT 'static_pool',
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS fulfillment_profile_requirements (
id BIGSERIAL PRIMARY KEY,
profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE CASCADE,
role_key TEXT NOT NULL,
credential_type TEXT NOT NULL,
quantity_per_unit INTEGER NOT NULL DEFAULT 1,
is_required BOOLEAN NOT NULL DEFAULT TRUE,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(profile_id, role_key)
);
CREATE TABLE IF NOT EXISTS sku_fulfillment_bindings (
id BIGSERIAL PRIMARY KEY,
sku_code TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
shop_id TEXT NOT NULL DEFAULT '',
profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
priority INTEGER NOT NULL DEFAULT 100,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sku_fulfillment_bindings_lookup
ON sku_fulfillment_bindings(sku_code, provider, platform, shop_id, enabled, priority);
CREATE TABLE IF NOT EXISTS orders (
id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL,
platform TEXT NOT NULL,
shop_id TEXT NOT NULL DEFAULT '',
shop_name TEXT NOT NULL DEFAULT '',
platform_order_id TEXT NOT NULL,
order_status TEXT NOT NULL DEFAULT 'created',
pay_status TEXT NOT NULL DEFAULT 'unpaid',
buyer_id TEXT NOT NULL DEFAULT '',
buyer_name TEXT NOT NULL DEFAULT '',
receiver_contact TEXT NOT NULL DEFAULT '',
total_amount INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CNY',
raw_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(provider, platform, shop_id, platform_order_id)
);
CREATE TABLE IF NOT EXISTS order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
sku_code TEXT NOT NULL,
sku_name TEXT NOT NULL DEFAULT '',
quantity INTEGER NOT NULL DEFAULT 1,
spec_json JSONB NOT NULL DEFAULT '{}'::jsonb,
item_snapshot_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_sku_code ON order_items(sku_code);
CREATE TABLE IF NOT EXISTS inventory_items (
id BIGSERIAL PRIMARY KEY,
batch_no TEXT NOT NULL DEFAULT '',
sku_code TEXT NOT NULL,
credential_type TEXT NOT NULL,
display_value TEXT NOT NULL DEFAULT '',
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
source_type TEXT NOT NULL DEFAULT 'static_import',
status TEXT NOT NULL DEFAULT 'available',
invalid_reason TEXT NOT NULL DEFAULT '',
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_items_unique_payload
ON inventory_items(sku_code, credential_type, md5(payload_json::text));
CREATE INDEX IF NOT EXISTS idx_inventory_items_pool
ON inventory_items(sku_code, credential_type, status, id);
CREATE TABLE IF NOT EXISTS fulfillment_tasks (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
order_item_id BIGINT NOT NULL REFERENCES order_items(id) ON DELETE CASCADE,
unit_index INTEGER NOT NULL DEFAULT 1,
task_no TEXT NOT NULL UNIQUE,
provider TEXT NOT NULL,
platform TEXT NOT NULL,
shop_id TEXT NOT NULL DEFAULT '',
shop_name TEXT NOT NULL DEFAULT '',
platform_order_id TEXT NOT NULL,
profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE RESTRICT,
executor_key TEXT NOT NULL,
task_status TEXT NOT NULL DEFAULT 'pending_payment',
inventory_status TEXT NOT NULL DEFAULT 'pending',
delivery_status TEXT NOT NULL DEFAULT 'pending',
result_code TEXT NOT NULL DEFAULT '',
result_message TEXT NOT NULL DEFAULT '',
claim_token TEXT NOT NULL DEFAULT '',
claim_expires_at TIMESTAMPTZ,
automation_mode TEXT NOT NULL DEFAULT 'manual',
requires_claim BOOLEAN NOT NULL DEFAULT FALSE,
user_action_status TEXT NOT NULL DEFAULT 'not_required',
attempt_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT NOT NULL DEFAULT '',
context_json JSONB NOT NULL DEFAULT '{}'::jsonb,
claimed_at TIMESTAMPTZ,
role_confirmed_at TIMESTAMPTZ,
redeemed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(order_item_id, unit_index)
);
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_status ON fulfillment_tasks(task_status, delivery_status);
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_order ON fulfillment_tasks(order_id, order_item_id);
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_claim_token ON fulfillment_tasks(claim_token);
CREATE TABLE IF NOT EXISTS claim_tokens (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'active',
expired_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
max_use_count INTEGER NOT NULL DEFAULT 1,
used_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_claim_tokens_status ON claim_tokens(status, expired_at);
CREATE TABLE IF NOT EXISTS task_inventory_bindings (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES fulfillment_tasks(id) ON DELETE CASCADE,
inventory_item_id BIGINT NOT NULL REFERENCES inventory_items(id) ON DELETE RESTRICT,
role_key TEXT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
binding_status TEXT NOT NULL DEFAULT 'reserved',
consumed_at TIMESTAMPTZ,
released_at TIMESTAMPTZ,
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(task_id, role_key, inventory_item_id)
);
CREATE INDEX IF NOT EXISTS idx_task_inventory_bindings_task ON task_inventory_bindings(task_id);
CREATE INDEX IF NOT EXISTS idx_task_inventory_bindings_inventory ON task_inventory_bindings(inventory_item_id);
CREATE TABLE IF NOT EXISTS task_events (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES fulfillment_tasks(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_task_events_task_created_at ON task_events(task_id, created_at DESC);
CREATE TABLE IF NOT EXISTS tencent_browser_contexts (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE,
browser_session_id TEXT NOT NULL DEFAULT '',
login_type TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
role_id TEXT NOT NULL DEFAULT '',
role_name TEXT NOT NULL DEFAULT '',
area TEXT NOT NULL DEFAULT '',
partition_name TEXT NOT NULL DEFAULT '',
screenshot_path TEXT NOT NULL DEFAULT '',
artifacts_json JSONB NOT NULL DEFAULT '{}'::jsonb,
state_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS message_deliveries (
id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
shop_id TEXT NOT NULL DEFAULT '',
shop_name TEXT NOT NULL DEFAULT '',
channel TEXT NOT NULL,
order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
task_id BIGINT REFERENCES fulfillment_tasks(id) ON DELETE SET NULL,
platform_order_id TEXT NOT NULL DEFAULT '',
recipient_key TEXT NOT NULL DEFAULT '',
message_content TEXT NOT NULL DEFAULT '',
claim_url TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
request_url TEXT NOT NULL DEFAULT '',
request_headers_json JSONB NOT NULL DEFAULT '{}'::jsonb,
request_body_json JSONB NOT NULL DEFAULT '{}'::jsonb,
response_status INTEGER NOT NULL DEFAULT 0,
response_json JSONB NOT NULL DEFAULT '{}'::jsonb,
error_message TEXT NOT NULL DEFAULT '',
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_task_channel ON message_deliveries(task_id, channel, created_at DESC);
CREATE TABLE IF NOT EXISTS webhook_events (
id BIGSERIAL PRIMARY KEY,
provider TEXT NOT NULL DEFAULT '',
platform TEXT NOT NULL DEFAULT '',
shop_id TEXT NOT NULL DEFAULT '',
shop_name TEXT NOT NULL DEFAULT '',
event_type TEXT NOT NULL,
event_key TEXT NOT NULL,
signature_valid BOOLEAN NOT NULL DEFAULT FALSE,
headers_json JSONB NOT NULL DEFAULT '{}'::jsonb,
query_json JSONB NOT NULL DEFAULT '{}'::jsonb,
body_json JSONB NOT NULL DEFAULT '{}'::jsonb,
processed BOOLEAN NOT NULL DEFAULT FALSE,
process_error TEXT NOT NULL DEFAULT '',
related_order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_webhook_events_lookup ON webhook_events(provider, platform, shop_id, event_key);
CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed, created_at DESC);
-129
View File
@@ -1,129 +0,0 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
platform_order_id TEXT NOT NULL,
order_status TEXT NOT NULL DEFAULT 'created',
pay_status TEXT NOT NULL DEFAULT 'unpaid',
buyer_id TEXT NOT NULL DEFAULT '',
buyer_name TEXT NOT NULL DEFAULT '',
receiver_contact TEXT NOT NULL DEFAULT '',
total_amount INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CNY',
raw_payload_json TEXT NOT NULL DEFAULT '{}',
paid_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(platform, platform_order_id)
);
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
sku_code TEXT NOT NULL,
sku_name TEXT NOT NULL DEFAULT '',
quantity INTEGER NOT NULL DEFAULT 1,
spec_json TEXT NOT NULL DEFAULT '{}',
delivery_mode TEXT NOT NULL DEFAULT 'claim_link',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_sku_code ON order_items(sku_code);
CREATE TABLE IF NOT EXISTS cdk_inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL DEFAULT '',
sku_code TEXT NOT NULL,
cdk_code TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'available',
reserved_by_task_id INTEGER,
invalid_reason TEXT NOT NULL DEFAULT '',
delivered_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(sku_code, cdk_code)
);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_sku_code ON cdk_inventory(sku_code);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_status ON cdk_inventory(status);
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_reserved_task ON cdk_inventory(reserved_by_task_id);
CREATE TABLE IF NOT EXISTS delivery_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
order_item_id INTEGER NOT NULL,
platform_order_id TEXT NOT NULL,
task_no TEXT NOT NULL,
task_status TEXT NOT NULL DEFAULT 'pending_payment',
login_type TEXT NOT NULL DEFAULT '',
claim_token_id INTEGER,
reserved_cdk_id INTEGER,
browser_session_id TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
role_id TEXT NOT NULL DEFAULT '',
role_name TEXT NOT NULL DEFAULT '',
area TEXT NOT NULL DEFAULT '',
partition_name TEXT NOT NULL DEFAULT '',
result_code TEXT NOT NULL DEFAULT '',
result_message TEXT NOT NULL DEFAULT '',
screenshot_path TEXT NOT NULL DEFAULT '',
artifacts_json TEXT NOT NULL DEFAULT '{}',
last_error TEXT NOT NULL DEFAULT '',
retry_count INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
claimed_at TEXT,
role_confirmed_at TEXT,
redeemed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY(order_item_id) REFERENCES order_items(id) ON DELETE CASCADE,
FOREIGN KEY(reserved_cdk_id) REFERENCES cdk_inventory(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_tasks_task_no ON delivery_tasks(task_no);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_id ON delivery_tasks(order_id);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_item_id ON delivery_tasks(order_item_id);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_status ON delivery_tasks(task_status);
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_platform_order_id ON delivery_tasks(platform_order_id);
CREATE TABLE IF NOT EXISTS claim_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
token TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
expired_at TEXT NOT NULL,
used_at TEXT,
max_use_count INTEGER NOT NULL DEFAULT 1,
used_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(task_id) REFERENCES delivery_tasks(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_claim_tokens_token ON claim_tokens(token);
CREATE INDEX IF NOT EXISTS idx_claim_tokens_task_id ON claim_tokens(task_id);
CREATE INDEX IF NOT EXISTS idx_claim_tokens_status ON claim_tokens(status);
CREATE TABLE IF NOT EXISTS webhook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
event_type TEXT NOT NULL,
event_key TEXT NOT NULL,
signature_valid INTEGER NOT NULL DEFAULT 0,
headers_json TEXT NOT NULL DEFAULT '{}',
query_json TEXT NOT NULL DEFAULT '{}',
body_json TEXT NOT NULL DEFAULT '{}',
processed INTEGER NOT NULL DEFAULT 0,
process_error TEXT NOT NULL DEFAULT '',
related_order_id INTEGER,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_webhook_events_platform_key ON webhook_events(platform, event_key);
CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed);
CREATE INDEX IF NOT EXISTS idx_webhook_events_created_at ON webhook_events(created_at);
@@ -1,12 +0,0 @@
CREATE TABLE IF NOT EXISTS admin_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'operator',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_admin_users_role ON admin_users(role);
CREATE INDEX IF NOT EXISTS idx_admin_users_status ON admin_users(status);
@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS admin_audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_user_id INTEGER NOT NULL,
actor_username TEXT NOT NULL,
actor_role TEXT NOT NULL,
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL DEFAULT '',
payload_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
FOREIGN KEY (actor_user_id) REFERENCES admin_users(id)
);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_actor_user_id ON admin_audit_logs(actor_user_id);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_action ON admin_audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_target_type ON admin_audit_logs(target_type);
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at ON admin_audit_logs(created_at);
@@ -1,29 +0,0 @@
CREATE TABLE IF NOT EXISTS message_deliveries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
channel TEXT NOT NULL,
order_id INTEGER,
task_id INTEGER,
platform_order_id TEXT NOT NULL DEFAULT '',
recipient_key TEXT NOT NULL DEFAULT '',
message_content TEXT NOT NULL DEFAULT '',
claim_url TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
request_url TEXT NOT NULL DEFAULT '',
request_headers_json TEXT NOT NULL DEFAULT '{}',
request_body_json TEXT NOT NULL DEFAULT '{}',
response_status INTEGER NOT NULL DEFAULT 0,
response_json TEXT NOT NULL DEFAULT '{}',
error_message TEXT NOT NULL DEFAULT '',
sent_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL,
FOREIGN KEY (task_id) REFERENCES delivery_tasks(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_order_id ON message_deliveries(order_id);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_task_id ON message_deliveries(task_id);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_platform_order_id ON message_deliveries(platform_order_id);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_status ON message_deliveries(status);
CREATE INDEX IF NOT EXISTS idx_message_deliveries_created_at ON message_deliveries(created_at);
@@ -1,94 +0,0 @@
PRAGMA foreign_keys = OFF;
CREATE TABLE IF NOT EXISTS orders__new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL DEFAULT 'agiso',
platform TEXT NOT NULL,
shop_id TEXT NOT NULL DEFAULT '',
shop_name TEXT NOT NULL DEFAULT '',
platform_order_id TEXT NOT NULL,
order_status TEXT NOT NULL DEFAULT 'created',
pay_status TEXT NOT NULL DEFAULT 'unpaid',
buyer_id TEXT NOT NULL DEFAULT '',
buyer_name TEXT NOT NULL DEFAULT '',
receiver_contact TEXT NOT NULL DEFAULT '',
total_amount INTEGER NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'CNY',
raw_payload_json TEXT NOT NULL DEFAULT '{}',
paid_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
INSERT INTO orders__new (
id,
provider,
platform,
shop_id,
shop_name,
platform_order_id,
order_status,
pay_status,
buyer_id,
buyer_name,
receiver_contact,
total_amount,
currency,
raw_payload_json,
paid_at,
created_at,
updated_at
)
SELECT
id,
'agiso',
platform,
'',
'',
platform_order_id,
order_status,
pay_status,
buyer_id,
buyer_name,
receiver_contact,
total_amount,
currency,
raw_payload_json,
paid_at,
created_at,
updated_at
FROM orders;
DROP TABLE orders;
ALTER TABLE orders__new RENAME TO orders;
CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_provider_platform_shop_order
ON orders(provider, platform, shop_id, platform_order_id);
ALTER TABLE webhook_events ADD COLUMN provider TEXT NOT NULL DEFAULT 'agiso';
ALTER TABLE webhook_events ADD COLUMN shop_id TEXT NOT NULL DEFAULT '';
ALTER TABLE webhook_events ADD COLUMN shop_name TEXT NOT NULL DEFAULT '';
UPDATE webhook_events
SET provider = CASE
WHEN trim(provider) = '' THEN 'agiso'
ELSE provider
END;
CREATE INDEX IF NOT EXISTS idx_webhook_events_provider_platform_shop
ON webhook_events(provider, platform, shop_id);
ALTER TABLE message_deliveries ADD COLUMN provider TEXT NOT NULL DEFAULT 'agiso';
ALTER TABLE message_deliveries ADD COLUMN shop_id TEXT NOT NULL DEFAULT '';
ALTER TABLE message_deliveries ADD COLUMN shop_name TEXT NOT NULL DEFAULT '';
UPDATE message_deliveries
SET provider = CASE
WHEN trim(provider) = '' THEN 'agiso'
ELSE provider
END;
CREATE INDEX IF NOT EXISTS idx_message_deliveries_provider_platform_shop
ON message_deliveries(provider, platform, shop_id);
PRAGMA foreign_keys = ON;
@@ -1,3 +0,0 @@
UPDATE orders
SET total_amount = total_amount * 100
WHERE total_amount != 0;
@@ -1,11 +0,0 @@
UPDATE orders
SET platform = 'xianyu'
WHERE lower(trim(platform)) = 'aldsidle';
UPDATE webhook_events
SET platform = 'xianyu'
WHERE lower(trim(platform)) = 'aldsidle';
UPDATE message_deliveries
SET platform = 'xianyu'
WHERE lower(trim(platform)) = 'aldsidle';
@@ -1,86 +1,94 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function createAdminAuditLog(input) {
const result = getDb().prepare(`
INSERT INTO admin_audit_logs (
actor_user_id,
actor_username,
actor_role,
action,
target_type,
target_id,
payload_json,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.actorUserId,
input.actorUsername,
input.actorRole,
input.action,
input.targetType,
input.targetId,
input.payloadJson,
input.createdAt,
export async function createAdminAuditLog(input) {
const result = await query(
`
INSERT INTO admin_audit_logs (
actor_user_id,
actor_username,
actor_role,
action,
target_type,
target_id,
payload_json,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
RETURNING *
`,
[
input.actorUserId || null,
input.actorUsername || '',
input.actorRole || '',
input.action,
input.targetType,
input.targetId,
input.payloadJson || '{}',
input.createdAt,
],
)
return getAdminAuditLogById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function getAdminAuditLogById(logId) {
return getDb().prepare('SELECT * FROM admin_audit_logs WHERE id = ? LIMIT 1').get(logId) || null
export async function getAdminAuditLogById(logId) {
const result = await query('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)])
return result.rows[0] || null
}
export function listAdminAuditLogs(query = {}) {
export async function listAdminAuditLogs(queryInput = {}) {
const conditions = []
const values = []
const params = []
if (query.actorUsername) {
conditions.push('actor_username = ?')
values.push(query.actorUsername)
if (queryInput.actorUsername) {
params.push(queryInput.actorUsername)
conditions.push(`actor_username = $${params.length}`)
}
if (query.action) {
conditions.push('action = ?')
values.push(query.action)
if (queryInput.action) {
params.push(queryInput.action)
conditions.push(`action = $${params.length}`)
}
if (query.targetType) {
conditions.push('target_type = ?')
values.push(query.targetType)
if (queryInput.targetType) {
params.push(queryInput.targetType)
conditions.push(`target_type = $${params.length}`)
}
if (query.dateFrom) {
conditions.push('created_at >= ?')
values.push(query.dateFrom)
if (queryInput.dateFrom) {
params.push(queryInput.dateFrom)
conditions.push(`created_at >= $${params.length}`)
}
if (query.dateTo) {
conditions.push('created_at <= ?')
values.push(query.dateTo)
if (queryInput.dateTo) {
params.push(queryInput.dateTo)
conditions.push(`created_at <= $${params.length}`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const page = Number(query.page) || 1
const pageSize = Number(query.pageSize) || 20
const page = Number(queryInput.page) || 1
const pageSize = Number(queryInput.pageSize) || 20
const offset = (page - 1) * pageSize
const db = getDb()
const items = db.prepare(`
SELECT *
FROM admin_audit_logs
${whereClause}
ORDER BY id DESC
LIMIT ? OFFSET ?
`).all(...values, pageSize, offset)
const totalResult = await query(
`SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`,
params,
)
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM admin_audit_logs
${whereClause}
`).get(...values)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`
SELECT *
FROM admin_audit_logs
${whereClause}
ORDER BY id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items,
total: Number(totalRow?.total || 0),
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
+105 -98
View File
@@ -1,117 +1,124 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function getAdminUserByUsername(username) {
return getDb().prepare('SELECT * FROM admin_users WHERE username = ? LIMIT 1').get(username) || null
export async function getAdminUserById(userId) {
const result = await query(
'SELECT * FROM admin_users WHERE id = $1 LIMIT 1',
[Number(userId)],
)
return result.rows[0] || null
}
export function getAdminUserById(userId) {
return getDb().prepare('SELECT * FROM admin_users WHERE id = ? LIMIT 1').get(userId) || null
export async function getAdminUserByUsername(username) {
const result = await query(
'SELECT * FROM admin_users WHERE username = $1 LIMIT 1',
[String(username || '').trim().toLowerCase()],
)
return result.rows[0] || null
}
export function createAdminUser(input) {
const result = getDb().prepare(`
INSERT INTO admin_users (
username,
password_hash,
role,
status,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?)
`).run(
input.username,
input.passwordHash,
input.role,
input.status,
input.createdAt,
input.updatedAt,
export async function createAdminUser(input) {
const result = await query(
`
INSERT INTO admin_users (
username,
password_hash,
role,
status,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`,
[
input.username,
input.passwordHash,
input.role,
input.status,
input.createdAt,
input.updatedAt,
],
)
return getAdminUserById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function listAdminUsers(query = {}) {
const conditions = []
const values = []
if (query.username) {
conditions.push('username LIKE ?')
values.push(`%${query.username}%`)
}
if (query.role) {
conditions.push('role = ?')
values.push(query.role)
}
if (query.status) {
conditions.push('status = ?')
values.push(query.status)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const page = Number(query.page) || 1
const pageSize = Number(query.pageSize) || 20
const offset = (page - 1) * pageSize
const db = getDb()
const items = db.prepare(`
SELECT *
FROM admin_users
${whereClause}
ORDER BY id DESC
LIMIT ? OFFSET ?
`).all(...values, pageSize, offset)
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM admin_users
${whereClause}
`).get(...values)
return {
items,
total: Number(totalRow?.total || 0),
}
}
export function updateAdminUser(userId, patch = {}) {
const current = getAdminUserById(userId)
export async function updateAdminUser(userId, patch) {
const current = await getAdminUserById(userId)
if (!current) {
return null
}
const next = {
username: Object.prototype.hasOwnProperty.call(patch, 'username') ? patch.username : current.username,
password_hash: Object.prototype.hasOwnProperty.call(patch, 'password_hash') ? patch.password_hash : current.password_hash,
role: Object.prototype.hasOwnProperty.call(patch, 'role') ? patch.role : current.role,
status: Object.prototype.hasOwnProperty.call(patch, 'status') ? patch.status : current.status,
updated_at: Object.prototype.hasOwnProperty.call(patch, 'updated_at') ? patch.updated_at : current.updated_at,
}
getDb().prepare(`
UPDATE admin_users
SET username = ?, password_hash = ?, role = ?, status = ?, updated_at = ?
WHERE id = ?
`).run(
next.username,
next.password_hash,
next.role,
next.status,
next.updated_at,
userId,
const next = { ...current, ...patch }
const result = await query(
`
UPDATE admin_users
SET
username = $1,
password_hash = $2,
role = $3,
status = $4,
updated_at = $5
WHERE id = $6
RETURNING *
`,
[
next.username,
next.password_hash,
next.role,
next.status,
next.updated_at,
Number(userId),
],
)
return getAdminUserById(userId)
return result.rows[0] || null
}
export function countActiveAdminUsers() {
const row = getDb().prepare(`
SELECT COUNT(*) AS total
FROM admin_users
WHERE role = 'admin' AND status = 'active'
`).get()
export async function listAdminUsers({ page = 1, pageSize = 20, username = '', role = '', status = '' } = {}) {
const offset = (page - 1) * pageSize
const filters = []
const params = []
return Number(row?.total || 0)
if (username) {
params.push(`%${username}%`)
filters.push(`username ILIKE $${params.length}`)
}
if (role) {
params.push(role)
filters.push(`role = $${params.length}`)
}
if (status) {
params.push(status)
filters.push(`status = $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query(`SELECT COUNT(*)::int AS total FROM admin_users ${whereClause}`, params)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`
SELECT *
FROM admin_users
${whereClause}
ORDER BY id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export async function countActiveAdminUsers() {
const result = await query(
`SELECT COUNT(*)::int AS total FROM admin_users WHERE role = 'admin' AND status = 'active'`,
)
return Number(result.rows[0]?.total || 0)
}
+189 -86
View File
@@ -1,46 +1,118 @@
import { getDb } from '../db/client.js'
import { query, withTransaction } from '../db/client.js'
export function findFirstAvailableCdkBySkuCode(skuCode) {
return getDb().prepare(`
const CDK_SELECT = `
SELECT
ii.id,
ii.sku_code,
ii.batch_no,
ii.credential_type,
ii.display_value AS cdk_code,
ii.display_value,
ii.status,
ii.invalid_reason,
ii.consumed_at AS delivered_at,
ii.created_at,
ii.updated_at,
tib.task_id AS reserved_by_task_id,
ft.task_no AS reserved_by_task_no,
ft.platform_order_id
FROM inventory_items ii
LEFT JOIN LATERAL (
SELECT *
FROM cdk_inventory
WHERE sku_code = ? AND status = 'available'
ORDER BY id ASC
FROM task_inventory_bindings
WHERE inventory_item_id = ii.id AND binding_status IN ('reserved', 'consumed')
ORDER BY id DESC
LIMIT 1
`).get(skuCode) || null
) tib ON TRUE
LEFT JOIN fulfillment_tasks ft ON ft.id = tib.task_id
`
export async function findFirstAvailableCdkBySkuCode(skuCode, credentialType = 'tencent_code') {
const result = await query(
`${CDK_SELECT}
WHERE ii.sku_code = $1 AND ii.credential_type = $2 AND ii.status = 'available'
ORDER BY ii.id ASC
LIMIT 1`,
[skuCode, credentialType],
)
return result.rows[0] || null
}
export function assignReservedCdk(cdkId, taskId, updatedAt) {
getDb().prepare(`
UPDATE cdk_inventory
SET
status = 'reserved',
reserved_by_task_id = ?,
updated_at = ?
WHERE id = ? AND status = 'available'
`).run(taskId, updatedAt, cdkId)
export async function assignReservedCdk(cdkId, taskId, updatedAt, roleKey = 'primary_code') {
return withTransaction(async (client) => {
const inventoryResult = await client.query(
`
UPDATE inventory_items
SET status = 'reserved', updated_at = $1
WHERE id = $2 AND status = 'available'
RETURNING id
`,
[updatedAt, Number(cdkId)],
)
return getCdkById(cdkId)
if (!inventoryResult.rows[0]) {
return getCdkById(cdkId)
}
await client.query(
`
INSERT INTO task_inventory_bindings (
task_id,
inventory_item_id,
role_key,
quantity,
binding_status,
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, 1, 'reserved', '{}'::jsonb, $4, $4)
ON CONFLICT (task_id, role_key, inventory_item_id) DO UPDATE
SET binding_status = 'reserved', updated_at = EXCLUDED.updated_at, released_at = NULL
`,
[Number(taskId), Number(cdkId), roleKey, updatedAt],
)
return getCdkById(cdkId)
})
}
export function getCdkById(cdkId) {
return getDb().prepare('SELECT * FROM cdk_inventory WHERE id = ? LIMIT 1').get(cdkId) || null
export async function getCdkById(cdkId) {
const result = await query(
`${CDK_SELECT}
WHERE ii.id = $1
LIMIT 1`,
[Number(cdkId)],
)
return result.rows[0] || null
}
export function markCdkDelivered(cdkId, deliveredAt) {
getDb().prepare(`
UPDATE cdk_inventory
SET
status = 'delivered',
delivered_at = ?,
updated_at = ?
WHERE id = ?
`).run(deliveredAt, deliveredAt, cdkId)
export async function markCdkDelivered(cdkId, deliveredAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE inventory_items
SET status = 'consumed', consumed_at = $1, updated_at = $1
WHERE id = $2
`,
[deliveredAt, Number(cdkId)],
)
return getCdkById(cdkId)
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'consumed', consumed_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[deliveredAt, Number(cdkId)],
)
return getCdkById(cdkId)
})
}
export function listCdks({
export async function listCdks({
page = 1,
pageSize = 20,
skuCode = '',
@@ -52,63 +124,78 @@ export function listCdks({
const params = []
if (skuCode) {
filters.push('sku_code = ?')
params.push(skuCode)
filters.push(`ii.sku_code = $${params.length}`)
}
if (status) {
filters.push('status = ?')
params.push(status)
params.push(status === 'delivered' ? 'consumed' : status)
filters.push(`ii.status = $${params.length}`)
}
if (batchNo) {
filters.push('batch_no LIKE ?')
params.push(`%${batchNo}%`)
filters.push(`ii.batch_no ILIKE $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const db = getDb()
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM cdk_inventory
${whereClause}
`).get(...params)
const totalResult = await query(
`SELECT COUNT(*)::int AS total FROM inventory_items ii ${whereClause}`,
params,
)
const items = db.prepare(`
SELECT *
FROM cdk_inventory
${whereClause}
ORDER BY id DESC
LIMIT ? OFFSET ?
`).all(...params, pageSize, offset)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`${CDK_SELECT}
${whereClause}
ORDER BY ii.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
)
return {
items,
total: Number(totalRow?.total || 0),
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export function createCdks(rows) {
const db = getDb()
const stmt = db.prepare(`
INSERT OR IGNORE INTO cdk_inventory (
batch_no,
sku_code,
cdk_code,
status,
reserved_by_task_id,
invalid_reason,
delivered_at,
created_at,
updated_at
) VALUES (?, ?, ?, 'available', NULL, '', NULL, ?, ?)
`)
export async function createCdks(rows) {
let created = 0
for (const row of rows) {
const result = stmt.run(row.batchNo, row.skuCode, row.cdkCode, row.createdAt, row.updatedAt)
if (Number(result.changes || 0) > 0) {
const payloadJson = JSON.stringify(row.payload || { code: row.cdkCode })
const displayValue = String(row.cdkCode || row.displayValue || '').trim()
const result = await query(
`
INSERT INTO inventory_items (
batch_no,
sku_code,
credential_type,
display_value,
payload_json,
source_type,
status,
invalid_reason,
metadata_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, 'static_import', 'available', '', '{}'::jsonb, $6, $7)
ON CONFLICT DO NOTHING
RETURNING id
`,
[
row.batchNo || '',
row.skuCode,
row.credentialType || 'tencent_code',
displayValue,
payloadJson,
row.createdAt,
row.updatedAt,
],
)
if (result.rows[0]?.id) {
created += 1
}
}
@@ -116,28 +203,44 @@ export function createCdks(rows) {
return created
}
export function releaseReservedCdk(cdkId, updatedAt) {
getDb().prepare(`
UPDATE cdk_inventory
SET
status = 'available',
reserved_by_task_id = NULL,
updated_at = ?
WHERE id = ? AND status = 'reserved'
`).run(updatedAt, cdkId)
export async function releaseReservedCdk(cdkId, updatedAt) {
return withTransaction(async (client) => {
await client.query(
`
UPDATE task_inventory_bindings
SET binding_status = 'released', released_at = $1, updated_at = $1
WHERE inventory_item_id = $2 AND binding_status = 'reserved'
`,
[updatedAt, Number(cdkId)],
)
return getCdkById(cdkId)
await client.query(
`
UPDATE inventory_items
SET status = 'available', updated_at = $1
WHERE id = $2 AND status = 'reserved'
`,
[updatedAt, Number(cdkId)],
)
return getCdkById(cdkId)
})
}
export function invalidateCdk(cdkId, invalidReason, updatedAt) {
getDb().prepare(`
UPDATE cdk_inventory
SET
status = 'invalid',
invalid_reason = ?,
updated_at = ?
WHERE id = ? AND status = 'available'
`).run(invalidReason, updatedAt, cdkId)
export async function invalidateCdk(cdkId, invalidReason, updatedAt) {
const result = await query(
`
UPDATE inventory_items
SET status = 'invalid', invalid_reason = $1, updated_at = $2
WHERE id = $3 AND status = 'available'
RETURNING id
`,
[invalidReason, updatedAt, Number(cdkId)],
)
if (!result.rows[0]) {
return getCdkById(cdkId)
}
return getCdkById(cdkId)
}
@@ -1,69 +1,77 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function createClaimToken(input) {
const result = getDb().prepare(`
INSERT INTO claim_tokens (
task_id,
token,
status,
expired_at,
used_at,
max_use_count,
used_count,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.taskId,
input.token,
input.status,
input.expiredAt,
input.usedAt,
input.maxUseCount,
input.usedCount,
input.createdAt,
input.updatedAt,
export async function createClaimToken(input) {
const result = await query(
`
INSERT INTO claim_tokens (
task_id,
token,
status,
expired_at,
used_at,
max_use_count,
used_count,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`,
[
input.taskId,
input.token,
input.status,
input.expiredAt,
input.usedAt || null,
input.maxUseCount,
input.usedCount,
input.createdAt,
input.updatedAt,
],
)
return getClaimTokenById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function getClaimTokenById(tokenId) {
return getDb().prepare('SELECT * FROM claim_tokens WHERE id = ? LIMIT 1').get(tokenId) || null
export async function getClaimTokenById(tokenId) {
const result = await query('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)])
return result.rows[0] || null
}
export function findClaimTokenByToken(token) {
return getDb().prepare('SELECT * FROM claim_tokens WHERE token = ? LIMIT 1').get(token) || null
export async function findClaimTokenByToken(token) {
const result = await query('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()])
return result.rows[0] || null
}
export function updateClaimToken(tokenId, patch) {
const current = getClaimTokenById(tokenId)
export async function updateClaimToken(tokenId, patch) {
const current = await getClaimTokenById(tokenId)
if (!current) {
return null
}
const next = { ...current, ...patch }
getDb().prepare(`
UPDATE claim_tokens
SET
status = ?,
expired_at = ?,
used_at = ?,
max_use_count = ?,
used_count = ?,
updated_at = ?
WHERE id = ?
`).run(
next.status,
next.expired_at,
next.used_at,
next.max_use_count,
next.used_count,
next.updated_at,
tokenId,
const result = await query(
`
UPDATE claim_tokens
SET
status = $1,
expired_at = $2,
used_at = $3,
max_use_count = $4,
used_count = $5,
updated_at = $6
WHERE id = $7
RETURNING *
`,
[
next.status,
next.expired_at,
next.used_at,
next.max_use_count,
next.used_count,
next.updated_at,
Number(tokenId),
],
)
return getClaimTokenById(tokenId)
return result.rows[0] || null
}
@@ -0,0 +1,203 @@
import { query, withTransaction } from '../db/client.js'
export async function getFulfillmentProfileByKey(profileKey) {
const result = await query(
'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1',
[String(profileKey || '').trim()],
)
return result.rows[0] || null
}
export async function upsertFulfillmentProfile(input) {
const result = await query(
`
INSERT INTO fulfillment_profiles (
profile_key,
name,
executor_key,
requires_claim,
auto_dispatch,
inventory_strategy,
config_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9)
ON CONFLICT (profile_key) DO UPDATE
SET
name = EXCLUDED.name,
executor_key = EXCLUDED.executor_key,
requires_claim = EXCLUDED.requires_claim,
auto_dispatch = EXCLUDED.auto_dispatch,
inventory_strategy = EXCLUDED.inventory_strategy,
config_json = EXCLUDED.config_json,
updated_at = EXCLUDED.updated_at
RETURNING *
`,
[
input.profileKey,
input.name,
input.executorKey,
Boolean(input.requiresClaim),
Boolean(input.autoDispatch),
input.inventoryStrategy || 'static_pool',
input.configJson || '{}',
input.createdAt,
input.updatedAt,
],
)
return result.rows[0] || null
}
export async function replaceFulfillmentProfileRequirements(profileId, requirements = [], timestamp) {
await withTransaction(async (client) => {
await client.query(
'DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1',
[Number(profileId)],
)
for (const requirement of requirements) {
await client.query(
`
INSERT INTO fulfillment_profile_requirements (
profile_id,
role_key,
credential_type,
quantity_per_unit,
is_required,
config_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8)
`,
[
Number(profileId),
requirement.roleKey,
requirement.credentialType,
Number(requirement.quantityPerUnit || 1),
requirement.isRequired !== false,
requirement.configJson || '{}',
timestamp,
timestamp,
],
)
}
})
}
export async function listFulfillmentProfileRequirements(profileId) {
const result = await query(
`
SELECT *
FROM fulfillment_profile_requirements
WHERE profile_id = $1
ORDER BY id ASC
`,
[Number(profileId)],
)
return result.rows
}
export async function upsertSkuFulfillmentBinding(input) {
const existing = await query(
`
SELECT *
FROM sku_fulfillment_bindings
WHERE sku_code = $1 AND provider = $2 AND platform = $3 AND shop_id = $4
LIMIT 1
`,
[input.skuCode, input.provider || '', input.platform || '', input.shopId || ''],
)
if (!existing.rows[0]) {
const inserted = await query(
`
INSERT INTO sku_fulfillment_bindings (
sku_code,
provider,
platform,
shop_id,
profile_id,
enabled,
priority,
config_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)
RETURNING *
`,
[
input.skuCode,
input.provider || '',
input.platform || '',
input.shopId || '',
Number(input.profileId),
input.enabled !== false,
Number(input.priority || 100),
input.configJson || '{}',
input.createdAt,
input.updatedAt,
],
)
return inserted.rows[0] || null
}
const updated = await query(
`
UPDATE sku_fulfillment_bindings
SET
profile_id = $1,
enabled = $2,
priority = $3,
config_json = $4::jsonb,
updated_at = $5
WHERE id = $6
RETURNING *
`,
[
Number(input.profileId),
input.enabled !== false,
Number(input.priority || 100),
input.configJson || '{}',
input.updatedAt,
Number(existing.rows[0].id),
],
)
return updated.rows[0] || null
}
export async function resolveFulfillmentBinding({ skuCode, provider = '', platform = '', shopId = '' }) {
const result = await query(
`
SELECT
sfb.*,
fp.profile_key,
fp.name AS profile_name,
fp.executor_key,
fp.requires_claim,
fp.auto_dispatch,
fp.inventory_strategy,
fp.config_json AS profile_config_json
FROM sku_fulfillment_bindings sfb
JOIN fulfillment_profiles fp ON fp.id = sfb.profile_id
WHERE sfb.enabled = TRUE
AND sfb.sku_code = $1
AND (sfb.provider = '' OR sfb.provider = $2)
AND (sfb.platform = '' OR sfb.platform = $3)
AND (sfb.shop_id = '' OR sfb.shop_id = $4)
ORDER BY
CASE WHEN sfb.shop_id = '' THEN 1 ELSE 0 END,
CASE WHEN sfb.platform = '' THEN 1 ELSE 0 END,
CASE WHEN sfb.provider = '' THEN 1 ELSE 0 END,
sfb.priority ASC,
sfb.id ASC
LIMIT 1
`,
[skuCode, provider, platform, shopId],
)
return result.rows[0] || null
}
@@ -1,105 +1,117 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function createMessageDelivery(input) {
const result = getDb().prepare(`
INSERT INTO message_deliveries (
provider,
platform,
shop_id,
shop_name,
channel,
order_id,
task_id,
platform_order_id,
recipient_key,
message_content,
claim_url,
status,
request_url,
request_headers_json,
request_body_json,
response_status,
response_json,
error_message,
sent_at,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.provider,
input.platform,
input.shopId,
input.shopName,
input.channel,
input.orderId,
input.taskId,
input.platformOrderId,
input.recipientKey,
input.messageContent,
input.claimUrl,
input.status,
input.requestUrl,
input.requestHeadersJson,
input.requestBodyJson,
input.responseStatus,
input.responseJson,
input.errorMessage,
input.sentAt,
input.createdAt,
input.updatedAt,
export async function createMessageDelivery(input) {
const result = await query(
`
INSERT INTO message_deliveries (
provider,
platform,
shop_id,
shop_name,
channel,
order_id,
task_id,
platform_order_id,
recipient_key,
message_content,
claim_url,
status,
request_url,
request_headers_json,
request_body_json,
response_status,
response_json,
error_message,
sent_at,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb, $15::jsonb, $16, $17::jsonb, $18, $19, $20, $21)
RETURNING *
`,
[
input.provider,
input.platform,
input.shopId || '',
input.shopName || '',
input.channel,
input.orderId || null,
input.taskId || null,
input.platformOrderId || '',
input.recipientKey || '',
input.messageContent || '',
input.claimUrl || '',
input.status || 'pending',
input.requestUrl || '',
input.requestHeadersJson || '{}',
input.requestBodyJson || '{}',
Number(input.responseStatus || 0),
input.responseJson || '{}',
input.errorMessage || '',
input.sentAt || null,
input.createdAt,
input.updatedAt,
],
)
return getMessageDeliveryById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function getMessageDeliveryById(deliveryId) {
return getDb().prepare('SELECT * FROM message_deliveries WHERE id = ? LIMIT 1').get(deliveryId) || null
export async function getMessageDeliveryById(deliveryId) {
const result = await query('SELECT * FROM message_deliveries WHERE id = $1 LIMIT 1', [Number(deliveryId)])
return result.rows[0] || null
}
export function updateMessageDelivery(deliveryId, patch = {}) {
const current = getMessageDeliveryById(deliveryId)
export async function updateMessageDelivery(deliveryId, patch = {}) {
const current = await getMessageDeliveryById(deliveryId)
if (!current) {
return null
}
const next = { ...current, ...patch }
getDb().prepare(`
UPDATE message_deliveries
SET
status = ?,
request_url = ?,
request_headers_json = ?,
request_body_json = ?,
response_status = ?,
response_json = ?,
error_message = ?,
sent_at = ?,
updated_at = ?
WHERE id = ?
`).run(
next.status,
next.request_url,
next.request_headers_json,
next.request_body_json,
next.response_status,
next.response_json,
next.error_message,
next.sent_at,
next.updated_at,
deliveryId,
const result = await query(
`
UPDATE message_deliveries
SET
status = $1,
request_url = $2,
request_headers_json = $3::jsonb,
request_body_json = $4::jsonb,
response_status = $5,
response_json = $6::jsonb,
error_message = $7,
sent_at = $8,
updated_at = $9
WHERE id = $10
RETURNING *
`,
[
next.status,
next.request_url || '',
next.request_headers_json || '{}',
next.request_body_json || '{}',
Number(next.response_status || 0),
next.response_json || '{}',
next.error_message || '',
next.sent_at || null,
next.updated_at,
Number(deliveryId),
],
)
return getMessageDeliveryById(deliveryId)
return result.rows[0] || null
}
export function findLatestSuccessfulMessageDeliveryByTask(taskId, channel) {
return getDb().prepare(`
SELECT *
FROM message_deliveries
WHERE task_id = ? AND channel = ? AND status = 'success'
ORDER BY id DESC
LIMIT 1
`).get(taskId, channel) || null
export async function findLatestSuccessfulMessageDeliveryByTask(taskId, channel) {
const result = await query(
`
SELECT *
FROM message_deliveries
WHERE task_id = $1 AND channel = $2 AND status = 'success'
ORDER BY id DESC
LIMIT 1
`,
[Number(taskId), channel],
)
return result.rows[0] || null
}
@@ -1,80 +1,53 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function listOrderItemsByOrderId(orderId) {
return getDb().prepare(`
SELECT *
FROM order_items
WHERE order_id = ?
ORDER BY id ASC
`).all(orderId)
export async function listOrderItemsByOrderId(orderId) {
const result = await query(
`
SELECT *
FROM order_items
WHERE order_id = $1
ORDER BY id ASC
`,
[Number(orderId)],
)
return result.rows
}
export function replaceOrderItems(orderId, items) {
const db = getDb()
const existingItems = listOrderItemsByOrderId(orderId)
const updateStatement = db.prepare(`
UPDATE order_items
SET
sku_code = ?,
sku_name = ?,
quantity = ?,
spec_json = ?,
delivery_mode = ?,
updated_at = ?
WHERE id = ?
`)
const insertStatement = db.prepare(`
INSERT INTO order_items (
order_id,
sku_code,
sku_name,
quantity,
spec_json,
delivery_mode,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)
export async function replaceOrderItems(orderId, items) {
await query('DELETE FROM order_items WHERE order_id = $1', [Number(orderId)])
for (let index = 0; index < items.length; index += 1) {
const item = items[index]
const existingItem = existingItems[index] || null
if (existingItem) {
updateStatement.run(
for (const item of items) {
await query(
`
INSERT INTO order_items (
order_id,
sku_code,
sku_name,
quantity,
spec_json,
item_snapshot_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8)
`,
[
Number(orderId),
item.skuCode,
item.skuName,
item.quantity,
item.specJson,
item.deliveryMode,
item.specJson || '{}',
item.itemSnapshotJson || item.specJson || '{}',
item.createdAt,
item.updatedAt,
existingItem.id,
)
continue
}
insertStatement.run(
orderId,
item.skuCode,
item.skuName,
item.quantity,
item.specJson,
item.deliveryMode,
item.createdAt,
item.updatedAt,
],
)
}
if (existingItems.length > items.length) {
const redundantIds = existingItems.slice(items.length).map((item) => item.id)
const placeholders = redundantIds.map(() => '?').join(', ')
db.prepare(`DELETE FROM order_items WHERE order_id = ? AND id IN (${placeholders})`).run(orderId, ...redundantIds)
}
return listOrderItemsByOrderId(orderId)
}
export function getOrderItemById(orderItemId) {
return getDb().prepare('SELECT * FROM order_items WHERE id = ? LIMIT 1').get(orderItemId) || null
export async function getOrderItemById(orderItemId) {
const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
return result.rows[0] || null
}
+123 -109
View File
@@ -1,101 +1,115 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function findOrderByPlatformOrderId({ provider = 'agiso', platform, shopId = '', platformOrderId }) {
return getDb().prepare(`
SELECT *
FROM orders
WHERE provider = ? AND platform = ? AND shop_id = ? AND platform_order_id = ?
LIMIT 1
`).get(provider, platform, shopId, platformOrderId) || null
}
export function createOrder(input) {
const result = getDb().prepare(`
INSERT INTO orders (
provider,
platform,
shop_id,
shop_name,
platform_order_id,
order_status,
pay_status,
buyer_id,
buyer_name,
receiver_contact,
total_amount,
currency,
raw_payload_json,
paid_at,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.provider,
input.platform,
input.shopId,
input.shopName,
input.platformOrderId,
input.orderStatus,
input.payStatus,
input.buyerId,
input.buyerName,
input.receiverContact,
input.totalAmount,
input.currency,
input.rawPayloadJson,
input.paidAt,
input.createdAt,
input.updatedAt,
export async function findOrderByPlatformOrderId({ provider = 'agiso', platform, shopId = '', platformOrderId }) {
const result = await query(
`
SELECT *
FROM orders
WHERE provider = $1 AND platform = $2 AND shop_id = $3 AND platform_order_id = $4
LIMIT 1
`,
[provider, platform, shopId, platformOrderId],
)
return getOrderById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function updateOrder(orderId, input) {
getDb().prepare(`
UPDATE orders
SET
provider = ?,
platform = ?,
shop_id = ?,
shop_name = ?,
order_status = ?,
pay_status = ?,
buyer_id = ?,
buyer_name = ?,
receiver_contact = ?,
total_amount = ?,
currency = ?,
raw_payload_json = ?,
paid_at = ?,
updated_at = ?
WHERE id = ?
`).run(
input.provider,
input.platform,
input.shopId,
input.shopName,
input.orderStatus,
input.payStatus,
input.buyerId,
input.buyerName,
input.receiverContact,
input.totalAmount,
input.currency,
input.rawPayloadJson,
input.paidAt,
input.updatedAt,
orderId,
export async function createOrder(input) {
const result = await query(
`
INSERT INTO orders (
provider,
platform,
shop_id,
shop_name,
platform_order_id,
order_status,
pay_status,
buyer_id,
buyer_name,
receiver_contact,
total_amount,
currency,
raw_payload_json,
paid_at,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14, $15, $16)
RETURNING *
`,
[
input.provider,
input.platform,
input.shopId,
input.shopName,
input.platformOrderId,
input.orderStatus,
input.payStatus,
input.buyerId,
input.buyerName,
input.receiverContact,
input.totalAmount,
input.currency,
input.rawPayloadJson || '{}',
input.paidAt || null,
input.createdAt,
input.updatedAt,
],
)
return getOrderById(orderId)
return result.rows[0] || null
}
export function getOrderById(orderId) {
return getDb().prepare('SELECT * FROM orders WHERE id = ? LIMIT 1').get(orderId) || null
export async function updateOrder(orderId, input) {
const result = await query(
`
UPDATE orders
SET
provider = $1,
platform = $2,
shop_id = $3,
shop_name = $4,
order_status = $5,
pay_status = $6,
buyer_id = $7,
buyer_name = $8,
receiver_contact = $9,
total_amount = $10,
currency = $11,
raw_payload_json = $12::jsonb,
paid_at = $13,
updated_at = $14
WHERE id = $15
RETURNING *
`,
[
input.provider,
input.platform,
input.shopId,
input.shopName,
input.orderStatus,
input.payStatus,
input.buyerId,
input.buyerName,
input.receiverContact,
input.totalAmount,
input.currency,
input.rawPayloadJson || '{}',
input.paidAt || null,
input.updatedAt,
Number(orderId),
],
)
return result.rows[0] || null
}
export function listOrders({
export async function getOrderById(orderId) {
const result = await query('SELECT * FROM orders WHERE id = $1 LIMIT 1', [Number(orderId)])
return result.rows[0] || null
}
export async function listOrders({
page = 1,
pageSize = 20,
platformOrderId = '',
@@ -109,50 +123,50 @@ export function listOrders({
const params = []
if (platformOrderId) {
filters.push('o.platform_order_id LIKE ?')
params.push(`%${platformOrderId}%`)
filters.push(`o.platform_order_id ILIKE $${params.length}`)
}
if (payStatus) {
filters.push('o.pay_status = ?')
params.push(payStatus)
filters.push(`o.pay_status = $${params.length}`)
}
if (skuCode) {
filters.push('EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id AND oi.sku_code = ?)')
params.push(skuCode)
filters.push(`EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id AND oi.sku_code = $${params.length})`)
}
if (dateFrom) {
filters.push('o.created_at >= ?')
params.push(dateFrom)
filters.push(`o.created_at >= $${params.length}`)
}
if (dateTo) {
filters.push('o.created_at <= ?')
params.push(dateTo)
filters.push(`o.created_at <= $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const db = getDb()
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM orders o
${whereClause}
`).get(...params)
const totalResult = await query(`SELECT COUNT(*)::int AS total FROM orders o ${whereClause}`, params)
const items = db.prepare(`
SELECT
o.*,
(SELECT COUNT(*) FROM delivery_tasks dt WHERE dt.order_id = o.id) AS task_count
FROM orders o
${whereClause}
ORDER BY o.id DESC
LIMIT ? OFFSET ?
`).all(...params, pageSize, offset)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`
SELECT
o.*,
(SELECT COUNT(*)::int FROM fulfillment_tasks ft WHERE ft.order_id = o.id) AS task_count
FROM orders o
${whereClause}
ORDER BY o.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items,
total: Number(totalRow?.total || 0),
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
+344 -163
View File
@@ -1,152 +1,227 @@
import { getDb } from '../db/client.js'
import { query, withTransaction } from '../db/client.js'
export function listTasksByOrderId(orderId) {
return getDb().prepare(`
SELECT *
FROM delivery_tasks
WHERE order_id = ?
ORDER BY id ASC
`).all(orderId)
const TASK_FIELDS = `
ft.*,
ct.id AS claim_token_id,
ct.token AS claim_token,
ct.status AS claim_token_status,
ct.expired_at AS claim_token_expired_at,
ctx.browser_session_id,
ctx.login_type,
ctx.nickname,
ctx.role_id,
ctx.role_name,
ctx.area,
ctx.partition_name,
ctx.screenshot_path,
ctx.artifacts_json,
ctx.state_json,
inv.inventory_item_id AS reserved_cdk_id,
inv.display_value AS cdk_code,
inv.credential_type AS cdk_credential_type,
inv.binding_status AS inventory_binding_status
`
const TASK_JOINS = `
FROM fulfillment_tasks ft
LEFT JOIN claim_tokens ct ON ct.task_id = ft.id
LEFT JOIN tencent_browser_contexts ctx ON ctx.task_id = ft.id
LEFT JOIN LATERAL (
SELECT
tib.inventory_item_id,
tib.binding_status,
ii.display_value,
ii.credential_type
FROM task_inventory_bindings tib
JOIN inventory_items ii ON ii.id = tib.inventory_item_id
WHERE tib.task_id = ft.id AND tib.binding_status IN ('reserved', 'consumed')
ORDER BY tib.id ASC
LIMIT 1
) inv ON TRUE
`
function buildTaskSelect(extraFields = '') {
const normalizedExtra = String(extraFields || '').trim()
const fieldSql = normalizedExtra ? `${TASK_FIELDS}, ${normalizedExtra}` : TASK_FIELDS
return `SELECT ${fieldSql} ${TASK_JOINS}`
}
export function createTask(input) {
const result = getDb().prepare(`
INSERT INTO delivery_tasks (
order_id,
order_item_id,
platform_order_id,
task_no,
task_status,
login_type,
claim_token_id,
reserved_cdk_id,
browser_session_id,
nickname,
role_id,
role_name,
area,
partition_name,
result_code,
result_message,
screenshot_path,
artifacts_json,
last_error,
retry_count,
expires_at,
claimed_at,
role_confirmed_at,
redeemed_at,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.orderId,
input.orderItemId,
input.platformOrderId,
input.taskNo,
input.taskStatus,
input.loginType,
input.claimTokenId,
input.reservedCdkId,
input.browserSessionId,
input.nickname,
input.roleId,
input.roleName,
input.area,
input.partitionName,
input.resultCode,
input.resultMessage,
input.screenshotPath,
input.artifactsJson,
input.lastError,
input.retryCount,
input.expiresAt,
input.claimedAt,
input.roleConfirmedAt,
input.redeemedAt,
input.createdAt,
input.updatedAt,
export async function listTasksByOrderId(orderId) {
const result = await query(
`${buildTaskSelect()}
WHERE ft.order_id = $1
ORDER BY ft.id ASC`,
[Number(orderId)],
)
return getTaskById(Number(result.lastInsertRowid))
return result.rows
}
export function updateTask(taskId, patch) {
const current = getTaskById(taskId)
export async function createTask(input) {
return withTransaction(async (client) => {
const taskResult = await client.query(
`
INSERT INTO fulfillment_tasks (
order_id,
order_item_id,
unit_index,
task_no,
provider,
platform,
shop_id,
shop_name,
platform_order_id,
profile_id,
executor_key,
task_status,
inventory_status,
delivery_status,
result_code,
result_message,
claim_token,
claim_expires_at,
automation_mode,
requires_claim,
user_action_status,
attempt_count,
last_error,
context_json,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19,
$20, $21, $22, $23, $24::jsonb, $25, $26
)
RETURNING id
`,
[
input.orderId,
input.orderItemId,
input.unitIndex,
input.taskNo,
input.provider,
input.platform,
input.shopId || '',
input.shopName || '',
input.platformOrderId,
input.profileId,
input.executorKey,
input.taskStatus || 'pending_payment',
input.inventoryStatus || 'pending',
input.deliveryStatus || 'pending',
input.resultCode || '',
input.resultMessage || '',
input.claimToken || '',
input.claimExpiresAt || null,
input.automationMode || 'manual',
Boolean(input.requiresClaim),
input.userActionStatus || 'not_required',
input.attemptCount || 0,
input.lastError || '',
input.contextJson || '{}',
input.createdAt,
input.updatedAt,
],
)
const taskId = Number(taskResult.rows[0]?.id || 0)
if (input.tencentContext) {
await upsertTencentBrowserContextWithClient(client, taskId, input.tencentContext, input.createdAt)
}
return getTaskById(taskId)
})
}
export async function updateTask(taskId, patch) {
const current = await getTaskById(taskId)
if (!current) {
return null
}
const next = { ...current, ...patch }
return withTransaction(async (client) => {
const next = { ...current, ...patch }
getDb().prepare(`
UPDATE delivery_tasks
SET
task_status = ?,
login_type = ?,
claim_token_id = ?,
reserved_cdk_id = ?,
browser_session_id = ?,
nickname = ?,
role_id = ?,
role_name = ?,
area = ?,
partition_name = ?,
result_code = ?,
result_message = ?,
screenshot_path = ?,
artifacts_json = ?,
last_error = ?,
retry_count = ?,
expires_at = ?,
claimed_at = ?,
role_confirmed_at = ?,
redeemed_at = ?,
updated_at = ?
WHERE id = ?
`).run(
next.task_status,
next.login_type,
next.claim_token_id,
next.reserved_cdk_id,
next.browser_session_id,
next.nickname,
next.role_id,
next.role_name,
next.area,
next.partition_name,
next.result_code,
next.result_message,
next.screenshot_path,
next.artifacts_json,
next.last_error,
next.retry_count,
next.expires_at,
next.claimed_at,
next.role_confirmed_at,
next.redeemed_at,
next.updated_at,
taskId,
await client.query(
`
UPDATE fulfillment_tasks
SET
task_status = $1,
inventory_status = $2,
delivery_status = $3,
result_code = $4,
result_message = $5,
claim_token = $6,
claim_expires_at = $7,
automation_mode = $8,
requires_claim = $9,
user_action_status = $10,
attempt_count = $11,
last_error = $12,
context_json = $13::jsonb,
updated_at = $14
WHERE id = $15
`,
[
next.task_status,
next.inventory_status,
next.delivery_status,
next.result_code,
next.result_message,
next.claim_token || '',
next.claim_expires_at || null,
next.automation_mode || 'manual',
Boolean(next.requires_claim),
next.user_action_status || 'not_required',
next.attempt_count || 0,
next.last_error || '',
next.context_json || '{}',
next.updated_at,
Number(taskId),
],
)
if (containsTencentPatch(patch)) {
await upsertTencentBrowserContextWithClient(client, Number(taskId), {
browserSessionId: patch.browser_session_id,
loginType: patch.login_type,
nickname: patch.nickname,
roleId: patch.role_id,
roleName: patch.role_name,
area: patch.area,
partitionName: patch.partition_name,
screenshotPath: patch.screenshot_path,
artifactsJson: patch.artifacts_json,
stateJson: patch.state_json,
}, patch.updated_at || current.updated_at)
}
return getTaskById(taskId)
})
}
export async function getTaskById(taskId) {
const result = await query(
`${buildTaskSelect()}
WHERE ft.id = $1
LIMIT 1`,
[Number(taskId)],
)
return getTaskById(taskId)
return result.rows[0] || null
}
export function getTaskById(taskId) {
return getDb().prepare('SELECT * FROM delivery_tasks WHERE id = ? LIMIT 1').get(taskId) || null
export async function findTaskByClaimTokenId(claimTokenId) {
const result = await query(
`${buildTaskSelect()}
JOIN claim_tokens ctf ON ctf.task_id = ft.id
WHERE ctf.id = $1
LIMIT 1`,
[Number(claimTokenId)],
)
return result.rows[0] || null
}
export function findTaskByClaimTokenId(claimTokenId) {
return getDb().prepare(`
SELECT *
FROM delivery_tasks
WHERE claim_token_id = ?
LIMIT 1
`).get(claimTokenId) || null
}
export function listTasks({
export async function listTasks({
page = 1,
pageSize = 20,
status = '',
@@ -162,67 +237,173 @@ export function listTasks({
const params = []
if (status) {
filters.push('dt.task_status = ?')
params.push(status)
filters.push(`ft.task_status = $${params.length}`)
}
if (platformOrderId) {
filters.push('dt.platform_order_id LIKE ?')
params.push(`%${platformOrderId}%`)
filters.push(`ft.platform_order_id ILIKE $${params.length}`)
}
if (taskNo) {
filters.push('dt.task_no LIKE ?')
params.push(`%${taskNo}%`)
filters.push(`ft.task_no ILIKE $${params.length}`)
}
if (skuCode) {
filters.push('oi.sku_code = ?')
params.push(skuCode)
filters.push(`oi.sku_code = $${params.length}`)
}
if (roleId) {
filters.push('dt.role_id LIKE ?')
params.push(`%${roleId}%`)
filters.push(`ctx.role_id ILIKE $${params.length}`)
}
if (dateFrom) {
filters.push('dt.created_at >= ?')
params.push(dateFrom)
filters.push(`ft.created_at >= $${params.length}`)
}
if (dateTo) {
filters.push('dt.created_at <= ?')
params.push(dateTo)
filters.push(`ft.created_at <= $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const db = getDb()
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM delivery_tasks dt
LEFT JOIN order_items oi ON oi.id = dt.order_item_id
${whereClause}
`).get(...params)
const totalResult = await query(
`
SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
LEFT JOIN tencent_browser_contexts ctx ON ctx.task_id = ft.id
${whereClause}
`,
params,
)
const items = db.prepare(`
SELECT
dt.*,
oi.sku_code,
oi.sku_name,
ci.cdk_code,
ct.token AS claim_token
FROM delivery_tasks dt
LEFT JOIN order_items oi ON oi.id = dt.order_item_id
LEFT JOIN cdk_inventory ci ON ci.id = dt.reserved_cdk_id
LEFT JOIN claim_tokens ct ON ct.id = dt.claim_token_id
${whereClause}
ORDER BY dt.id DESC
LIMIT ? OFFSET ?
`).all(...params, pageSize, offset)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`${buildTaskSelect('oi.sku_code, oi.sku_name, oi.quantity')}
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
${whereClause}
ORDER BY ft.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
)
return {
items,
total: Number(totalRow?.total || 0),
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
async function upsertTencentBrowserContextWithClient(client, taskId, patch = {}, timestamp) {
const currentResult = await client.query(
'SELECT * FROM tencent_browser_contexts WHERE task_id = $1 LIMIT 1',
[Number(taskId)],
)
const current = currentResult.rows[0] || null
const next = {
browser_session_id: patch.browserSessionId ?? current?.browser_session_id ?? '',
login_type: patch.loginType ?? current?.login_type ?? '',
nickname: patch.nickname ?? current?.nickname ?? '',
role_id: patch.roleId ?? current?.role_id ?? '',
role_name: patch.roleName ?? current?.role_name ?? '',
area: patch.area ?? current?.area ?? '',
partition_name: patch.partitionName ?? current?.partition_name ?? '',
screenshot_path: patch.screenshotPath ?? current?.screenshot_path ?? '',
artifacts_json: patch.artifactsJson ?? current?.artifacts_json ?? '{}',
state_json: patch.stateJson ?? current?.state_json ?? '{}',
updated_at: timestamp,
}
if (!current) {
await client.query(
`
INSERT INTO tencent_browser_contexts (
task_id,
browser_session_id,
login_type,
nickname,
role_id,
role_name,
area,
partition_name,
screenshot_path,
artifacts_json,
state_json,
created_at,
updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12, $13)
`,
[
Number(taskId),
next.browser_session_id,
next.login_type,
next.nickname,
next.role_id,
next.role_name,
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
timestamp,
timestamp,
],
)
return
}
await client.query(
`
UPDATE tencent_browser_contexts
SET
browser_session_id = $1,
login_type = $2,
nickname = $3,
role_id = $4,
role_name = $5,
area = $6,
partition_name = $7,
screenshot_path = $8,
artifacts_json = $9::jsonb,
state_json = $10::jsonb,
updated_at = $11
WHERE task_id = $12
`,
[
next.browser_session_id,
next.login_type,
next.nickname,
next.role_id,
next.role_name,
next.area,
next.partition_name,
next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
timestamp,
Number(taskId),
],
)
}
function containsTencentPatch(patch = {}) {
return [
'browser_session_id',
'login_type',
'nickname',
'role_id',
'role_name',
'area',
'partition_name',
'screenshot_path',
'artifacts_json',
'state_json',
].some((key) => Object.prototype.hasOwnProperty.call(patch, key))
}
@@ -1,69 +1,76 @@
import { getDb } from '../db/client.js'
import { query } from '../db/client.js'
export function createWebhookEvent(input) {
const result = getDb().prepare(`
INSERT INTO webhook_events (
provider,
platform,
shop_id,
shop_name,
event_type,
event_key,
signature_valid,
headers_json,
query_json,
body_json,
processed,
process_error,
related_order_id,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
input.provider,
input.platform,
input.shopId,
input.shopName,
input.eventType,
input.eventKey,
input.signatureValid ? 1 : 0,
input.headersJson,
input.queryJson,
input.bodyJson,
input.processed ? 1 : 0,
input.processError,
input.relatedOrderId,
input.createdAt,
export async function createWebhookEvent(input) {
const result = await query(
`
INSERT INTO webhook_events (
provider,
platform,
shop_id,
shop_name,
event_type,
event_key,
signature_valid,
headers_json,
query_json,
body_json,
processed,
process_error,
related_order_id,
created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::jsonb, $11, $12, $13, $14)
RETURNING *
`,
[
input.provider,
input.platform,
input.shopId || '',
input.shopName || '',
input.eventType,
input.eventKey,
Boolean(input.signatureValid),
input.headersJson || '{}',
input.queryJson || '{}',
input.bodyJson || '{}',
Boolean(input.processed),
input.processError || '',
input.relatedOrderId || null,
input.createdAt,
],
)
return getWebhookEventById(Number(result.lastInsertRowid))
return result.rows[0] || null
}
export function updateWebhookEvent(eventId, patch) {
const current = getWebhookEventById(eventId)
export async function updateWebhookEvent(eventId, patch) {
const current = await getWebhookEventById(eventId)
if (!current) {
return null
}
const next = { ...current, ...patch }
const result = await query(
`
UPDATE webhook_events
SET
processed = $1,
process_error = $2,
related_order_id = $3
WHERE id = $4
RETURNING *
`,
[Boolean(next.processed), next.process_error || '', next.related_order_id || null, Number(eventId)],
)
getDb().prepare(`
UPDATE webhook_events
SET
processed = ?,
process_error = ?,
related_order_id = ?
WHERE id = ?
`).run(next.processed ? 1 : 0, next.process_error, next.related_order_id, eventId)
return getWebhookEventById(eventId)
return result.rows[0] || null
}
export function getWebhookEventById(eventId) {
return getDb().prepare('SELECT * FROM webhook_events WHERE id = ? LIMIT 1').get(eventId) || null
export async function getWebhookEventById(eventId) {
const result = await query('SELECT * FROM webhook_events WHERE id = $1 LIMIT 1', [Number(eventId)])
return result.rows[0] || null
}
export function listWebhookEvents({
export async function listWebhookEvents({
page = 1,
pageSize = 20,
provider = '',
@@ -78,62 +85,67 @@ export function listWebhookEvents({
const params = []
if (provider) {
filters.push('provider = ?')
params.push(provider)
filters.push(`provider = $${params.length}`)
}
if (platform) {
filters.push('platform = ?')
params.push(platform)
filters.push(`platform = $${params.length}`)
}
if (processed === '0' || processed === '1') {
filters.push('processed = ?')
params.push(Number(processed))
params.push(processed === '1')
filters.push(`processed = $${params.length}`)
}
if (relatedOrderId) {
filters.push('related_order_id = ?')
params.push(Number(relatedOrderId))
filters.push(`related_order_id = $${params.length}`)
}
if (dateFrom) {
filters.push('created_at >= ?')
params.push(dateFrom)
filters.push(`created_at >= $${params.length}`)
}
if (dateTo) {
filters.push('created_at <= ?')
params.push(dateTo)
filters.push(`created_at <= $${params.length}`)
}
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const db = getDb()
const totalRow = db.prepare(`
SELECT COUNT(*) AS total
FROM webhook_events
${whereClause}
`).get(...params)
const totalResult = await query(`SELECT COUNT(*)::int AS total FROM webhook_events ${whereClause}`, params)
const items = db.prepare(`
SELECT *
FROM webhook_events
${whereClause}
ORDER BY id DESC
LIMIT ? OFFSET ?
`).all(...params, pageSize, offset)
params.push(pageSize)
params.push(offset)
const itemsResult = await query(
`
SELECT *
FROM webhook_events
${whereClause}
ORDER BY id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}
`,
params,
)
return {
items,
total: Number(totalRow?.total || 0),
items: itemsResult.rows,
total: Number(totalResult.rows[0]?.total || 0),
}
}
export function listWebhookEventsByOrderId(orderId) {
return getDb().prepare(`
SELECT *
FROM webhook_events
WHERE related_order_id = ?
ORDER BY id DESC
`).all(orderId)
export async function listWebhookEventsByOrderId(orderId) {
const result = await query(
`
SELECT *
FROM webhook_events
WHERE related_order_id = $1
ORDER BY id DESC
`,
[Number(orderId)],
)
return result.rows
}
@@ -18,7 +18,7 @@ export async function ensureAdminUsersBootstrapped() {
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : []
for (const configuredUser of configuredUsers) {
const username = String(configuredUser?.username || '').trim()
const username = String(configuredUser?.username || '').trim().toLowerCase()
const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role)
@@ -26,12 +26,12 @@ export async function ensureAdminUsersBootstrapped() {
continue
}
if (getAdminUserByUsername(username)) {
if (await getAdminUserByUsername(username)) {
continue
}
const now = nowIso()
createAdminUser({
await createAdminUser({
username,
passwordHash: hashAdminPassword(password),
role,
@@ -42,10 +42,10 @@ export async function ensureAdminUsersBootstrapped() {
}
}
export function loginAdmin(username, password) {
export async function loginAdmin(username, password) {
ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim()
const normalizedUsername = String(username || '').trim().toLowerCase()
const normalizedPassword = String(password || '').trim()
if (!normalizedUsername || !normalizedPassword) {
@@ -55,7 +55,7 @@ export function loginAdmin(username, password) {
})
}
const user = getAdminUserByUsername(normalizedUsername)
const user = await getAdminUserByUsername(normalizedUsername)
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) {
throw createHttpError('账号或密码错误', {
statusCode: 401,
@@ -66,7 +66,7 @@ export function loginAdmin(username, password) {
return createAdminSession(user)
}
export function verifyAdminSessionToken(token) {
export async function verifyAdminSessionToken(token) {
ensureAdminAuthConfigured()
const normalizedToken = String(token || '').trim()
@@ -113,7 +113,7 @@ export function verifyAdminSessionToken(token) {
}
const userId = Number(payload?.uid || 0)
const user = getAdminUserById(userId)
const user = await getAdminUserById(userId)
if (!user || user.status !== 'active') {
throw createHttpError('后台账号已不可用,请重新登录', {
statusCode: 401,
@@ -122,16 +122,16 @@ export function verifyAdminSessionToken(token) {
}
return {
sessionId: String(payload.sid || '').trim(),
userId: user.id,
username: user.username,
sessionId: String(payload?.sid || '').trim(),
userId: Number(user.id),
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
expiresAt,
}
}
export function getAdminSessionSummary(token) {
const session = verifyAdminSessionToken(token)
export async function getAdminSessionSummary(token) {
const session = await verifyAdminSessionToken(token)
return {
authenticated: true,
@@ -155,10 +155,10 @@ export function requireAdminRole(session, allowedRoles) {
})
}
export function getAdminUserList(query = {}) {
export async function getAdminUserList(query = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = listAdminUsers({
const { items, total } = await listAdminUsers({
page,
pageSize,
username: String(query.username || '').trim(),
@@ -172,7 +172,7 @@ export function getAdminUserList(query = {}) {
}
}
export function createManagedAdminUser(payload = {}) {
export async function createManagedAdminUser(payload = {}) {
const username = normalizeUsername(payload.username)
const password = normalizePassword(payload.password)
@@ -193,7 +193,7 @@ export function createManagedAdminUser(payload = {}) {
validateUsername(username)
validatePassword(password)
if (getAdminUserByUsername(username)) {
if (await getAdminUserByUsername(username)) {
throw createHttpError('后台账号已存在', {
statusCode: 409,
errorCode: 'admin_user_exists',
@@ -201,7 +201,7 @@ export function createManagedAdminUser(payload = {}) {
}
const now = nowIso()
const created = createAdminUser({
const created = await createAdminUser({
username,
passwordHash: hashAdminPassword(password),
role: normalizeAdminRole(payload.role),
@@ -215,8 +215,8 @@ export function createManagedAdminUser(payload = {}) {
}
}
export function updateManagedAdminUserRole(userId, payload = {}, session) {
const user = getRequiredAdminUser(userId)
export async function updateManagedAdminUserRole(userId, payload = {}, session) {
const user = await getRequiredAdminUser(userId)
const role = normalizeAdminRole(payload.role)
if (user.role === role) {
@@ -225,8 +225,8 @@ export function updateManagedAdminUserRole(userId, payload = {}, session) {
}
}
ensureAdminUserChangeAllowed(user, { nextRole: role }, session)
const updated = updateAdminUser(user.id, {
await ensureAdminUserChangeAllowed(user, { nextRole: role }, session)
const updated = await updateAdminUser(user.id, {
role,
updated_at: nowIso(),
})
@@ -236,8 +236,8 @@ export function updateManagedAdminUserRole(userId, payload = {}, session) {
}
}
export function updateManagedAdminUserStatus(userId, payload = {}, session) {
const user = getRequiredAdminUser(userId)
export async function updateManagedAdminUserStatus(userId, payload = {}, session) {
const user = await getRequiredAdminUser(userId)
const status = normalizeAdminUserStatus(payload.status)
if (user.status === status) {
@@ -246,8 +246,8 @@ export function updateManagedAdminUserStatus(userId, payload = {}, session) {
}
}
ensureAdminUserChangeAllowed(user, { nextStatus: status }, session)
const updated = updateAdminUser(user.id, {
await ensureAdminUserChangeAllowed(user, { nextStatus: status }, session)
const updated = await updateAdminUser(user.id, {
status,
updated_at: nowIso(),
})
@@ -257,8 +257,8 @@ export function updateManagedAdminUserStatus(userId, payload = {}, session) {
}
}
export function resetManagedAdminUserPassword(userId, payload = {}) {
const user = getRequiredAdminUser(userId)
export async function resetManagedAdminUserPassword(userId, payload = {}) {
const user = await getRequiredAdminUser(userId)
const password = normalizePassword(payload.password)
if (!password) {
@@ -269,7 +269,7 @@ export function resetManagedAdminUserPassword(userId, payload = {}) {
}
validatePassword(password)
const updated = updateAdminUser(user.id, {
const updated = await updateAdminUser(user.id, {
password_hash: hashAdminPassword(password),
updated_at: nowIso(),
})
@@ -298,8 +298,8 @@ function createAdminSession(user) {
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12))
const payload = {
sid: crypto.randomBytes(12).toString('hex'),
uid: user.id,
usr: user.username,
uid: Number(user.id),
usr: String(user.username || ''),
role: normalizeAdminRole(user.role),
iat: issuedAt,
exp: expiresAt,
@@ -311,8 +311,8 @@ function createAdminSession(user) {
token: `${encodedPayload}.${signature}`,
expiresAt,
user: {
userId: user.id,
username: user.username,
userId: Number(user.id),
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
},
}
@@ -409,8 +409,8 @@ function validatePassword(password) {
}
}
function getRequiredAdminUser(userId) {
const user = getAdminUserById(Number(userId))
async function getRequiredAdminUser(userId) {
const user = await getAdminUserById(Number(userId))
if (!user) {
throw createHttpError('后台用户不存在', {
@@ -422,18 +422,18 @@ function getRequiredAdminUser(userId) {
return user
}
function ensureAdminUserChangeAllowed(user, options = {}, session) {
async function ensureAdminUserChangeAllowed(user, options = {}, session) {
const nextRole = options.nextRole || user.role
const nextStatus = options.nextStatus || user.status
if (session?.userId === user.id && (nextRole !== 'admin' || nextStatus !== 'active')) {
if (session?.userId === Number(user.id) && (nextRole !== 'admin' || nextStatus !== 'active')) {
throw createHttpError('不能停用或降级当前登录账号', {
statusCode: 409,
errorCode: 'admin_user_self_change_not_allowed',
})
}
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && countActiveAdminUsers() <= 1) {
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) {
throw createHttpError('至少保留一个启用中的管理员账号', {
statusCode: 409,
errorCode: 'admin_user_last_admin_not_allowed',
@@ -443,8 +443,8 @@ function ensureAdminUserChangeAllowed(user, options = {}, session) {
function mapAdminUser(user) {
return {
userId: user.id,
username: user.username,
userId: Number(user.id),
username: String(user.username || ''),
role: normalizeAdminRole(user.role),
status: normalizeAdminUserStatus(user.status),
createdAt: user.created_at,
+4
View File
@@ -127,6 +127,10 @@ function defaultErrorCodeForStatus(statusCode) {
}
function normalizeResponseDateTime(value) {
if (value instanceof Date) {
return formatResponseDateTime(value.toISOString())
}
if (Array.isArray(value)) {
return value.map((item) => normalizeResponseDateTime(item))
}
+2 -12
View File
@@ -1,4 +1,3 @@
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
@@ -90,17 +89,8 @@ function resolveConsoleMethod(level) {
}
function resolveDataRoot() {
const configuredDatabasePath = String(runtimeConfig.database?.filePath || '').trim()
if (configuredDatabasePath) {
const configuredDataRoot = path.dirname(configuredDatabasePath)
if (existsSync(configuredDataRoot)) {
return configuredDataRoot
}
}
return path.resolve(PROJECT_ROOT, 'data')
const configured = String(runtimeConfig.data?.root || '').trim()
return configured ? path.resolve(configured) : path.resolve(PROJECT_ROOT, 'data')
}
function normalizeLogValue(value) {