源修改

This commit is contained in:
ray
2026-06-01 11:25:41 +08:00
parent e5d4b027b2
commit 4af439a6c9
4 changed files with 89 additions and 25 deletions

View File

@@ -1,4 +1,5 @@
ALIYUN_APS_BASE_URL=https://aps.aliyun.com ALIYUN_APS_BASE_URL=https://aps.aliyun.com
ALIYUN_APS_SOURCE_ID=default
ALIYUN_APS_HEADLESS=false ALIYUN_APS_HEADLESS=false
ALIYUN_APS_BROWSER_MODE=launch ALIYUN_APS_BROWSER_MODE=launch
ALIYUN_APS_BROWSER_CHANNEL= ALIYUN_APS_BROWSER_CHANNEL=

View File

@@ -259,6 +259,7 @@ ALIYUN_APS_HOT_FINAL_STATUSES=已完成,已关闭,已取消,已退款完成
`.env` 需要配置: `.env` 需要配置:
```env ```env
ALIYUN_APS_SOURCE_ID=default
ALIYUN_APS_DB_HOST= ALIYUN_APS_DB_HOST=
ALIYUN_APS_DB_PORT=3306 ALIYUN_APS_DB_PORT=3306
ALIYUN_APS_DB_USER= ALIYUN_APS_DB_USER=
@@ -268,6 +269,39 @@ ALIYUN_APS_DB_CHARSET=utf8mb4
ALIYUN_APS_DB_CONNECTION_LIMIT=5 ALIYUN_APS_DB_CONNECTION_LIMIT=5
``` ```
### 多账号 source_id
如果两个 APS 账号写入同一个数据库,每个账号必须配置不同的 `ALIYUN_APS_SOURCE_ID`
```env
# 账号 A
ALIYUN_APS_SOURCE_ID=aliyun_account_a
# 账号 B
ALIYUN_APS_SOURCE_ID=aliyun_account_b
```
同步写库时会把 `source_id` 写入:
- `aps_customer`
- `aps_order`
- `aps_order_detail`
- `aps_bill`
- `aliyun_aps_messages`
增量水位也会按 `source_id` 查询,避免两个账号互相影响。
建议两个账号使用不同项目目录或不同 `data/.browser` 目录,避免本地登录态和 checkpoint 互相覆盖。
生产库建议把唯一键调整为 `source_id + 业务唯一键`,例如:
```sql
-- 示例,实际约束名以生产库为准
-- aps_order: UNIQUE(source_id, order_id)
-- aps_order_detail: UNIQUE(source_id, order_id)
-- aliyun_aps_messages: UNIQUE(source_id, msg_id)
```
## 浏览器配置 ## 浏览器配置
默认不再强制使用 Google Chrome。 默认不再强制使用 Google Chrome。

View File

