功能: - 退货库存预警:横林二仓/苏北/睢宁/山东青岗集/河南洛宁 五张看板, 按各仓客退上架仓对应的自定义库存位取数(各仓位不同:横林/苏北/睢宁 c1,山东 c2) - 支持导入聚水潭「箱及仓位库存」导出文件覆盖创建时间、件数、供应商 (副本只有仓库级数量,没有箱级维度,真实上架时间只能靠导入) - 退货入库核销与每日库存监控:状态落本地系统库,含定时任务与差异清单 - 服务端导出(xlsx/csv),不受列表分页与展示上限约束 稳定性: - 前端幂等请求在后端重启窗口内自动退避重连,不再直接抛 500 - 后端补齐统一 JSON 错误兜底与 /api JSON 404,异常不再返回 HTML - 隧道 ssh 改为绝对路径解析并隐藏窗口,避免非交互式启动 ENOENT 与黑窗弹窗 - 业务库 DATETIME 按北京时间解析(useUTC: false),修复时间显示整体 +8 小时 合并:与上游「隧道断线自愈 + 配置负缓存」为同一问题的两份实现, 本分支采用上游方案(带 TTL 的负缓存 + 统一失效函数 + 单测)。
977 lines
48 KiB
JavaScript
977 lines
48 KiB
JavaScript
const { cleanupRetiredFeatureSettings } = require('./retiredFeatures');
|
||
const database = require('../config/database');
|
||
const { ensureDepartmentPagePermissions, ensureSidebarRolePermissions } = require('./pageAccess');
|
||
const { repairDuplicateExternalDepartments } = require('./socialAuth');
|
||
const { ensureSetupConfigDefaults } = require('./setupWizard');
|
||
const { ensureDesktopPresenceSchema } = require('./desktopPresenceService');
|
||
const { ensureDesktopExecutionTaskSchema } = require('./desktopExecutionTaskService');
|
||
const { ensureThirdPartySsoSchema } = require('./thirdPartySsoService');
|
||
const { ensureThirdPartySsoDirectSchema } = require('./thirdPartySsoDirectService');
|
||
const { ensureProductTagConfigSchema } = require('./productTagConfigService');
|
||
const {
|
||
ensureProductMonitoringSchema,
|
||
warmProductMonitoringDataset,
|
||
} = require('./productMonitoringService');
|
||
const { ensureReturnMonitorSchema } = require('./returnMonitorSchema');
|
||
const {
|
||
ensureReturnStockWarningSchema,
|
||
} = require('./returnStockWarningSchema');
|
||
const { warmReturnStockWarningCache } = require('./returnStockWarningService');
|
||
|
||
const USER_COLUMNS = [
|
||
['auth_source', "VARCHAR(20) NOT NULL DEFAULT 'local' COMMENT 'Recent auth source: local/dingtalk/google'"],
|
||
['avatar_url', "VARCHAR(500) NULL COMMENT 'Avatar URL'"],
|
||
['mobile', "VARCHAR(30) NULL COMMENT 'Mobile phone'"],
|
||
['job_title', "VARCHAR(100) NULL COMMENT 'Job title'"],
|
||
['dingtalk_user_id', "VARCHAR(128) NULL COMMENT 'DingTalk user ID'"],
|
||
['dingtalk_union_id', "VARCHAR(128) NULL COMMENT 'DingTalk union ID'"],
|
||
['google_sub', "VARCHAR(128) NULL COMMENT 'Google subject ID'"],
|
||
['department_names', "TEXT NULL COMMENT 'External department names list'"],
|
||
['dingtalk_department_ids', "TEXT NULL COMMENT 'DingTalk department IDs (JSON)'"],
|
||
];
|
||
|
||
const DEPARTMENT_COLUMNS = [
|
||
['source', "VARCHAR(20) NOT NULL DEFAULT 'local' COMMENT 'Department source: local/dingtalk'"],
|
||
['external_id', "VARCHAR(64) NULL COMMENT 'External department ID'"],
|
||
];
|
||
|
||
const SUMMARY_DAILY_DELIVERY_COLUMNS = [
|
||
['timely_count', "BIGINT DEFAULT 0 COMMENT 'Orders signed within 48 hours'"],
|
||
['timely_rate', "DECIMAL(5,2) DEFAULT 0 COMMENT 'Timely sign rate within 48 hours (%)'"],
|
||
];
|
||
|
||
const SUMMARY_DAILY_DELIVERY_COLUMN_TYPES = [
|
||
['total_delivery_hours', "DECIMAL(12,2) DEFAULT 0 COMMENT 'Total delivery hours (h)'", 'decimal(12,2)'],
|
||
['avg_delivery_hours', "DECIMAL(8,2) DEFAULT 0 COMMENT 'Average delivery hours (h)'", 'decimal(8,2)'],
|
||
];
|
||
|
||
async function getColumnInfo(tableName, columnName) {
|
||
const rows = await database.local.query(
|
||
`SELECT column_type, is_nullable
|
||
FROM information_schema.columns
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = ?
|
||
AND column_name = ?
|
||
LIMIT 1`,
|
||
[tableName, columnName]
|
||
);
|
||
|
||
return rows[0] || null;
|
||
}
|
||
|
||
async function columnExists(tableName, columnName) {
|
||
const column = await getColumnInfo(tableName, columnName);
|
||
return Boolean(column);
|
||
}
|
||
|
||
async function ensureColumn(tableName, columnName, definition) {
|
||
const exists = await columnExists(tableName, columnName);
|
||
if (exists) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} ADD COLUMN \`${columnName}\` ${definition}`);
|
||
}
|
||
|
||
async function ensureColumnType(tableName, columnName, definition, expectedColumnType) {
|
||
const column = await getColumnInfo(tableName, columnName);
|
||
if (!column) {
|
||
await database.local.query(`ALTER TABLE ${tableName} ADD COLUMN \`${columnName}\` ${definition}`);
|
||
return;
|
||
}
|
||
|
||
const actualColumnType = String(column.column_type || column.COLUMN_TYPE || '').toLowerCase();
|
||
if (actualColumnType === expectedColumnType.toLowerCase()) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} MODIFY COLUMN \`${columnName}\` ${definition}`);
|
||
}
|
||
|
||
async function ensureColumnSchema(tableName, columnName, definition, expectedColumnType, nullable) {
|
||
const column = await getColumnInfo(tableName, columnName);
|
||
if (!column) {
|
||
await database.local.query(`ALTER TABLE ${tableName} ADD COLUMN \`${columnName}\` ${definition}`);
|
||
return;
|
||
}
|
||
|
||
const actualColumnType = String(column.column_type || column.COLUMN_TYPE || '').toLowerCase();
|
||
const actualNullable = String(column.is_nullable || column.IS_NULLABLE || '').toUpperCase();
|
||
const expectedNullable = nullable ? 'YES' : 'NO';
|
||
|
||
if (actualColumnType === expectedColumnType.toLowerCase() && actualNullable === expectedNullable) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} MODIFY COLUMN \`${columnName}\` ${definition}`);
|
||
}
|
||
|
||
async function ensureTable(tableName, createSql) {
|
||
await database.local.query(createSql);
|
||
|
||
const rows = await database.local.query(
|
||
`SELECT 1
|
||
FROM information_schema.tables
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = ?
|
||
LIMIT 1`,
|
||
[tableName]
|
||
);
|
||
|
||
if (rows.length === 0) {
|
||
throw new Error(`Table ${tableName} create failed`);
|
||
}
|
||
}
|
||
|
||
async function tableExists(tableName) {
|
||
const rows = await database.local.query(
|
||
`SELECT 1
|
||
FROM information_schema.tables
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = ?
|
||
LIMIT 1`,
|
||
[tableName]
|
||
);
|
||
return rows.length > 0;
|
||
}
|
||
|
||
async function indexExists(tableName, indexName) {
|
||
const rows = await database.local.query(
|
||
`SELECT 1
|
||
FROM information_schema.statistics
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = ?
|
||
AND index_name = ?
|
||
LIMIT 1`,
|
||
[tableName, indexName]
|
||
);
|
||
|
||
return rows.length > 0;
|
||
}
|
||
|
||
async function ensureIndex(tableName, indexName, definition) {
|
||
if (await indexExists(tableName, indexName)) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} ADD INDEX ${indexName} ${definition}`);
|
||
}
|
||
|
||
async function indexHasColumn(tableName, indexName, columnName) {
|
||
const rows = await database.local.query(
|
||
`SELECT 1
|
||
FROM information_schema.statistics
|
||
WHERE table_schema = DATABASE()
|
||
AND table_name = ?
|
||
AND index_name = ?
|
||
AND column_name = ?
|
||
LIMIT 1`,
|
||
[tableName, indexName, columnName]
|
||
);
|
||
|
||
return rows.length > 0;
|
||
}
|
||
|
||
async function dropIndexIfExists(tableName, indexName) {
|
||
if (!(await indexExists(tableName, indexName))) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} DROP INDEX ${indexName}`);
|
||
}
|
||
|
||
async function ensureNamedIndex(tableName, indexName, addClause) {
|
||
if (await indexExists(tableName, indexName)) {
|
||
return;
|
||
}
|
||
|
||
await database.local.query(`ALTER TABLE ${tableName} ADD ${addClause}`);
|
||
}
|
||
|
||
async function ensureRedisConfigColumns() {
|
||
await ensureColumn(
|
||
'redis_configs',
|
||
'profile_key',
|
||
"VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Redis profile key: default/changzhou'"
|
||
);
|
||
await ensureColumn(
|
||
'redis_configs',
|
||
'profile_name',
|
||
"VARCHAR(100) NULL COMMENT 'Redis profile display name'"
|
||
);
|
||
await ensureColumn(
|
||
'redis_configs',
|
||
'enabled',
|
||
"TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether this Redis profile is enabled'"
|
||
);
|
||
await ensureIndex('redis_configs', 'idx_redis_configs_profile_key', '(`profile_key`)');
|
||
|
||
await database.local.query(
|
||
"UPDATE redis_configs SET profile_key = 'default' WHERE profile_key IS NULL OR profile_key = ''"
|
||
);
|
||
await database.local.query(
|
||
"UPDATE redis_configs SET profile_name = '默认缓存库' WHERE profile_key = 'default' AND (profile_name IS NULL OR profile_name = '')"
|
||
);
|
||
await database.local.query(
|
||
"UPDATE redis_configs SET profile_name = '常州缓存库' WHERE profile_key = 'changzhou' AND (profile_name IS NULL OR profile_name = '')"
|
||
);
|
||
}
|
||
|
||
async function seedRedisProfileIfMissing(profile) {
|
||
const rows = await database.local.query(
|
||
'SELECT id FROM redis_configs WHERE profile_key = ? ORDER BY id ASC LIMIT 1',
|
||
[profile.profile_key]
|
||
);
|
||
if (rows.length > 0) {
|
||
return;
|
||
}
|
||
|
||
await database.local.insert('redis_configs', profile);
|
||
}
|
||
|
||
async function seedRedisConfigFromEnv() {
|
||
const defaultHost = String(process.env.REDIS_HOST || '').trim();
|
||
await seedRedisProfileIfMissing({
|
||
profile_key: 'default',
|
||
profile_name: '默认缓存库',
|
||
host: defaultHost,
|
||
port: Number(process.env.REDIS_PORT || 6379),
|
||
password: process.env.REDIS_PASSWORD || '',
|
||
db: Number(process.env.REDIS_DB || 0),
|
||
enabled: defaultHost ? 1 : 0,
|
||
is_default: 1,
|
||
});
|
||
|
||
const changzhouHost = String(
|
||
process.env.CHANGZHOU_REDIS_HOST
|
||
|| process.env.CHANGZHOU_LOCAL_REDIS_HOST
|
||
|| '127.0.0.1'
|
||
).trim();
|
||
await seedRedisProfileIfMissing({
|
||
profile_key: 'changzhou',
|
||
profile_name: '常州缓存库',
|
||
host: changzhouHost,
|
||
port: Number(process.env.CHANGZHOU_REDIS_PORT || process.env.CHANGZHOU_LOCAL_REDIS_PORT || 6379),
|
||
password: process.env.CHANGZHOU_REDIS_PASSWORD || process.env.CHANGZHOU_LOCAL_REDIS_PASSWORD || '',
|
||
db: Number(process.env.CHANGZHOU_REDIS_DB || process.env.CHANGZHOU_LOCAL_REDIS_DB || 0),
|
||
enabled: changzhouHost ? 1 : 0,
|
||
is_default: 0,
|
||
});
|
||
}
|
||
|
||
async function ensureLocalSchema() {
|
||
await ensureTable(
|
||
'ops_b_incentive_snapshots',
|
||
`CREATE TABLE IF NOT EXISTS ops_b_incentive_snapshots (
|
||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
snapshot_key VARCHAR(80) NOT NULL,
|
||
scope_start DATE NOT NULL,
|
||
scope_end DATE NOT NULL,
|
||
metric_end DATE NOT NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'completed',
|
||
finalized TINYINT(1) NOT NULL DEFAULT 0,
|
||
rows_json MEDIUMTEXT NOT NULL,
|
||
summary_json TEXT NULL,
|
||
generated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
completed_at DATETIME NULL,
|
||
error_message TEXT NULL,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uk_ops_b_snapshot_key (snapshot_key),
|
||
KEY idx_ops_b_snapshot_period (scope_start, scope_end, metric_end, status)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
||
);
|
||
await ensureTable(
|
||
'ops_b_incentive_periods',
|
||
`CREATE TABLE IF NOT EXISTS ops_b_incentive_periods (
|
||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
scope_start DATE NOT NULL COMMENT '上架日期区间起',
|
||
scope_end DATE NOT NULL COMMENT '上架日期区间止',
|
||
pull_date DATE NOT NULL COMMENT '拉取(冻结)日期',
|
||
fee_cap DECIMAL(5,4) NOT NULL COMMENT '费比达标上限',
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
UNIQUE KEY uk_ops_b_period_scope (scope_start, scope_end)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
||
);
|
||
await ensureTable(
|
||
'product_analysis_user_prefs',
|
||
`CREATE TABLE IF NOT EXISTS product_analysis_user_prefs (
|
||
user_id BIGINT PRIMARY KEY COMMENT '用户ID',
|
||
prefs_json VARCHAR(2000) NOT NULL COMMENT '个性化配置 JSON',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分析用户个性化配置'`
|
||
);
|
||
if (await tableExists('pricing_calculator_user_prefs')) {
|
||
await database.local.query(
|
||
`INSERT INTO product_analysis_user_prefs (user_id, prefs_json, created_at, updated_at)
|
||
SELECT user_id, prefs_json, created_at, updated_at
|
||
FROM pricing_calculator_user_prefs
|
||
WHERE JSON_VALID(prefs_json)
|
||
AND JSON_EXTRACT(prefs_json, '$.productAnalysisSignalThresholds') IS NOT NULL
|
||
ON DUPLICATE KEY UPDATE
|
||
prefs_json = VALUES(prefs_json),
|
||
updated_at = VALUES(updated_at)`
|
||
);
|
||
}
|
||
await ensureDesktopPresenceSchema();
|
||
await ensureDesktopExecutionTaskSchema();
|
||
await ensureThirdPartySsoSchema();
|
||
await ensureThirdPartySsoDirectSchema();
|
||
await ensureProductTagConfigSchema();
|
||
|
||
await ensureTable(
|
||
'auth_configs',
|
||
`CREATE TABLE IF NOT EXISTS auth_configs (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'Config ID',
|
||
auth_base_url VARCHAR(255) NULL COMMENT 'Auth callback base URL',
|
||
dingtalk_enabled BOOLEAN DEFAULT FALSE COMMENT 'Whether DingTalk login is enabled',
|
||
dingtalk_app_key VARCHAR(255) NULL COMMENT 'DingTalk app key',
|
||
dingtalk_app_secret VARCHAR(255) NULL COMMENT 'DingTalk app secret',
|
||
dingtalk_sync_root_dept_id BIGINT DEFAULT 1 COMMENT 'DingTalk sync root department ID',
|
||
google_enabled BOOLEAN DEFAULT FALSE COMMENT 'Whether Google login is enabled',
|
||
google_client_id VARCHAR(255) NULL COMMENT 'Google client ID',
|
||
google_client_secret VARCHAR(255) NULL COMMENT 'Google client secret',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Created at',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Updated at'
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Auth config table'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'redis_configs',
|
||
`CREATE TABLE IF NOT EXISTS redis_configs (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'Redis config ID',
|
||
profile_key VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Redis profile key: default/changzhou',
|
||
profile_name VARCHAR(100) NULL COMMENT 'Redis profile display name',
|
||
host VARCHAR(255) NULL COMMENT 'Redis host',
|
||
port INT NOT NULL DEFAULT 6379 COMMENT 'Redis port',
|
||
password VARCHAR(255) NULL COMMENT 'Redis password',
|
||
db INT NOT NULL DEFAULT 0 COMMENT 'Redis database index',
|
||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether this Redis profile is enabled',
|
||
is_default BOOLEAN DEFAULT TRUE COMMENT 'Whether this config is default',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Created at',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Updated at',
|
||
INDEX idx_redis_configs_default (is_default),
|
||
INDEX idx_redis_configs_profile_key (profile_key)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Redis config table'`
|
||
);
|
||
await ensureRedisConfigColumns();
|
||
await seedRedisConfigFromEnv();
|
||
|
||
await ensureTable(
|
||
'database_configs',
|
||
`CREATE TABLE IF NOT EXISTS database_configs (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID',
|
||
name VARCHAR(100) NOT NULL COMMENT '配置名称',
|
||
driver VARCHAR(20) NOT NULL COMMENT '驱动: mysql/postgresql/mssql',
|
||
host VARCHAR(255) NOT NULL COMMENT '主机地址',
|
||
port INT NOT NULL DEFAULT 3306 COMMENT '端口',
|
||
database_name VARCHAR(100) NOT NULL COMMENT '数据库名',
|
||
username VARCHAR(100) NOT NULL COMMENT '用户名',
|
||
password VARCHAR(255) COMMENT '密码',
|
||
\`ssl\` BOOLEAN DEFAULT TRUE COMMENT '是否SSL',
|
||
ssl_ca TEXT COMMENT 'SSL CA证书',
|
||
max_pool_size INT DEFAULT 10 COMMENT '最大连接池',
|
||
query_timeout INT DEFAULT 300 COMMENT '查询超时(秒)',
|
||
use_env_config BOOLEAN DEFAULT FALSE COMMENT '是否优先使用 .env 业务数据库配置',
|
||
is_default BOOLEAN DEFAULT FALSE COMMENT '是否默认',
|
||
status ENUM('active', 'inactive') DEFAULT 'active' COMMENT '状态',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
INDEX idx_database_configs_name (name),
|
||
INDEX idx_database_configs_default (is_default)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库配置表'`
|
||
);
|
||
await ensureColumn(
|
||
'database_configs',
|
||
'use_env_config',
|
||
"BOOLEAN DEFAULT FALSE COMMENT '是否优先使用 .env 业务数据库配置'"
|
||
);
|
||
|
||
await ensureTable(
|
||
'department_page_permissions',
|
||
`CREATE TABLE IF NOT EXISTS department_page_permissions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'Department page permission ID',
|
||
department_id BIGINT NOT NULL COMMENT 'Department ID',
|
||
page_key VARCHAR(100) NOT NULL COMMENT 'Page key',
|
||
page_name VARCHAR(100) NOT NULL COMMENT 'Page name',
|
||
allowed BOOLEAN DEFAULT TRUE COMMENT 'Whether access is allowed',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Created at',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Updated at',
|
||
UNIQUE KEY uk_department_page (department_id, page_key),
|
||
INDEX idx_department_id (department_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Department page permission table'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'user_page_permissions',
|
||
`CREATE TABLE IF NOT EXISTS user_page_permissions (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'User page permission ID',
|
||
user_id BIGINT NOT NULL COMMENT 'User ID',
|
||
page_key VARCHAR(100) NOT NULL COMMENT 'Page key',
|
||
page_name VARCHAR(100) NOT NULL COMMENT 'Page name',
|
||
allowed BOOLEAN DEFAULT TRUE COMMENT 'Whether this user can additionally access the page',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Created at',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Updated at',
|
||
UNIQUE KEY uk_user_page (user_id, page_key),
|
||
INDEX idx_user_id (user_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User page permission override table'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'channel_large_customer_monthly_read',
|
||
`CREATE TABLE IF NOT EXISTS channel_large_customer_monthly_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
period_month CHAR(7) NOT NULL COMMENT '统计月份 yyyy-MM',
|
||
channel_name VARCHAR(100) NOT NULL COMMENT '购买渠道',
|
||
shop_name VARCHAR(255) NOT NULL DEFAULT '未归类' COMMENT '店铺名',
|
||
buyer_id VARCHAR(255) NOT NULL COMMENT '买家标识',
|
||
buyer_name VARCHAR(255) NOT NULL COMMENT '买家名称',
|
||
min_single_order_qty INT NOT NULL DEFAULT 5 COMMENT '大客户单笔件数门槛',
|
||
total_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '月累计购买金额',
|
||
total_goods_count DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '月累计购买件数',
|
||
order_count INT NOT NULL DEFAULT 0 COMMENT '月购买频率/订单数',
|
||
first_order_date DATE NULL COMMENT '月首单日期',
|
||
last_order_date DATE NULL COMMENT '月末单日期',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_channel_large_customer_monthly (period_month, channel_name, shop_name, buyer_id, min_single_order_qty),
|
||
INDEX idx_channel_large_customer_month (period_month),
|
||
INDEX idx_channel_large_customer_buyer (buyer_id(120)),
|
||
INDEX idx_channel_large_customer_channel (channel_name),
|
||
INDEX idx_channel_large_customer_shop (shop_name(120)),
|
||
INDEX idx_channel_large_customer_amount (total_amount)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道大客户分析月度读库'`
|
||
);
|
||
await ensureColumn(
|
||
'channel_large_customer_monthly_read',
|
||
'shop_name',
|
||
"VARCHAR(255) NOT NULL DEFAULT '未归类' COMMENT '店铺名' AFTER channel_name"
|
||
);
|
||
if (!(await indexHasColumn('channel_large_customer_monthly_read', 'uk_channel_large_customer_monthly', 'shop_name'))) {
|
||
await dropIndexIfExists('channel_large_customer_monthly_read', 'uk_channel_large_customer_monthly');
|
||
}
|
||
await ensureNamedIndex(
|
||
'channel_large_customer_monthly_read',
|
||
'uk_channel_large_customer_monthly',
|
||
'UNIQUE KEY uk_channel_large_customer_monthly (period_month, channel_name, shop_name, buyer_id, min_single_order_qty)'
|
||
);
|
||
await ensureIndex('channel_large_customer_monthly_read', 'idx_channel_large_customer_shop', '(shop_name(120))');
|
||
|
||
await ensureTable(
|
||
'channel_large_customer_order_read',
|
||
`CREATE TABLE IF NOT EXISTS channel_large_customer_order_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
period_month CHAR(7) NOT NULL COMMENT '统计月份 yyyy-MM',
|
||
channel_name VARCHAR(100) NOT NULL COMMENT '购买渠道',
|
||
shop_name VARCHAR(255) NOT NULL DEFAULT '未归类' COMMENT '店铺名',
|
||
buyer_id VARCHAR(255) NOT NULL COMMENT '买家标识',
|
||
buyer_name VARCHAR(255) NOT NULL COMMENT '买家名称',
|
||
min_single_order_qty INT NOT NULL DEFAULT 5 COMMENT '大客户单笔件数门槛',
|
||
order_no VARCHAR(100) NOT NULL COMMENT '订单编号',
|
||
order_date DATE NULL COMMENT '订单日期',
|
||
order_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '订单金额',
|
||
order_goods_count DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '订单件数',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_channel_large_customer_order (period_month, channel_name, shop_name, buyer_id, min_single_order_qty, order_no),
|
||
INDEX idx_channel_large_customer_order_month (period_month),
|
||
INDEX idx_channel_large_customer_order_buyer (buyer_id(120)),
|
||
INDEX idx_channel_large_customer_order_channel (channel_name),
|
||
INDEX idx_channel_large_customer_order_shop (shop_name(120)),
|
||
INDEX idx_channel_large_customer_order_no (order_no)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道大客户分析订单明细读库'`
|
||
);
|
||
await ensureColumn(
|
||
'channel_large_customer_order_read',
|
||
'shop_name',
|
||
"VARCHAR(255) NOT NULL DEFAULT '未归类' COMMENT '店铺名' AFTER channel_name"
|
||
);
|
||
if (!(await indexHasColumn('channel_large_customer_order_read', 'uk_channel_large_customer_order', 'shop_name'))) {
|
||
await dropIndexIfExists('channel_large_customer_order_read', 'uk_channel_large_customer_order');
|
||
}
|
||
await ensureNamedIndex(
|
||
'channel_large_customer_order_read',
|
||
'uk_channel_large_customer_order',
|
||
'UNIQUE KEY uk_channel_large_customer_order (period_month, channel_name, shop_name, buyer_id, min_single_order_qty, order_no)'
|
||
);
|
||
await ensureIndex('channel_large_customer_order_read', 'idx_channel_large_customer_order_shop', '(shop_name(120))');
|
||
|
||
await ensureTable(
|
||
'channel_large_customer_sync',
|
||
`CREATE TABLE IF NOT EXISTS channel_large_customer_sync (
|
||
period_month CHAR(7) NOT NULL COMMENT '同步月份 yyyy-MM',
|
||
min_single_order_qty INT NOT NULL DEFAULT 5 COMMENT '大客户单笔件数门槛',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
row_count INT NOT NULL DEFAULT 0 COMMENT '同步行数',
|
||
status ENUM('success', 'failed') NOT NULL DEFAULT 'success' COMMENT '同步状态',
|
||
error_message VARCHAR(1000) NULL COMMENT '错误信息',
|
||
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近同步时间',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
PRIMARY KEY (period_month, min_single_order_qty),
|
||
INDEX idx_channel_large_customer_sync_status (status),
|
||
INDEX idx_channel_large_customer_sync_synced_at (synced_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道大客户分析同步日志'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'product_code_items',
|
||
`CREATE TABLE IF NOT EXISTS product_code_items (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||
operation_center VARCHAR(50) NOT NULL COMMENT '运营中心',
|
||
center_abbr VARCHAR(20) NOT NULL COMMENT '中心缩写',
|
||
category_abbr VARCHAR(50) NOT NULL COMMENT '类目缩写',
|
||
serial_number INT NOT NULL COMMENT '序号',
|
||
current_sequence INT NOT NULL COMMENT '目前编写序号',
|
||
style_code VARCHAR(80) NOT NULL COMMENT '款式编码',
|
||
product_code VARCHAR(100) NOT NULL COMMENT '商品编码',
|
||
product_name VARCHAR(255) NOT NULL COMMENT '商品名称',
|
||
color_spec VARCHAR(255) NOT NULL COMMENT '颜色及规格',
|
||
brand VARCHAR(50) NOT NULL DEFAULT '一件代发' COMMENT '品牌',
|
||
category_name VARCHAR(200) NOT NULL COMMENT '分类',
|
||
virtual_category VARCHAR(50) NOT NULL DEFAULT 'ODM' COMMENT '虚拟分类',
|
||
product_tag VARCHAR(100) NOT NULL DEFAULT '1688一件代发' COMMENT '商品标签',
|
||
product_status VARCHAR(50) NOT NULL DEFAULT '启用' COMMENT '商品状态',
|
||
inventory_sync VARCHAR(20) NOT NULL DEFAULT '否' COMMENT '库存同步',
|
||
product_attribute VARCHAR(50) NOT NULL DEFAULT '成品' COMMENT '商品属性',
|
||
weight DECIMAL(12,4) NULL COMMENT '重量kg',
|
||
length_cm DECIMAL(12,4) NULL COMMENT '长cm',
|
||
width_cm DECIMAL(12,4) NULL COMMENT '宽cm',
|
||
height_cm DECIMAL(12,4) NULL COMMENT '高cm',
|
||
volume DECIMAL(18,4) NULL COMMENT '体积',
|
||
unit VARCHAR(20) NOT NULL DEFAULT '个' COMMENT '单位',
|
||
other_attr7 VARCHAR(100) NOT NULL COMMENT '其它属性7',
|
||
other_attr5 VARCHAR(50) NOT NULL DEFAULT '销售' COMMENT '其它属性5',
|
||
created_by_user_id BIGINT NULL COMMENT '创建人ID',
|
||
created_by_username VARCHAR(100) NULL COMMENT '创建人账号',
|
||
created_by_name VARCHAR(100) NULL COMMENT '创建人姓名',
|
||
created_department_id BIGINT NULL COMMENT '创建部门ID',
|
||
created_department_name VARCHAR(100) NULL COMMENT '创建部门名称',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_product_code_items_product_code (product_code),
|
||
KEY idx_product_code_items_style_code (style_code),
|
||
KEY idx_product_code_items_center_category (center_abbr, category_abbr, serial_number),
|
||
KEY idx_product_code_items_category_name (category_name(120)),
|
||
KEY idx_product_code_items_created_at (created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品编码看板数据表'`
|
||
);
|
||
|
||
await dropIndexIfExists('product_code_items', 'uk_product_code_items_style_code');
|
||
await dropIndexIfExists('product_code_items', 'uk_product_code_items_center_category_serial');
|
||
await ensureNamedIndex(
|
||
'product_code_items',
|
||
'idx_product_code_items_style_code',
|
||
'INDEX idx_product_code_items_style_code (style_code)'
|
||
);
|
||
await ensureNamedIndex(
|
||
'product_code_items',
|
||
'idx_product_code_items_center_category',
|
||
'INDEX idx_product_code_items_center_category (center_abbr, category_abbr, serial_number)'
|
||
);
|
||
await ensureColumnSchema('product_code_items', 'length_cm', "DECIMAL(12,4) NULL COMMENT '长cm'", 'decimal(12,4)', true);
|
||
await ensureColumnSchema('product_code_items', 'width_cm', "DECIMAL(12,4) NULL COMMENT '宽cm'", 'decimal(12,4)', true);
|
||
await ensureColumnSchema('product_code_items', 'height_cm', "DECIMAL(12,4) NULL COMMENT '高cm'", 'decimal(12,4)', true);
|
||
await ensureColumnSchema('product_code_items', 'volume', "DECIMAL(18,4) NULL COMMENT '体积'", 'decimal(18,4)', true);
|
||
|
||
await ensureTable(
|
||
'planning_rank_board_shares',
|
||
`CREATE TABLE IF NOT EXISTS planning_rank_board_shares (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||
share_token VARCHAR(120) NOT NULL COMMENT '公开分享令牌',
|
||
share_type VARCHAR(40) NOT NULL DEFAULT 'rank_board_analysis' COMMENT '分享类型',
|
||
title VARCHAR(255) NULL COMMENT '分享标题',
|
||
query_json LONGTEXT NOT NULL COMMENT '分享查询条件 JSON',
|
||
snapshot_batch_id VARCHAR(64) NULL COMMENT '创建分享时的快照批次',
|
||
snapshot_date DATE NULL COMMENT '创建分享时的快照日期',
|
||
calc_version VARCHAR(64) NULL COMMENT '创建分享时的计算版本',
|
||
access_count INT NOT NULL DEFAULT 0 COMMENT '访问次数',
|
||
last_accessed_at DATETIME NULL COMMENT '最后访问时间',
|
||
expires_at DATETIME NULL COMMENT '失效时间',
|
||
status ENUM('active', 'revoked') NOT NULL DEFAULT 'active' COMMENT '状态',
|
||
created_by_user_id BIGINT NULL COMMENT '创建人ID',
|
||
created_by_username VARCHAR(100) NULL COMMENT '创建人账号',
|
||
created_by_name VARCHAR(100) NULL COMMENT '创建人姓名',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_planning_rank_board_shares_token (share_token),
|
||
KEY idx_planning_rank_board_shares_status (status, expires_at),
|
||
KEY idx_planning_rank_board_shares_creator (created_by_user_id, updated_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='榜单数据分析公开分享链接'`
|
||
);
|
||
await ensureColumn('planning_rank_board_shares', 'snapshot_batch_id', "VARCHAR(64) NULL COMMENT '创建分享时的快照批次' AFTER query_json");
|
||
await ensureColumn('planning_rank_board_shares', 'snapshot_date', "DATE NULL COMMENT '创建分享时的快照日期' AFTER snapshot_batch_id");
|
||
await ensureColumn('planning_rank_board_shares', 'calc_version', "VARCHAR(64) NULL COMMENT '创建分享时的计算版本' AFTER snapshot_date");
|
||
await ensureColumn('planning_rank_board_shares', 'access_count', "INT NOT NULL DEFAULT 0 COMMENT '访问次数' AFTER calc_version");
|
||
await ensureColumn('planning_rank_board_shares', 'last_accessed_at', "DATETIME NULL COMMENT '最后访问时间' AFTER access_count");
|
||
await ensureColumn('planning_rank_board_shares', 'expires_at', "DATETIME NULL COMMENT '失效时间' AFTER last_accessed_at");
|
||
await ensureColumn('planning_rank_board_shares', 'status', "ENUM('active', 'revoked') NOT NULL DEFAULT 'active' COMMENT '状态' AFTER expires_at");
|
||
await ensureIndex('planning_rank_board_shares', 'idx_planning_rank_board_shares_status', '(status, expires_at)');
|
||
await ensureIndex('planning_rank_board_shares', 'idx_planning_rank_board_shares_creator', '(created_by_user_id, updated_at)');
|
||
|
||
await ensureTable(
|
||
'product_code_style_registry',
|
||
`CREATE TABLE IF NOT EXISTS product_code_style_registry (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||
operation_center VARCHAR(50) NOT NULL COMMENT '运营中心',
|
||
center_abbr VARCHAR(20) NOT NULL COMMENT '中心缩写',
|
||
category_abbr VARCHAR(50) NOT NULL COMMENT '类目缩写',
|
||
serial_number INT NOT NULL COMMENT '序号',
|
||
style_code VARCHAR(80) NOT NULL COMMENT '款式编码',
|
||
category_name VARCHAR(200) NOT NULL COMMENT '分类',
|
||
created_by_user_id BIGINT NULL COMMENT '创建人ID',
|
||
created_by_username VARCHAR(100) NULL COMMENT '创建人账号',
|
||
created_by_name VARCHAR(100) NULL COMMENT '创建人姓名',
|
||
created_department_id BIGINT NULL COMMENT '创建部门ID',
|
||
created_department_name VARCHAR(100) NULL COMMENT '创建部门名称',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_product_code_style_registry_style_code (style_code),
|
||
UNIQUE KEY uk_product_code_style_registry_center_category_serial (center_abbr, category_abbr, serial_number),
|
||
KEY idx_product_code_style_registry_center_category (center_abbr, category_abbr),
|
||
KEY idx_product_code_style_registry_created_at (created_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品编码款式登记表'`
|
||
);
|
||
|
||
await database.local.query(
|
||
`INSERT INTO product_code_style_registry (
|
||
operation_center,
|
||
center_abbr,
|
||
category_abbr,
|
||
serial_number,
|
||
style_code,
|
||
category_name,
|
||
created_by_user_id,
|
||
created_by_username,
|
||
created_by_name,
|
||
created_department_id,
|
||
created_department_name,
|
||
created_at,
|
||
updated_at
|
||
)
|
||
SELECT
|
||
MIN(operation_center) AS operation_center,
|
||
center_abbr,
|
||
category_abbr,
|
||
serial_number,
|
||
style_code,
|
||
MIN(category_name) AS category_name,
|
||
MAX(created_by_user_id) AS created_by_user_id,
|
||
MAX(created_by_username) AS created_by_username,
|
||
MAX(created_by_name) AS created_by_name,
|
||
MAX(created_department_id) AS created_department_id,
|
||
MAX(created_department_name) AS created_department_name,
|
||
MIN(created_at) AS created_at,
|
||
MAX(updated_at) AS updated_at
|
||
FROM product_code_items
|
||
GROUP BY center_abbr, category_abbr, serial_number, style_code
|
||
ON DUPLICATE KEY UPDATE
|
||
operation_center = VALUES(operation_center),
|
||
category_name = VALUES(category_name),
|
||
created_by_user_id = VALUES(created_by_user_id),
|
||
created_by_username = VALUES(created_by_username),
|
||
created_by_name = VALUES(created_by_name),
|
||
created_department_id = VALUES(created_department_id),
|
||
created_department_name = VALUES(created_department_name),
|
||
created_at = LEAST(created_at, VALUES(created_at)),
|
||
updated_at = GREATEST(updated_at, VALUES(updated_at))`
|
||
);
|
||
|
||
await database.local.query(
|
||
`DELETE registry
|
||
FROM product_code_style_registry registry
|
||
LEFT JOIN (
|
||
SELECT center_abbr, category_abbr, serial_number, style_code
|
||
FROM product_code_items
|
||
GROUP BY center_abbr, category_abbr, serial_number, style_code
|
||
) source
|
||
ON registry.center_abbr = source.center_abbr
|
||
AND registry.category_abbr = source.category_abbr
|
||
AND registry.serial_number = source.serial_number
|
||
AND registry.style_code = source.style_code
|
||
WHERE source.style_code IS NULL`
|
||
);
|
||
|
||
await ensureTable(
|
||
'ali1688_drop_ship_return_addresses',
|
||
`CREATE TABLE IF NOT EXISTS ali1688_drop_ship_return_addresses (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||
drop_ship_id VARCHAR(100) NOT NULL COMMENT '代发ID',
|
||
recipient_name VARCHAR(100) NOT NULL COMMENT '退货联系人',
|
||
recipient_phone VARCHAR(50) NOT NULL COMMENT '联系电话',
|
||
return_address VARCHAR(500) NOT NULL COMMENT '退货地址',
|
||
remark VARCHAR(500) NULL COMMENT '备注',
|
||
status VARCHAR(20) NOT NULL DEFAULT '启用' COMMENT '状态',
|
||
created_by_user_id BIGINT NULL COMMENT '创建人ID',
|
||
created_by_username VARCHAR(100) NULL COMMENT '创建人账号',
|
||
created_by_name VARCHAR(100) NULL COMMENT '创建人姓名',
|
||
created_department_id BIGINT NULL COMMENT '创建部门ID',
|
||
created_department_name VARCHAR(100) NULL COMMENT '创建部门名称',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_ali1688_drop_ship_return_addresses_drop_ship_id (drop_ship_id),
|
||
KEY idx_ali1688_drop_ship_return_addresses_status (status),
|
||
KEY idx_ali1688_drop_ship_return_addresses_updated_at (updated_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1688代发ID退货地址录入表'`
|
||
);
|
||
|
||
for (const [columnName, definition] of USER_COLUMNS) {
|
||
await ensureColumn('users', columnName, definition);
|
||
}
|
||
|
||
for (const [columnName, definition] of DEPARTMENT_COLUMNS) {
|
||
await ensureColumn('departments', columnName, definition);
|
||
}
|
||
|
||
for (const [columnName, definition] of SUMMARY_DAILY_DELIVERY_COLUMNS) {
|
||
await ensureColumn('summary_daily_delivery', columnName, definition);
|
||
}
|
||
|
||
for (const [columnName, definition, expectedColumnType] of SUMMARY_DAILY_DELIVERY_COLUMN_TYPES) {
|
||
await ensureColumnType('summary_daily_delivery', columnName, definition, expectedColumnType);
|
||
}
|
||
|
||
await ensureTable(
|
||
'tmall_fee_ratio_targets',
|
||
`CREATE TABLE IF NOT EXISTS tmall_fee_ratio_targets (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
shop_name VARCHAR(100) NOT NULL COMMENT '店铺名称',
|
||
stat_month CHAR(7) NOT NULL COMMENT '统计月份 yyyy-MM',
|
||
target_fee_ratio DECIMAL(10,4) NOT NULL DEFAULT 0 COMMENT '目标费比',
|
||
remark VARCHAR(500) NULL COMMENT '备注',
|
||
created_by BIGINT NULL COMMENT '创建人ID',
|
||
updated_by BIGINT NULL COMMENT '更新人ID',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_fee_ratio_targets_shop_month (shop_name, stat_month),
|
||
INDEX idx_tmall_fee_ratio_targets_shop_name (shop_name),
|
||
INDEX idx_tmall_fee_ratio_targets_stat_month (stat_month)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫目标费比维护表'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_source_watermarks',
|
||
`CREATE TABLE IF NOT EXISTS tmall_source_watermarks (
|
||
source_key VARCHAR(60) NOT NULL COMMENT '源标识',
|
||
biz_date DATE NOT NULL COMMENT '业务日期',
|
||
ready_type VARCHAR(30) NOT NULL DEFAULT 'batch' COMMENT '就绪判定类型',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本/批次',
|
||
row_count INT NOT NULL DEFAULT 0 COMMENT '探测到的行数',
|
||
status VARCHAR(30) NOT NULL DEFAULT 'pending' COMMENT '状态: missing/candidate/ready/failed',
|
||
observed_payload_json LONGTEXT NULL COMMENT '探测详情JSON',
|
||
first_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '首次发现时间',
|
||
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近观察时间',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
PRIMARY KEY (source_key, biz_date),
|
||
INDEX idx_tmall_source_watermarks_biz_date (biz_date),
|
||
INDEX idx_tmall_source_watermarks_status (status),
|
||
INDEX idx_tmall_source_watermarks_source_version (source_version)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫源数据水位表'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_read_sync',
|
||
`CREATE TABLE IF NOT EXISTS tmall_read_sync (
|
||
domain_key VARCHAR(60) NOT NULL COMMENT '读库领域标识',
|
||
biz_date DATE NOT NULL COMMENT '业务日期',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
row_count INT NOT NULL DEFAULT 0 COMMENT '同步行数',
|
||
status VARCHAR(30) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/success/failed/stale/skipped',
|
||
error_message VARCHAR(1000) NULL COMMENT '错误信息',
|
||
synced_at DATETIME NULL COMMENT '最近同步时间',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
PRIMARY KEY (domain_key, biz_date),
|
||
INDEX idx_tmall_read_sync_biz_date (biz_date),
|
||
INDEX idx_tmall_read_sync_status (status),
|
||
INDEX idx_tmall_read_sync_source_version (source_version)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫读库同步状态表'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_item_daily_read',
|
||
`CREATE TABLE IF NOT EXISTS tmall_item_daily_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
stat_date DATE NOT NULL COMMENT '统计日期',
|
||
shop_name VARCHAR(255) NOT NULL COMMENT '店铺名称',
|
||
shop_key VARCHAR(255) NOT NULL COMMENT '归一化店铺键',
|
||
item_id VARCHAR(100) NOT NULL COMMENT '商品ID',
|
||
product_name VARCHAR(255) NULL COMMENT '商品名称',
|
||
product_status VARCHAR(100) NULL COMMENT '商品状态',
|
||
owner VARCHAR(100) NULL COMMENT '负责人',
|
||
oper VARCHAR(100) NULL COMMENT '运营',
|
||
dept VARCHAR(100) NULL COMMENT '部门',
|
||
real_time INT NULL COMMENT '上架天数/实时值',
|
||
uv DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '访客数',
|
||
cart_cnt DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '加购件数',
|
||
order_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '下单金额',
|
||
pay_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '支付金额',
|
||
trade_cnt DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '支付件数',
|
||
order_cnt DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '下单件数',
|
||
expense DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '推广消耗金额',
|
||
pay_order_cnt DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '支付笔数',
|
||
total_trade_cnt DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '总成交笔数',
|
||
guided_trade_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '推广引导总成交金额',
|
||
sales_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '销售额',
|
||
real_conclude_fee DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '真实费比分子/成交费用',
|
||
clear_sales_cost DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '成交花费',
|
||
goods_count DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '件数',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_item_daily_read (stat_date, shop_key(120), item_id),
|
||
INDEX idx_tmall_item_daily_read_stat_date (stat_date),
|
||
INDEX idx_tmall_item_daily_read_item_id (item_id),
|
||
INDEX idx_tmall_item_daily_read_shop_key (shop_key(120)),
|
||
INDEX idx_tmall_item_daily_read_owner (owner),
|
||
INDEX idx_tmall_item_daily_read_dept (dept)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫商品日维度读库'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_fee_daily_read',
|
||
`CREATE TABLE IF NOT EXISTS tmall_fee_daily_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
stat_date DATE NOT NULL COMMENT '统计日期',
|
||
stat_month CHAR(7) NOT NULL COMMENT '统计月份 yyyy-MM',
|
||
shop_name VARCHAR(255) NOT NULL COMMENT '店铺名称',
|
||
shop_key VARCHAR(255) NOT NULL COMMENT '归一化店铺键',
|
||
item_id VARCHAR(100) NOT NULL COMMENT '商品ID',
|
||
sales_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '销售额',
|
||
real_conclude_fee DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '真实成交费用',
|
||
clear_sales_cost DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '清算销售成本/成交花费',
|
||
profit DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '利润',
|
||
real_fee_ratio DECIMAL(18,6) NULL COMMENT '真实费比',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_fee_daily_read (stat_date, shop_key(120), item_id),
|
||
INDEX idx_tmall_fee_daily_read_stat_month (stat_month),
|
||
INDEX idx_tmall_fee_daily_read_item_id (item_id),
|
||
INDEX idx_tmall_fee_daily_read_shop_key (shop_key(120))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫费比日维度读库'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_feedback_daily_read',
|
||
`CREATE TABLE IF NOT EXISTS tmall_feedback_daily_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
stat_date DATE NOT NULL COMMENT '统计日期',
|
||
shop_name VARCHAR(255) NOT NULL COMMENT '店铺名称',
|
||
shop_key VARCHAR(255) NOT NULL COMMENT '归一化店铺键',
|
||
item_id VARCHAR(100) NOT NULL COMMENT '商品ID',
|
||
product_name VARCHAR(255) NULL COMMENT '商品名称',
|
||
product_img VARCHAR(500) NULL COMMENT '商品图片',
|
||
product_url VARCHAR(500) NULL COMMENT '商品链接',
|
||
bad_feedback_count INT NOT NULL DEFAULT 0 COMMENT '中差评数量',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_feedback_daily_read (stat_date, shop_key(120), item_id),
|
||
INDEX idx_tmall_feedback_daily_read_item_id (item_id),
|
||
INDEX idx_tmall_feedback_daily_read_shop_key (shop_key(120))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫差评日汇总读库'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_feedback_detail_read',
|
||
`CREATE TABLE IF NOT EXISTS tmall_feedback_detail_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
stat_date DATE NOT NULL COMMENT '统计日期',
|
||
created_time DATETIME NULL COMMENT '原始创建时间',
|
||
shop_name VARCHAR(255) NOT NULL COMMENT '店铺名称',
|
||
shop_key VARCHAR(255) NOT NULL COMMENT '归一化店铺键',
|
||
item_id VARCHAR(100) NOT NULL COMMENT '商品ID',
|
||
product_name VARCHAR(255) NULL COMMENT '商品名称',
|
||
product_img VARCHAR(500) NULL COMMENT '商品图片',
|
||
product_url VARCHAR(500) NULL COMMENT '商品链接',
|
||
feed_grade VARCHAR(50) NULL COMMENT '评分',
|
||
feed_grade_type VARCHAR(100) NULL COMMENT '评分类型',
|
||
deal_status VARCHAR(100) NULL COMMENT '处理状态',
|
||
deal_way VARCHAR(100) NULL COMMENT '处理方式',
|
||
operator_name VARCHAR(100) NULL COMMENT '处理人',
|
||
content TEXT NULL COMMENT '反馈内容',
|
||
evaluation_tags TEXT NULL COMMENT '评价标签',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
INDEX idx_tmall_feedback_detail_read_stat_date (stat_date),
|
||
INDEX idx_tmall_feedback_detail_read_item_id (item_id),
|
||
INDEX idx_tmall_feedback_detail_read_shop_key (shop_key(120))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫差评明细读库'`
|
||
);
|
||
await ensureColumn('tmall_feedback_detail_read', 'evaluation_tags', "TEXT NULL COMMENT '评价标签'");
|
||
|
||
await ensureTable(
|
||
'tmall_bundle_snapshot_read',
|
||
`CREATE TABLE IF NOT EXISTS tmall_bundle_snapshot_read (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
biz_date DATE NOT NULL COMMENT '业务日期',
|
||
stat_date_range VARCHAR(50) NULL COMMENT '原始统计范围',
|
||
stat_start_date DATE NULL COMMENT '范围开始日期',
|
||
stat_end_date DATE NULL COMMENT '范围结束日期',
|
||
shop_name VARCHAR(255) NOT NULL COMMENT '店铺名称',
|
||
shop_key VARCHAR(255) NOT NULL COMMENT '归一化店铺键',
|
||
item_id VARCHAR(100) NOT NULL COMMENT '商品ID',
|
||
product_name VARCHAR(255) NULL COMMENT '商品名称',
|
||
direct_pay_people_7d DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '7天直接支付人数',
|
||
indirect_deal_cnt_7d DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '7天间接成交数',
|
||
related_item_cnt INT NOT NULL DEFAULT 0 COMMENT '关联商品数',
|
||
source_version VARCHAR(255) NOT NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_bundle_snapshot_read (biz_date, shop_key(120), item_id, source_version),
|
||
INDEX idx_tmall_bundle_snapshot_read_item_id (item_id),
|
||
INDEX idx_tmall_bundle_snapshot_read_shop_key (shop_key(120))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫连带快照读库'`
|
||
);
|
||
|
||
await ensureTable(
|
||
'tmall_price_limit_snapshot',
|
||
`CREATE TABLE IF NOT EXISTS tmall_price_limit_snapshot (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||
snapshot_date DATE NOT NULL COMMENT '快照日期',
|
||
sku_code VARCHAR(100) NOT NULL COMMENT 'SKU编码',
|
||
owner_route VARCHAR(30) NOT NULL DEFAULT 'default' COMMENT '负责人分流',
|
||
effective_limit_price DECIMAL(18,2) NULL COMMENT '有效限价',
|
||
limit_field_used VARCHAR(100) NULL COMMENT '命中的限价字段',
|
||
source_table VARCHAR(100) NULL COMMENT '来源表',
|
||
source_time DATETIME NULL COMMENT '来源记录时间',
|
||
source_version VARCHAR(255) NULL COMMENT '源版本摘要',
|
||
sync_version VARCHAR(64) NULL COMMENT '同步口径版本',
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
UNIQUE KEY uk_tmall_price_limit_snapshot (snapshot_date, sku_code, owner_route),
|
||
INDEX idx_tmall_price_limit_snapshot_sku_code (sku_code),
|
||
INDEX idx_tmall_price_limit_snapshot_source_table (source_table)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天猫控价快照读库'`
|
||
);
|
||
|
||
await ensureProductMonitoringSchema();
|
||
await ensureReturnMonitorSchema();
|
||
await ensureReturnStockWarningSchema();
|
||
// 退货库存预警数据集(约 1,800 行,首次回源 3~10s)后台预热,不阻塞启动
|
||
void warmReturnStockWarningCache();
|
||
|
||
await ensureSidebarRolePermissions();
|
||
// 商品监控数据集(5.6 万行,构建约 30s)后台预热,不阻塞启动
|
||
void warmProductMonitoringDataset();
|
||
await ensureDepartmentPagePermissions();
|
||
await cleanupRetiredFeatureSettings(database.local);
|
||
await repairDuplicateExternalDepartments();
|
||
await ensureSetupConfigDefaults();
|
||
}
|
||
|
||
module.exports = {
|
||
ensureLocalSchema,
|
||
};
|