cangku/backend/src/services/localSchema.js
lunanting b3f4d1af64 feat: 客退数据监控五仓看板与本地运行稳定性加固
功能:
- 退货库存预警:横林二仓/苏北/睢宁/山东青岗集/河南洛宁 五张看板,
  按各仓客退上架仓对应的自定义库存位取数(各仓位不同:横林/苏北/睢宁 c1,山东 c2)
- 支持导入聚水潭「箱及仓位库存」导出文件覆盖创建时间、件数、供应商
  (副本只有仓库级数量,没有箱级维度,真实上架时间只能靠导入)
- 退货入库核销与每日库存监控:状态落本地系统库,含定时任务与差异清单
- 服务端导出(xlsx/csv),不受列表分页与展示上限约束

稳定性:
- 前端幂等请求在后端重启窗口内自动退避重连,不再直接抛 500
- 后端补齐统一 JSON 错误兜底与 /api JSON 404,异常不再返回 HTML
- 隧道 ssh 改为绝对路径解析并隐藏窗口,避免非交互式启动 ENOENT 与黑窗弹窗
- 业务库 DATETIME 按北京时间解析(useUTC: false),修复时间显示整体 +8 小时

合并:与上游「隧道断线自愈 + 配置负缓存」为同一问题的两份实现,
本分支采用上游方案(带 TTL 的负缓存 + 统一失效函数 + 单测)。
2026-09-23 11:32:16 +08:00

977 lines
48 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};