@@ -36,6 +36,7 @@ const ensureDir = (dirPath) => {
export const config = { export const config = {
rootDir, rootDir,
sourceId: process.env.ALIYUN_APS_SOURCE_ID || 'default',
baseUrl: process.env.ALIYUN_APS_BASE_URL || 'https://aps.aliyun.com', baseUrl: process.env.ALIYUN_APS_BASE_URL || 'https://aps.aliyun.com',
headless: toBool(process.env.ALIYUN_APS_HEADLESS, false), headless: toBool(process.env.ALIYUN_APS_HEADLESS, false),
browserMode: (process.env.ALIYUN_APS_BROWSER_MODE || 'launch').trim().toLowerCase(), browserMode: (process.env.ALIYUN_APS_BROWSER_MODE || 'launch').trim().toLowerCase(),

View File

@@ -7,6 +7,7 @@ let customerMapCache = null;
const MESSAGE_TABLE_DDL = ` const MESSAGE_TABLE_DDL = `
CREATE TABLE IF NOT EXISTS aliyun_aps_messages ( CREATE TABLE IF NOT EXISTS aliyun_aps_messages (
id bigint NOT NULL AUTO_INCREMENT, id bigint NOT NULL AUTO_INCREMENT,
source_id varchar(64) NOT NULL DEFAULT 'default' COMMENT '数据来源账号标识',
msg_id varchar(128) NULL DEFAULT NULL COMMENT '消息原始ID', msg_id varchar(128) NULL DEFAULT NULL COMMENT '消息原始ID',
title text NULL COMMENT '消息标题', title text NULL COMMENT '消息标题',
content text NULL COMMENT '消息内容', content text NULL COMMENT '消息内容',
@@ -28,11 +29,13 @@ CREATE TABLE IF NOT EXISTS aliyun_aps_messages (
extra_data json NULL COMMENT '其他字段(原始JSON)', extra_data json NULL COMMENT '其他字段(原始JSON)',
crawl_time datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '爬取时间', crawl_time datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '爬取时间',
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY uk_msg_id (msg_id) UNIQUE KEY uk_source_msg_id (source_id, msg_id),
KEY idx_source_message_time (source_id, gmt_modified, gmt_created)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阿里云APS站内消息' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阿里云APS站内消息'
`; `;
let customerLifecycleEnsured = false; let customerLifecycleEnsured = false;
const ensuredSourceTables = new Set();
function hasDbConfig() { function hasDbConfig() {
return Boolean(config.db.host && config.db.user && config.db.database); return Boolean(config.db.host && config.db.user && config.db.database);
@@ -135,7 +138,8 @@ async function getCustomerMap() {
if (customerMapCache) { if (customerMapCache) {
return customerMapCache; return customerMapCache;
} }
const [rows] = await getPool().query('SELECT account_id, login_name FROM aps_customer'); await ensureSourceColumn('aps_customer');
const [rows] = await getPool().execute('SELECT account_id, login_name FROM aps_customer WHERE source_id = ?', [config.sourceId]);
const map = new Map(); const map = new Map();
for (const row of rows) { for (const row of rows) {
const loginName = safeString(row.login_name); const loginName = safeString(row.login_name);
@@ -158,8 +162,8 @@ function resolveCustomerAccountId(customerMap, customerAccount) {
return customerMap.get(normalized) || customerMap.get(normalized.replace(/\s+/g, '')) || null; return customerMap.get(normalized) || customerMap.get(normalized.replace(/\s+/g, '')) || null;
} }
async function queryLatestValue(sql) { async function queryLatestValue(sql, params = []) {
const [rows] = await getPool().query(sql); const [rows] = await getPool().execute(sql, params);
const row = Array.isArray(rows) ? rows[0] : null; const row = Array.isArray(rows) ? rows[0] : null;
if (!row) { if (!row) {
return null; return null;
@@ -175,15 +179,18 @@ async function queryLatestValue(sql) {
} }
export async function getLatestOrderTimeFromDb() { export async function getLatestOrderTimeFromDb() {
return queryLatestValue('SELECT MAX(order_time) AS latest_time FROM aps_order'); await ensureSourceColumn('aps_order');
return queryLatestValue('SELECT MAX(order_time) AS latest_time FROM aps_order WHERE source_id = ?', [config.sourceId]);
} }
export async function getLatestBillConsumptionTimeFromDb() { export async function getLatestBillConsumptionTimeFromDb() {
return queryLatestValue('SELECT MAX(consumption_time) AS latest_time FROM aps_bill'); await ensureSourceColumn('aps_bill');
return queryLatestValue('SELECT MAX(consumption_time) AS latest_time FROM aps_bill WHERE source_id = ?', [config.sourceId]);
} }
export async function getLatestMessageTimeFromDb() { export async function getLatestMessageTimeFromDb() {
return queryLatestValue("SELECT MAX(COALESCE(NULLIF(gmt_modified, ''), NULLIF(gmt_created, ''))) AS latest_time FROM aliyun_aps_messages"); await ensureMessagesTable();
return queryLatestValue("SELECT MAX(COALESCE(NULLIF(gmt_modified, ''), NULLIF(gmt_created, ''))) AS latest_time FROM aliyun_aps_messages WHERE source_id = ?", [config.sourceId]);
} }
export async function closeDbPool() { export async function closeDbPool() {
@@ -197,12 +204,14 @@ export async function closeDbPool() {
export async function ensureMessagesTable() { export async function ensureMessagesTable() {
await getPool().query(MESSAGE_TABLE_DDL); await getPool().query(MESSAGE_TABLE_DDL);
await ensureSourceColumn('aliyun_aps_messages');
} }
export async function ensureCustomerLifecycleColumns() { export async function ensureCustomerLifecycleColumns() {
if (customerLifecycleEnsured) { if (customerLifecycleEnsured) {
return; return;
} }
await ensureSourceColumn('aps_customer');
await ensureColumnExists('aps_customer', 'active', "ALTER TABLE aps_customer ADD COLUMN active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有效 1=有效 0=释放'"); await ensureColumnExists('aps_customer', 'active', "ALTER TABLE aps_customer ADD COLUMN active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有效 1=有效 0=释放'");
await ensureColumnExists('aps_customer', 'status', "ALTER TABLE aps_customer ADD COLUMN status VARCHAR(32) DEFAULT 'active' COMMENT '客户状态'"); await ensureColumnExists('aps_customer', 'status', "ALTER TABLE aps_customer ADD COLUMN status VARCHAR(32) DEFAULT 'active' COMMENT '客户状态'");
await ensureColumnExists('aps_customer', 'released_at', "ALTER TABLE aps_customer ADD COLUMN released_at DATETIME NULL COMMENT '释放时间'"); await ensureColumnExists('aps_customer', 'released_at', "ALTER TABLE aps_customer ADD COLUMN released_at DATETIME NULL COMMENT '释放时间'");
@@ -210,6 +219,14 @@ export async function ensureCustomerLifecycleColumns() {
customerLifecycleEnsured = true; customerLifecycleEnsured = true;
} }
async function ensureSourceColumn(tableName) {
if (ensuredSourceTables.has(tableName)) {
return;
}
await ensureColumnExists(tableName, 'source_id', `ALTER TABLE ${tableName} ADD COLUMN source_id VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '数据来源账号标识'`);
ensuredSourceTables.add(tableName);
}
async function ensureColumnExists(tableName, columnName, alterSql) { async function ensureColumnExists(tableName, columnName, alterSql) {
const [rows] = await getPool().execute( const [rows] = await getPool().execute(
`SELECT COUNT(*) AS cnt `SELECT COUNT(*) AS cnt
@@ -233,13 +250,13 @@ export async function upsertCustomers(records) {
await ensureCustomerLifecycleColumns(); await ensureCustomerLifecycleColumns();
const sql = ` const sql = `
INSERT INTO aps_customer ( INSERT INTO aps_customer (
account_id, login_name, real_name, report_source, report_type, trade_mode, source_id, account_id, login_name, real_name, report_source, report_type, trade_mode,
real_name_status, relation_date, follow_staff, payment_notice_status, real_name_status, relation_date, follow_staff, payment_notice_status,
invite_register_type, is_new_customer, performance_start_point_reached, invite_register_type, is_new_customer, performance_start_point_reached,
customer_category, remark, no_consumption_months, customer_category, remark, no_consumption_months,
planned_release_time, planned_release_reason, planned_release_time, planned_release_reason,
active, status, released_at, release_reason active, status, released_at, release_reason
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 'active', NULL, NULL) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 'active', NULL, NULL)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
login_name=VALUES(login_name), login_name=VALUES(login_name),
real_name=VALUES(real_name), real_name=VALUES(real_name),
@@ -271,6 +288,7 @@ export async function upsertCustomers(records) {
continue; continue;
} }
await getPool().execute(sql, [ await getPool().execute(sql, [
config.sourceId,
accountId, accountId,
loginName, loginName,
safeString(record.realName), safeString(record.realName),
@@ -309,7 +327,7 @@ export async function upsertCustomerDetails(records) {
department = ?, department = ?,
payment_notice_status = COALESCE(?, payment_notice_status), payment_notice_status = COALESCE(?, payment_notice_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE account_id = ? WHERE source_id = ? AND account_id = ?
`; `;
for (const record of records) { for (const record of records) {
const accountId = safeString(record.accountId); const accountId = safeString(record.accountId);
@@ -324,6 +342,7 @@ export async function upsertCustomerDetails(records) {
safeString(record.phone), safeString(record.phone),
safeString(record.department), safeString(record.department),
safeString(record.paymentNoticeStatus), safeString(record.paymentNoticeStatus),
config.sourceId,
accountId, accountId,
]); ]);
} }
@@ -336,8 +355,8 @@ async function findCustomerAccountIdByName(customerName) {
return null; return null;
} }
const [rows] = await getPool().execute( const [rows] = await getPool().execute(
'SELECT account_id FROM aps_customer WHERE customer_name = ? OR real_name = ? OR login_name = ? LIMIT 1', 'SELECT account_id FROM aps_customer WHERE source_id = ? AND (customer_name = ? OR real_name = ? OR login_name = ?) LIMIT 1',
[normalized, normalized, normalized], [config.sourceId, normalized, normalized, normalized],
); );
return Array.isArray(rows) && rows.length > 0 ? safeString(rows[0].account_id) : null; return Array.isArray(rows) && rows.length > 0 ? safeString(rows[0].account_id) : null;
} }
@@ -349,8 +368,8 @@ async function markCustomerReleased(customerName, reason, releasedAt) {
} }
await ensureCustomerLifecycleColumns(); await ensureCustomerLifecycleColumns();
await getPool().execute( await getPool().execute(
'UPDATE aps_customer SET active = 0, status = ?, released_at = ?, release_reason = ? WHERE account_id = ?', 'UPDATE aps_customer SET active = 0, status = ?, released_at = ?, release_reason = ? WHERE source_id = ? AND account_id = ?',
['released', releasedAt || new Date().toISOString().slice(0, 19).replace('T', ' '), safeString(reason), accountId], ['released', releasedAt || new Date().toISOString().slice(0, 19).replace('T', ' '), safeString(reason), config.sourceId, accountId],
); );
return true; return true;
} }
@@ -362,8 +381,8 @@ async function markCustomerActive(customerName) {
} }
await ensureCustomerLifecycleColumns(); await ensureCustomerLifecycleColumns();
await getPool().execute( await getPool().execute(
'UPDATE aps_customer SET active = 1, status = ?, released_at = NULL, release_reason = NULL WHERE account_id = ?', 'UPDATE aps_customer SET active = 1, status = ?, released_at = NULL, release_reason = NULL WHERE source_id = ? AND account_id = ?',
['active', accountId], ['active', config.sourceId, accountId],
); );
return true; return true;
} }
@@ -389,13 +408,14 @@ export async function upsertOrders(records) {
if (!records?.length) { if (!records?.length) {
return { inserted: 0 }; return { inserted: 0 };
} }
await ensureSourceColumn('aps_order');
const customerMap = await getCustomerMap(); const customerMap = await getCustomerMap();
const sql = ` const sql = `
INSERT INTO aps_order ( INSERT INTO aps_order (
order_id, customer_account_id, customer_login_name, source_id, order_id, customer_account_id, customer_login_name,
customer_category, order_type, original_price_cny, paid_amount_cny, customer_category, order_type, original_price_cny, paid_amount_cny,
status, order_time, order_month status, order_time, order_month
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
customer_account_id=VALUES(customer_account_id), customer_account_id=VALUES(customer_account_id),
customer_login_name=VALUES(customer_login_name), customer_login_name=VALUES(customer_login_name),
@@ -415,6 +435,7 @@ export async function upsertOrders(records) {
const customerLoginName = safeString(record.customerAccount) || ''; const customerLoginName = safeString(record.customerAccount) || '';
const accountId = resolveCustomerAccountId(customerMap, customerLoginName); const accountId = resolveCustomerAccountId(customerMap, customerLoginName);
await getPool().execute(sql, [ await getPool().execute(sql, [
config.sourceId,
orderId, orderId,
accountId, accountId,
customerLoginName, customerLoginName,
@@ -434,14 +455,15 @@ export async function upsertOrderDetails(records) {
if (!records?.length) { if (!records?.length) {
return { inserted: 0 }; return { inserted: 0 };
} }
await ensureSourceColumn('aps_order_detail');
const sql = ` const sql = `
INSERT INTO aps_order_detail ( INSERT INTO aps_order_detail (
order_id, order_type, status, trade_type, customer_category, source_id, order_id, order_type, status, trade_type, customer_category,
dealer_name, dealer_uid, customer_type, opportunity_id, dealer_name, dealer_uid, customer_type, opportunity_id,
payment_time, order_time, product_name, product_code, payment_time, order_time, product_name, product_code,
original_price_cny, paid_amount_cny, discount, original_price_cny, paid_amount_cny, discount,
payable_amount_cny, coupon_amount_cny payable_amount_cny, coupon_amount_cny
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
order_type=VALUES(order_type), order_type=VALUES(order_type),
status=VALUES(status), status=VALUES(status),
@@ -467,6 +489,7 @@ export async function upsertOrderDetails(records) {
continue; continue;
} }
await getPool().execute(sql, [ await getPool().execute(sql, [
config.sourceId,
orderId, orderId,
safeString(record.orderType), safeString(record.orderType),
safeString(record.status), safeString(record.status),
@@ -494,10 +517,12 @@ export async function upsertBills(records) {
if (!records?.length) { if (!records?.length) {
return { inserted: 0 }; return { inserted: 0 };
} }
await ensureSourceColumn('aps_bill');
const customerMap = await getCustomerMap(); const customerMap = await getCustomerMap();
const selectSql = ` const selectSql = `
SELECT id FROM aps_bill SELECT id FROM aps_bill
WHERE billing_month = ? WHERE source_id = ?
AND billing_month = ?
AND commission_month = ? AND commission_month = ?
AND customer_login_name = ? AND customer_login_name = ?
AND consumption_time = ? AND consumption_time = ?
@@ -507,7 +532,7 @@ export async function upsertBills(records) {
`; `;
const insertSql = ` const insertSql = `
INSERT INTO aps_bill ( INSERT INTO aps_bill (
billing_month, customer_account_id, customer_login_name, source_id, billing_month, customer_account_id, customer_login_name,
bill_type, consumption_time, bill_type, consumption_time,
customer_category, customer_category,
product_name, product_category, product_name, product_category,
@@ -517,7 +542,7 @@ export async function upsertBills(records) {
rebated, rebated,
invite_register_type, invite_register_type,
service_start_time, service_end_time service_start_time, service_end_time
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`; `;
const updateSql = ` const updateSql = `
UPDATE aps_bill SET UPDATE aps_bill SET
@@ -546,6 +571,7 @@ export async function upsertBills(records) {
const consumptionTime = safeString(record.consumeDate); const consumptionTime = safeString(record.consumeDate);
const originalPrice = safeNumber(record.originalPriceCny); const originalPrice = safeNumber(record.originalPriceCny);
const [rows] = await getPool().execute(selectSql, [ const [rows] = await getPool().execute(selectSql, [
config.sourceId,
billingMonth, billingMonth,
commissionMonth, commissionMonth,
customerLoginName, customerLoginName,
@@ -572,6 +598,7 @@ export async function upsertBills(records) {
} }
await getPool().execute(insertSql, [ await getPool().execute(insertSql, [
config.sourceId,
billingMonth, billingMonth,
accountId, accountId,
customerLoginName, customerLoginName,
@@ -601,11 +628,11 @@ export async function upsertMessages(records) {
await ensureMessagesTable(); await ensureMessagesTable();
const sql = ` const sql = `
INSERT INTO aliyun_aps_messages ( INSERT INTO aliyun_aps_messages (
msg_id, title, content, msg_type, from_app, biz_code, msg_channel, source_id, msg_id, title, content, msg_type, from_app, biz_code, msg_channel,
category_id, category_name, lv1_category_id, lv2_category_id, lv3_category_id, category_id, category_name, lv1_category_id, lv2_category_id, lv3_category_id,
message_classification, customer_name, order_no, status, message_classification, customer_name, order_no, status,
gmt_created, gmt_modified, extra_data gmt_created, gmt_modified, extra_data
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
title=VALUES(title), title=VALUES(title),
content=VALUES(content), content=VALUES(content),
@@ -633,6 +660,7 @@ export async function upsertMessages(records) {
continue; continue;
} }
await getPool().execute(sql, [ await getPool().execute(sql, [
config.sourceId,
msgId, msgId,
safeString(record.title), safeString(record.title),
safeString(record.content), safeString(record.content),