diff --git a/cmd/channel_http_test.go b/cmd/channel_http_test.go index ab1bcd9..ba94501 100644 --- a/cmd/channel_http_test.go +++ b/cmd/channel_http_test.go @@ -119,7 +119,7 @@ func TestChannelHTTPListsMetricsOptionsAndAtomicCreation(t *testing.T) { rootRow := func(data map[string]any) map[string]any { for _, raw := range data["rows"].([]any) { row := raw.(map[string]any) - if row["id"] == float64(100) && row["sourceDb"] == "lx" { + if row["id"] == "100" && row["sourceDb"] == "lx" { return row } } @@ -151,7 +151,7 @@ func TestChannelHTTPListsMetricsOptionsAndAtomicCreation(t *testing.T) { list(8, "?lockStatus=-1", 1) list(1, "?busierAccount=leaf", 2) options := call(8, "GET", "/sw/promote/bindOptions?sourceDb=lx", "", 0).([]any) - if len(options) != 1 || options[0].(map[string]any)["platformBusierId"] != float64(14) { + if len(options) != 1 || options[0].(map[string]any)["platformBusierId"] != "14" { t.Fatal("上级未获取下级正常绑定") } body := `{"sourceDb":"lx","platformBusierId":"14","account":"中文渠道","password":"demo123","registerType":1}` @@ -193,6 +193,14 @@ func TestChannelHTTPListsMetricsOptionsAndAtomicCreation(t *testing.T) { if game["gameName"] != "汇总游戏" || game["registerCount"] != float64(2) || game["payAmount"] != "70.00" { t.Fatalf("游戏分布误用了原始注册/充值或错误金额口径: %+v", game) } + if err := db.Exec("UPDATE tab_promote SET id=9007199254740993 WHERE source_db='lx' AND id=100").Error; err != nil { + t.Fatal(err) + } + large := list(8, "?keyword=main-channel", 1)["rows"].([]any)[0].(map[string]any) + if large["id"] != "9007199254740993" { + t.Fatalf("HTTP response rounded channel ID: %v", large["id"]) + } + call(8, "GET", "/sw/promote/game-distribution?sourceDb=lx&promoteId="+large["id"].(string), "", 0) if err := db.Exec("DROP TABLE tab_promote").Error; err != nil { t.Fatal(err) } diff --git a/cmd/channel_portrait_http_test.go b/cmd/channel_portrait_http_test.go index 3341b22..285a27d 100644 --- a/cmd/channel_portrait_http_test.go +++ b/cmd/channel_portrait_http_test.go @@ -180,7 +180,7 @@ func TestChannelPortraitHTTPTotalTiersAndScope(t *testing.T) { } data = call(7, "sourceDb=lx&promoteId=101&period=last7", 0) basic = data["basic"].(map[string]any) - if basic["parentPromoteId"] != float64(100) || basic["parentPromoteName"] != "root-account" || basic["lastLoginTime"] != nil { + if basic["parentPromoteId"] != "100" || basic["parentPromoteName"] != "root-account" || basic["lastLoginTime"] != nil { t.Fatal("上级应读同平台当前账号,不能沿用旧快照名") } call(99, "sourceDb=lx&promoteId=100", 7) @@ -189,7 +189,7 @@ func TestChannelPortraitHTTPTotalTiersAndScope(t *testing.T) { t.Fatal(err) } data = call(7, "sourceDb=lx&promoteId=100", 0) - if data["available"] != false || data["metrics"] != nil || data["basic"].(map[string]any)["promoteId"] != float64(100) { + if data["available"] != false || data["metrics"] != nil || data["basic"].(map[string]any)["promoteId"] != "100" { t.Fatal("缺数据时应保留已授权基本资料并标明未知") } } diff --git a/cmd/ledger_http_test.go b/cmd/ledger_http_test.go index 0fc1b15..ca264a5 100644 --- a/cmd/ledger_http_test.go +++ b/cmd/ledger_http_test.go @@ -110,7 +110,7 @@ func TestLedgerHTTPQueriesAndExcelExports(t *testing.T) { data := call(7, "GET", "/sw/register/list?sourceDb=lx&sortField=totalPayAmount", "", 0).(map[string]any) rows := data["rows"].([]any) first := rows[0].(map[string]any) - if data["total"] != float64(2) || first["userId"] != float64(1) || first["totalPayAmount"] != "1012.34" || first["fgameName"] != "历史游戏" || first["busierName"] != "owner→leaf" { + if data["total"] != float64(2) || first["userId"] != "1" || first["totalPayAmount"] != "1012.34" || first["fgameName"] != "历史游戏" || first["busierName"] != "owner→leaf" { t.Fatalf("注册明细口径或字段错误: %+v", data) } games := call(7, "GET", "/sw/serverAnalysis/games?sourceDb=lx&limit=50", "", 0).([]any) diff --git a/cmd/main.go b/cmd/main.go index d55b8dc..70aa1a2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,7 +9,9 @@ import ( "strings" "kra/internal/config" + "kra/internal/data" "kra/internal/logging" + "kra/internal/modules" "kra/internal/server/router" "kra/internal/service/dto" systemservice "kra/internal/service/system" @@ -32,7 +34,9 @@ var ( // Version is the version of the compiled software. Version string // flagconf is the config flag. - flagconf string + flagconf string + flagMigrate bool + flagCheckSchema bool id, _ = os.Hostname() ) @@ -47,6 +51,8 @@ const ( func init() { flag.StringVar(&flagconf, "conf", "./configs", "config path, eg: -conf config.yaml") + flag.BoolVar(&flagMigrate, "migrate", false, "apply and verify all migrations, then exit without starting the service") + flag.BoolVar(&flagCheckSchema, "check-schema", false, "verify migration versions and required schema without writing, then exit") } // taskRegistry builds the process-wide registry from the dependency-free task @@ -159,6 +165,20 @@ func main() { loggerControl.Reload(root, options) }) defer unsubscribeLogger() + if flagMigrate || flagCheckSchema { + if flagMigrate && flagCheckSchema { + panic("-migrate and -check-schema are mutually exclusive") + } + var database *config.Database + if bc.Data != nil { + database = bc.Data.Database + } + if err := data.MigrateDatabase(context.Background(), database, modules.Catalog(), logger, flagCheckSchema); err != nil { + panic(err) + } + logger.Info("database migration verification completed", "mod", "system", "check_only", flagCheckSchema) + return + } application, cleanup, err := wireApp(bc.Server, runtime, logger, loggerControl, Version) if err != nil { diff --git a/deploy/docker/README.md b/deploy/docker/README.md index 65c284c..6909113 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -15,7 +15,22 @@ - 后端仅绑定 `127.0.0.1:9602`,前端仅绑定 `127.0.0.1:9601`。 - 宝塔 `sw2.fuhua91.com` 代理至 9601,前端容器将 `/api/` 转发至后端并去除前缀,支持 WebSocket、SSE 和文件上传。 -数据库沿用已迁移并验证的 `sw10`,平台别名为 `lx/hw/yq/oa`,其中三个 SDK 别名连接 `game_lx/game_hw/game_yq` 测试库,Redis 沿用当前关闭状态。生产配置使用 `admin.app.env: production`,关闭开发 Swagger。七条常规任务启用(四条商务/区服日汇总、预警检测、两条有效端刷新),两个历史回填停用。**使用同一 sw10 时只运行一个调度实例,切换到服务器前停止本机的后端服务。** +数据库沿用已迁移并验证的 `sw10`,设置 `admin.system.disable_auto_migrate: true`,由部署前的独立迁移命令更新结构。平台别名为 `lx/hw/yq/oa`,其中三个 SDK 别名连接 `game_lx/game_hw/game_yq` 测试库,Redis 沿用当前关闭状态。生产配置使用 `admin.app.env: production`,关闭开发 Swagger。七条常规任务启用(四条商务/区服日汇总、预警检测、两条有效端刷新),两个历史回填停用。**使用同一 sw10 时只运行一个调度实例,切换到服务器前停止本机的后端服务。** + +## 数据库迁移 + +新副本必须先完整复制旧商务数据库,再执行 [旧框架转换 SQL](../migrations/legacy-framework.sql),最后使用本次发布的后端执行全部增量迁移: + +```bash +/app/kratos-admin -conf /data/conf/config.yaml -migrate +/app/kratos-admin -conf /data/conf/config.yaml -check-schema +``` + +框架转换 SQL 的 `USE` 默认指向 sw10,其他目标库需修改该行。`-migrate` 使用与服务相同的模块迁移目录,包含新表、字段、索引、权限及任务定义,完成检查后退出;不会启动 HTTP、定时任务或 SDK 连接。`-check-schema` 只读检查版本、必需表和字段、主键与索引。旧业务表缺失时应重新完成复制,不能用空表替代历史数据。 + +转换 SQL 和增量迁移必须全部执行。服务关闭自动迁移后,会在连接失败、版本未完成或结构缺失时明确报错,不进入初始化向导。新增迁移需同时更新模块结构检查与相邻回归测试。部署脚本在停止旧调度实例后自动运行 `-migrate`,全部检查通过才启动新服务。 + +旧库 `tab_promote.id` 的 INT 自增容量由增量迁移扩大为同符号 BIGINT,保留现存 ID、自增高水位、关联数据与注释。渠道、玩家、流水等可能变大的旧业务 ID 在 JSON 和 Excel 中使用文本,请求兼容十进制字符串;不降低自增值或重新编号。 ## 构建与发布 @@ -35,9 +50,9 @@ bash /www/wwwroot/sw2.fuhua91.com/releases//deploy.sh --build-o bash /www/wwwroot/sw2.fuhua91.com/releases//deploy.sh ``` -脚本先构建和检查镜像,再停止当前受管容器,保留一份停止的 `-previous` 容器。健康检查失败时恢复前一组容器。配置、上传文件和数据库不随镜像替换;索引迁移由后端启动按版本执行。 +脚本先构建和检查镜像,再停止当前受管容器,保留一份停止的 `-previous` 容器。迁移或健康检查失败时恢复前一组容器。配置、上传文件和数据库不随镜像替换;索引在独立迁移容器中按版本执行。 -首次启动持续等待建表与索引迁移完成,以后端健康检查通过为准,不设置总等待时限。容器退出或重启时立即回退;迁移日志记录每一步的名称、结果和耗时。切换数据库前需备份运行配置;镜像回退不会自动还原数据库连接配置。 +迁移不设置总等待时限,日志记录每一步的名称、结果和耗时;耗时索引完成前不会启动业务服务。业务容器退出或重启时立即回退。切换数据库前需备份运行配置;镜像回退不会自动还原数据库连接配置或撤销已执行的数据库迁移,回退版本需兼容当前结构和迁移版本。 首次接入域名时,备份宝塔原站点配置,再安装 `bt-nginx-sw2.fuhua91.com.conf` 至 `/www/server/panel/vhost/nginx/`,执行 `/www/server/nginx/sbin/nginx -t` 通过后重载。模板保留证书验证目录,当前监听 HTTP;后续由宝塔为 sw2 单独配置匹配域名的证书。 diff --git a/deploy/docker/build-package.ps1 b/deploy/docker/build-package.ps1 index adfff11..2b6a52a 100644 --- a/deploy/docker/build-package.ps1 +++ b/deploy/docker/build-package.ps1 @@ -47,6 +47,7 @@ try { foreach ($name in @('deploy.sh', 'wait-backend.sh', 'bt-nginx-sw2.fuhua91.com.conf')) { Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination (Join-Path $stage $name) } + Copy-Item -LiteralPath (Join-Path $PSScriptRoot '..\migrations') -Destination (Join-Path $stage 'migrations') -Recurse & tar -czf $archive -C $stage . if ($LASTEXITCODE -ne 0) { throw 'Release archive failed' } $hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/deploy/docker/deploy.sh b/deploy/docker/deploy.sh index b439e04..68e227b 100644 --- a/deploy/docker/deploy.sh +++ b/deploy/docker/deploy.sh @@ -91,6 +91,12 @@ for name in "${NAMES[@]}"; do fi done +echo 'Applying and verifying database migrations before starting the service.' +docker run --rm --name sw2-migrate --label com.sw2.application=sw2 \ + --network "$NETWORK" --env-file "$BASE_DIR/runtime.env" \ + -v "$BASE_DIR/config:/data/conf:ro" -v "$BASE_DIR/log:/app/logs" \ + "$BACKEND_IMAGE" /app/kratos-admin -conf /data/conf/config.yaml -migrate + created+=(sw2-backend) docker run -d --name sw2-backend --label com.sw2.application=sw2 \ --restart unless-stopped --network "$NETWORK" \ diff --git a/deploy/migrations/legacy-framework.sql b/deploy/migrations/legacy-framework.sql new file mode 100644 index 0000000..749766a --- /dev/null +++ b/deploy/migrations/legacy-framework.sql @@ -0,0 +1,1769 @@ +-- SW → Kra 旧框架转换:完整迁移必须继续执行当前后端的 -migrate 和 -check-schema。 +-- 服务设置 admin.system.disable_auto_migrate: true;目标库由下方 USE 配置。 +-- 新副本:备份源表、创建 Kra 表、转换框架数据及关联,并归档旧表。 +-- 已迁移副本:自动执行菜单调整,不重新导入账号,不重建已存在的表。 +-- 检测到部分迁移状态时立即停止,防止覆盖未完成的数据。 +-- 首次迁移要求 26 张源表已存在;支持包含真实规则、预警和日志的完整业务副本。 +-- 本脚本不包含账号明文、密码哈希或连接凭据;原始业务大表保留在目标副本中。 +-- 请使用支持 DELIMITER 的 MySQL 客户端,在同一连接执行,遇错停止,不使用 --force。 +-- 工作台复用仪表盘;商务管理、数据分析、预警中心依次排在仪表盘下方。 +-- 与当前 Go 版本配套使用;由 -migrate 追加全部业务结构,转换 SQL 导入的任务保持停用。 +-- 已迁移分支不依赖已清理的旧源表或备份表;执行前请备份当前菜单及角色元数据。 +-- 唯一的库名配置:下方为测试副本 sw10;仅对已备份的目标副本执行,连接地址由数据库客户端选择。 +-- MySQL 5.7 不支持 USE @变量,因此只修改下一行,正文自动使用当前数据库。 +USE `sw10`; +SET @sw_kra_target_db = DATABASE(); +SET NAMES utf8mb4; +SET SESSION time_zone='+08:00'; +DELIMITER $$ +DROP PROCEDURE IF EXISTS sw_kra_migrate_20261103$$ +CREATE PROCEDURE sw_kra_migrate_20261103() +migration_run: BEGIN +DECLARE v_actual BIGINT DEFAULT 0; +DECLARE v_expected BIGINT DEFAULT 0; +DECLARE v_iter INT DEFAULT 0; +DECLARE v_lock INT DEFAULT 0; +DECLARE v_lock_name VARCHAR(64); +DECLARE v_dashboard BIGINT UNSIGNED DEFAULT NULL; +DECLARE v_workbench BIGINT UNSIGNED DEFAULT NULL; +DECLARE EXIT HANDLER FOR SQLEXCEPTION +BEGIN + ROLLBACK; + IF v_lock=1 THEN DO RELEASE_LOCK(v_lock_name); END IF; + RESIGNAL; +END; +IF DATABASE() IS NULL OR @sw_kra_target_db IS NULL OR NOT (BINARY DATABASE() <=> BINARY @sw_kra_target_db) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='当前数据库与配置不一致'; +END IF; +-- 按目标库生成迁移锁,避免不同数据库互相阻塞,名称不超过 MySQL 的 64 字符限制。 +SET v_lock_name=CONCAT('sw_to_kra:',MD5(DATABASE())); +SELECT GET_LOCK(v_lock_name,10) INTO v_lock; +IF COALESCE(v_lock,0)<>1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Migration lock unavailable'; END IF; +-- 一、已迁移库:确认目标表完整、迁移账本已完成,再调整菜单。 +IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='sys_users') THEN + IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() + AND table_name IN ('sys_schema_migrations','sys_users','sys_authorities','sys_base_menus','sys_user_authority','sys_authority_menus','sys_base_menu_parameters','sys_apis','sys_ignore_apis','sys_authority_apis','casbin_rule','sys_base_menu_btns','sys_authority_btns','sys_departments','sys_positions','sys_user_departments','sys_user_positions','sys_authority_departments','sys_dictionaries','sys_dictionary_details','sys_params','sys_api_tokens','jwt_blacklists','sys_security_config','sys_versions','sys_export_templates','sys_export_template_condition','sys_export_template_join','sys_operation_records','sys_login_logs','sys_data_access_logs','sys_error','media_file_upload_and_downloads','media_attachment_category','media_uploads','media_upload_chunks','sys_announcements','sys_integration_configs','sys_timed_tasks','sys_timed_task_logs','pay_orders','pay_events','sys_user_bind_platform','sys_user_preference','sys_alert_rule','sys_alert_record','sys_alert_operation_log','sys_message','sw_kra_id_map') AND engine='InnoDB')<>49 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Kra target tables incomplete; refusing partial migration'; + END IF; + IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='sw_kra_state') THEN + IF NOT EXISTS(SELECT 1 FROM sw_kra_state WHERE id=1 AND phase='completed') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Previous migration not completed'; + END IF; + ELSEIF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='sys_user') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Legacy users remain without completed migration state'; + END IF; + START TRANSACTION; + IF (SELECT COUNT(*) FROM sys_base_menus WHERE name='dashboard' AND parent_id=0 AND deleted_at IS NULL)<>1 + OR (SELECT COUNT(*) FROM sys_base_menus WHERE name='sw_workbench')>1 + OR (SELECT COUNT(DISTINCT name) FROM sys_base_menus WHERE parent_id=0 AND deleted_at IS NULL + AND name IN ('sw_BusMgmt','sw_DataAnalysis','sw_AlertCenter'))<>3 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Required menu identities differ'; + END IF; + SELECT id INTO v_dashboard FROM sys_base_menus WHERE name='dashboard' AND parent_id=0 AND deleted_at IS NULL FOR UPDATE; + SET v_workbench=(SELECT id FROM sys_base_menus WHERE name='sw_workbench'); + IF EXISTS(SELECT 1 FROM sys_base_menus WHERE parent_id=v_workbench) + OR EXISTS(SELECT 1 FROM sys_base_menu_btns WHERE sys_base_menu_id=v_workbench AND name<>'sw:workbench:query') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Workbench has unexpected children/buttons'; + END IF; + + -- 先补齐仪表盘授权,再移除重复工作台;已有授权不重复插入。 + INSERT INTO sys_authority_menus(sys_authority_authority_id,sys_base_menu_id) + SELECT a.authority_id,v_dashboard FROM sys_authorities a + WHERE (a.default_router IN ('sw_workbench','dashboard') OR EXISTS( + SELECT 1 FROM sys_authority_menus x WHERE x.sys_authority_authority_id=a.authority_id AND x.sys_base_menu_id=v_workbench)) + AND NOT EXISTS(SELECT 1 FROM sys_authority_menus x WHERE x.sys_authority_authority_id=a.authority_id AND x.sys_base_menu_id=v_dashboard); + UPDATE sys_authorities SET default_router='dashboard' WHERE default_router='sw_workbench'; + + -- 删除工作台页面、查询按钮及其关联,保留业务接口和查询权限。 + DELETE x FROM sys_authority_btns x LEFT JOIN sys_base_menu_btns b ON b.id=x.sys_base_menu_btn_id + WHERE x.sys_menu_id=v_workbench OR b.sys_base_menu_id=v_workbench; + DELETE FROM sys_authority_menus WHERE sys_base_menu_id=v_workbench; + DELETE FROM sys_base_menu_parameters WHERE sys_base_menu_id=v_workbench; + DELETE m FROM sw_kra_id_map m LEFT JOIN sys_base_menu_btns b ON b.id=m.new_id + WHERE m.entity='menu' AND (m.new_id=v_workbench OR b.sys_base_menu_id=v_workbench); + DELETE FROM sys_base_menu_btns WHERE sys_base_menu_id=v_workbench; + DELETE FROM sys_base_menus WHERE id=v_workbench; + + -- 使用固定排序值,确保重复执行结果一致;不调整子菜单排序。 + UPDATE sys_base_menus SET sort=CASE name + WHEN 'dashboard' THEN 1 + WHEN 'sw_BusMgmt' THEN 2 + WHEN 'sw_DataAnalysis' THEN 3 + WHEN 'sw_AlertCenter' THEN 4 + WHEN 'permission' THEN 5 + WHEN 'org' THEN 6 + WHEN 'systemConfig' THEN 7 + WHEN 'monitor' THEN 8 + WHEN 'media' THEN 9 + WHEN 'extensions' THEN 13 + WHEN 'person' THEN 16 + ELSE sort END WHERE parent_id=0; + + -- RuoYi SVG 名称转换为 Kra 已注册图标;只修复旧值,保留后续自定义选择。 + UPDATE sys_base_menus SET icon=CASE name + WHEN 'sw_BusMgmt' THEN 'Briefcase' + WHEN 'sw_BusinessList' THEN 'UserFilled' + WHEN 'sw_ChannelList' THEN 'Connection' + WHEN 'sw_ChannelActivationList' THEN 'CircleCheck' + WHEN 'sw_DataAnalysis' THEN 'DataAnalysis' + WHEN 'sw_ChannelDataStatistics' THEN 'Histogram' + WHEN 'sw_RechargeDetails' THEN 'Money' + WHEN 'sw_RegistrationDetails' THEN 'User' + WHEN 'sw_GameAnalytics' THEN 'TrendCharts' + WHEN 'sw_ServerAnalysis' THEN 'Monitor' + WHEN 'sw_RankingAnalysis' THEN 'Star' + WHEN 'sw_AlertCenter' THEN 'Bell' + WHEN 'sw_AlertList' THEN 'Warning' + WHEN 'sw_AlertRules' THEN 'Setting' + ELSE icon END + WHERE name IN ('sw_BusMgmt','sw_BusinessList','sw_ChannelList','sw_ChannelActivationList','sw_DataAnalysis','sw_ChannelDataStatistics','sw_RechargeDetails','sw_RegistrationDetails','sw_GameAnalytics','sw_ServerAnalysis','sw_RankingAnalysis','sw_AlertCenter','sw_AlertList','sw_AlertRules') + AND (icon IS NULL OR icon IN ('', 'category', 'email', 'chart', 'company', 'international', 'color', 'dict', 'cascader', 'education', 'excel', 'star', 'process-definition', 'documentation', 'example', 'data-analysis', 'user', 'money', 'warn')); + + IF EXISTS(SELECT 1 FROM sys_base_menus WHERE name='sw_workbench' OR path='sw-index') + OR EXISTS(SELECT 1 FROM sys_base_menu_btns WHERE name='sw:workbench:query') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Duplicate workbench remains'; + END IF; + IF EXISTS(SELECT 1 FROM sys_authorities a WHERE NOT EXISTS( + SELECT 1 FROM sys_base_menus m JOIN sys_authority_menus x ON x.sys_base_menu_id=m.id + WHERE x.sys_authority_authority_id=a.authority_id AND m.name=a.default_router AND m.deleted_at IS NULL)) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Role default router not accessible'; + END IF; + IF EXISTS(SELECT 1 FROM sys_authority_menus x LEFT JOIN sys_base_menus m ON m.id=x.sys_base_menu_id WHERE m.id IS NULL) + OR EXISTS(SELECT 1 FROM sys_authority_btns x LEFT JOIN sys_base_menu_btns b ON b.id=x.sys_base_menu_btn_id + LEFT JOIN sys_base_menus m ON m.id=x.sys_menu_id WHERE b.id IS NULL OR m.id IS NULL OR b.sys_base_menu_id<>x.sys_menu_id) + OR EXISTS(SELECT 1 FROM sys_base_menu_parameters x LEFT JOIN sys_base_menus m ON m.id=x.sys_base_menu_id WHERE m.id IS NULL) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Dangling menu relationship'; + END IF; + COMMIT; + DO RELEASE_LOCK(v_lock_name); SET v_lock=0; + SELECT '菜单调整完成,原有框架和业务数据已保留' AS result; + LEAVE migration_run; +END IF; + +-- 二、未迁移的新副本:核查源表及迁移状态,再执行完整迁移。 +IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name IN ('sw_kra_state','sys_users','sys_schema_migrations','bak_sw2kra_20261103_sys_user')) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Target already migrated or partially prepared; do not overwrite'; +END IF; +SELECT COUNT(*) INTO v_actual FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name IN ('sys_social','sys_dept','sys_user','sys_post','sys_role','sys_menu','sys_user_role','sys_role_menu','sys_role_dept','sys_user_post','sys_oper_log','sys_dict_type','sys_dict_data','sys_config','sys_login_info','sys_notice','sys_message','sys_oss','sys_oss_config','sys_client','sys_user_bind_platform','sys_user_preference','sj_job','sys_alert_rule','sys_alert_record','sys_alert_operation_log') AND engine='InnoDB'; + +IF v_actual<>26 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Source table set/engine differs'; END IF; + +IF EXISTS(SELECT 1 FROM sys_user WHERE del_flag='0' AND LEFT(password,4) NOT IN ('$2a$','$2b$','$2y$')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Unsupported source password format'; END IF; + +IF EXISTS(SELECT user_name FROM sys_user WHERE del_flag='0' GROUP BY user_name HAVING COUNT(*)>1) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Duplicate source username'; END IF; + +IF EXISTS(SELECT u.user_id FROM sys_user u LEFT JOIN sys_user_role ur ON ur.user_id=u.user_id LEFT JOIN sys_role r ON r.role_id=ur.role_id AND r.del_flag='0' WHERE u.del_flag='0' GROUP BY u.user_id HAVING COUNT(DISTINCT r.role_id)<>1) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Source user needs exactly one active role'; END IF; + +IF EXISTS(SELECT 1 FROM sys_role WHERE del_flag='0' AND data_scope NOT IN ('1','2','3','4','5')) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Unsupported source data scope'; END IF; + +IF EXISTS(SELECT 1 FROM sys_user u LEFT JOIN sys_user p ON p.user_id=u.parent_id WHERE u.del_flag='0' AND u.parent_id>0 AND (p.user_id IS NULL OR p.del_flag<>'0' OR p.level>=u.level)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Invalid source business ancestry'; END IF; + +-- 三、创建迁移账本和批次备份,逐表核对行数与字段内容。 + +CREATE TABLE sw_kra_state (id INT PRIMARY KEY, version VARCHAR(80) NOT NULL, phase VARCHAR(40) NOT NULL, started_at DATETIME(3), completed_at DATETIME(3)) ENGINE=InnoDB; + +INSERT INTO sw_kra_state VALUES(1,'20261103_framework_migration_v5','backup',NOW(3),NULL); + +CREATE TABLE sw_kra_checks (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,stage VARCHAR(32),check_name VARCHAR(160),expected_value BIGINT,actual_value BIGINT,passed TINYINT NOT NULL) ENGINE=InnoDB; + +CREATE TABLE `bak_sw2kra_20261103_sys_social` LIKE `sys_social`; + +CREATE TABLE `bak_sw2kra_20261103_sys_dept` LIKE `sys_dept`; + +CREATE TABLE `bak_sw2kra_20261103_sys_user` LIKE `sys_user`; + +CREATE TABLE `bak_sw2kra_20261103_sys_post` LIKE `sys_post`; + +CREATE TABLE `bak_sw2kra_20261103_sys_role` LIKE `sys_role`; + +CREATE TABLE `bak_sw2kra_20261103_sys_menu` LIKE `sys_menu`; + +CREATE TABLE `bak_sw2kra_20261103_sys_user_role` LIKE `sys_user_role`; + +CREATE TABLE `bak_sw2kra_20261103_sys_role_menu` LIKE `sys_role_menu`; + +CREATE TABLE `bak_sw2kra_20261103_sys_role_dept` LIKE `sys_role_dept`; + +CREATE TABLE `bak_sw2kra_20261103_sys_user_post` LIKE `sys_user_post`; + +CREATE TABLE `bak_sw2kra_20261103_sys_oper_log` LIKE `sys_oper_log`; + +CREATE TABLE `bak_sw2kra_20261103_sys_dict_type` LIKE `sys_dict_type`; + +CREATE TABLE `bak_sw2kra_20261103_sys_dict_data` LIKE `sys_dict_data`; + +CREATE TABLE `bak_sw2kra_20261103_sys_config` LIKE `sys_config`; + +CREATE TABLE `bak_sw2kra_20261103_sys_login_info` LIKE `sys_login_info`; + +CREATE TABLE `bak_sw2kra_20261103_sys_notice` LIKE `sys_notice`; + +CREATE TABLE `bak_sw2kra_20261103_sys_message` LIKE `sys_message`; + +CREATE TABLE `bak_sw2kra_20261103_sys_oss` LIKE `sys_oss`; + +CREATE TABLE `bak_sw2kra_20261103_sys_oss_config` LIKE `sys_oss_config`; + +CREATE TABLE `bak_sw2kra_20261103_sys_client` LIKE `sys_client`; + +CREATE TABLE `bak_sw2kra_20261103_sys_user_bind_platform` LIKE `sys_user_bind_platform`; + +CREATE TABLE `bak_sw2kra_20261103_sys_user_preference` LIKE `sys_user_preference`; + +CREATE TABLE `bak_sw2kra_20261103_sj_job` LIKE `sj_job`; + +CREATE TABLE `bak_sw2kra_20261103_sys_alert_rule` LIKE `sys_alert_rule`; + +CREATE TABLE `bak_sw2kra_20261103_sys_alert_record` LIKE `sys_alert_record`; + +CREATE TABLE `bak_sw2kra_20261103_sys_alert_operation_log` LIKE `sys_alert_operation_log`; + +START TRANSACTION; + +INSERT INTO `bak_sw2kra_20261103_sys_social` (`id`,`user_id`,`auth_id`,`source`,`open_id`,`user_name`,`nick_name`,`email`,`avatar`,`access_token`,`expire_in`,`refresh_token`,`access_code`,`union_id`,`scope`,`token_type`,`id_token`,`mac_algorithm`,`mac_key`,`code`,`oauth_token`,`oauth_token_secret`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`del_flag`) SELECT `id`,`user_id`,`auth_id`,`source`,`open_id`,`user_name`,`nick_name`,`email`,`avatar`,`access_token`,`expire_in`,`refresh_token`,`access_code`,`union_id`,`scope`,`token_type`,`id_token`,`mac_algorithm`,`mac_key`,`code`,`oauth_token`,`oauth_token_secret`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`del_flag` FROM `sys_social`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_social`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_social`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_social backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_social backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_social` s LEFT JOIN `bak_sw2kra_20261103_sys_social` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`auth_id` <=> BINARY b.`auth_id`) AND (BINARY s.`source` <=> BINARY b.`source`) AND (BINARY s.`open_id` <=> BINARY b.`open_id`) AND (BINARY s.`user_name` <=> BINARY b.`user_name`) AND (BINARY s.`nick_name` <=> BINARY b.`nick_name`) AND (BINARY s.`email` <=> BINARY b.`email`) AND (BINARY s.`avatar` <=> BINARY b.`avatar`) AND (BINARY s.`access_token` <=> BINARY b.`access_token`) AND (BINARY s.`expire_in` <=> BINARY b.`expire_in`) AND (BINARY s.`refresh_token` <=> BINARY b.`refresh_token`) AND (BINARY s.`access_code` <=> BINARY b.`access_code`) AND (BINARY s.`union_id` <=> BINARY b.`union_id`) AND (BINARY s.`scope` <=> BINARY b.`scope`) AND (BINARY s.`token_type` <=> BINARY b.`token_type`) AND (BINARY s.`id_token` <=> BINARY b.`id_token`) AND (BINARY s.`mac_algorithm` <=> BINARY b.`mac_algorithm`) AND (BINARY s.`mac_key` <=> BINARY b.`mac_key`) AND (BINARY s.`code` <=> BINARY b.`code`) AND (BINARY s.`oauth_token` <=> BINARY b.`oauth_token`) AND (BINARY s.`oauth_token_secret` <=> BINARY b.`oauth_token_secret`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_social backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_social backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_dept` (`dept_id`,`parent_id`,`ancestors`,`dept_name`,`dept_category`,`order_num`,`leader`,`phone`,`email`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`) SELECT `dept_id`,`parent_id`,`ancestors`,`dept_name`,`dept_category`,`order_num`,`leader`,`phone`,`email`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time` FROM `sys_dept`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_dept`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_dept`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dept backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dept backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_dept` s LEFT JOIN `bak_sw2kra_20261103_sys_dept` b ON s.`dept_id`=b.`dept_id` WHERE b.`dept_id` IS NULL OR NOT((BINARY s.`dept_id` <=> BINARY b.`dept_id`) AND (BINARY s.`parent_id` <=> BINARY b.`parent_id`) AND (BINARY s.`ancestors` <=> BINARY b.`ancestors`) AND (BINARY s.`dept_name` <=> BINARY b.`dept_name`) AND (BINARY s.`dept_category` <=> BINARY b.`dept_category`) AND (BINARY s.`order_num` <=> BINARY b.`order_num`) AND (BINARY s.`leader` <=> BINARY b.`leader`) AND (BINARY s.`phone` <=> BINARY b.`phone`) AND (BINARY s.`email` <=> BINARY b.`email`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dept backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dept backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_user` (`user_id`,`dept_id`,`user_name`,`nick_name`,`user_type`,`email`,`phone_number`,`gender`,`avatar`,`password`,`status`,`del_flag`,`login_ip`,`login_date`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`,`level`,`parent_id`,`id_path`,`real_name`,`qq_number`,`reject_reason`) SELECT `user_id`,`dept_id`,`user_name`,`nick_name`,`user_type`,`email`,`phone_number`,`gender`,`avatar`,`password`,`status`,`del_flag`,`login_ip`,`login_date`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`,`level`,`parent_id`,`id_path`,`real_name`,`qq_number`,`reject_reason` FROM `sys_user`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_user`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_user` s LEFT JOIN `bak_sw2kra_20261103_sys_user` b ON s.`user_id`=b.`user_id` WHERE b.`user_id` IS NULL OR NOT((BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`dept_id` <=> BINARY b.`dept_id`) AND (BINARY s.`user_name` <=> BINARY b.`user_name`) AND (BINARY s.`nick_name` <=> BINARY b.`nick_name`) AND (BINARY s.`user_type` <=> BINARY b.`user_type`) AND (BINARY s.`email` <=> BINARY b.`email`) AND (BINARY s.`phone_number` <=> BINARY b.`phone_number`) AND (BINARY s.`gender` <=> BINARY b.`gender`) AND (BINARY s.`avatar` <=> BINARY b.`avatar`) AND (BINARY s.`password` <=> BINARY b.`password`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`) AND (BINARY s.`login_ip` <=> BINARY b.`login_ip`) AND (BINARY s.`login_date` <=> BINARY b.`login_date`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`) AND (BINARY s.`level` <=> BINARY b.`level`) AND (BINARY s.`parent_id` <=> BINARY b.`parent_id`) AND (BINARY s.`id_path` <=> BINARY b.`id_path`) AND (BINARY s.`real_name` <=> BINARY b.`real_name`) AND (BINARY s.`qq_number` <=> BINARY b.`qq_number`) AND (BINARY s.`reject_reason` <=> BINARY b.`reject_reason`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_post` (`post_id`,`dept_id`,`post_code`,`post_category`,`post_name`,`post_sort`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `post_id`,`dept_id`,`post_code`,`post_category`,`post_name`,`post_sort`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_post`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_post`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_post`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_post backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_post backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_post` s LEFT JOIN `bak_sw2kra_20261103_sys_post` b ON s.`post_id`=b.`post_id` WHERE b.`post_id` IS NULL OR NOT((BINARY s.`post_id` <=> BINARY b.`post_id`) AND (BINARY s.`dept_id` <=> BINARY b.`dept_id`) AND (BINARY s.`post_code` <=> BINARY b.`post_code`) AND (BINARY s.`post_category` <=> BINARY b.`post_category`) AND (BINARY s.`post_name` <=> BINARY b.`post_name`) AND (BINARY s.`post_sort` <=> BINARY b.`post_sort`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_post backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_post backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_role` (`role_id`,`role_name`,`role_key`,`role_sort`,`data_scope`,`menu_check_strictly`,`dept_check_strictly`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `role_id`,`role_name`,`role_key`,`role_sort`,`data_scope`,`menu_check_strictly`,`dept_check_strictly`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_role`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_role`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_role`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_role` s LEFT JOIN `bak_sw2kra_20261103_sys_role` b ON s.`role_id`=b.`role_id` WHERE b.`role_id` IS NULL OR NOT((BINARY s.`role_id` <=> BINARY b.`role_id`) AND (BINARY s.`role_name` <=> BINARY b.`role_name`) AND (BINARY s.`role_key` <=> BINARY b.`role_key`) AND (BINARY s.`role_sort` <=> BINARY b.`role_sort`) AND (BINARY s.`data_scope` <=> BINARY b.`data_scope`) AND (BINARY s.`menu_check_strictly` <=> BINARY b.`menu_check_strictly`) AND (BINARY s.`dept_check_strictly` <=> BINARY b.`dept_check_strictly`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_menu` (`menu_id`,`menu_name`,`parent_id`,`order_num`,`path`,`component`,`query_param`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`active_menu`,`ext`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `menu_id`,`menu_name`,`parent_id`,`order_num`,`path`,`component`,`query_param`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`active_menu`,`ext`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_menu`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_menu`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_menu`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_menu backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_menu backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_menu` s LEFT JOIN `bak_sw2kra_20261103_sys_menu` b ON s.`menu_id`=b.`menu_id` WHERE b.`menu_id` IS NULL OR NOT((BINARY s.`menu_id` <=> BINARY b.`menu_id`) AND (BINARY s.`menu_name` <=> BINARY b.`menu_name`) AND (BINARY s.`parent_id` <=> BINARY b.`parent_id`) AND (BINARY s.`order_num` <=> BINARY b.`order_num`) AND (BINARY s.`path` <=> BINARY b.`path`) AND (BINARY s.`component` <=> BINARY b.`component`) AND (BINARY s.`query_param` <=> BINARY b.`query_param`) AND (BINARY s.`is_frame` <=> BINARY b.`is_frame`) AND (BINARY s.`is_cache` <=> BINARY b.`is_cache`) AND (BINARY s.`menu_type` <=> BINARY b.`menu_type`) AND (BINARY s.`visible` <=> BINARY b.`visible`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`perms` <=> BINARY b.`perms`) AND (BINARY s.`icon` <=> BINARY b.`icon`) AND (BINARY s.`active_menu` <=> BINARY b.`active_menu`) AND (BINARY s.`ext` <=> BINARY b.`ext`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_menu backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_menu backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_user_role` (`user_id`,`role_id`) SELECT `user_id`,`role_id` FROM `sys_user_role`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_role`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_user_role`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_role backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_role backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_user_role` s LEFT JOIN `bak_sw2kra_20261103_sys_user_role` b ON s.`user_id`=b.`user_id` AND s.`role_id`=b.`role_id` WHERE b.`user_id` IS NULL OR NOT((BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`role_id` <=> BINARY b.`role_id`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_role backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_role backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_role_menu` (`role_id`,`menu_id`) SELECT `role_id`,`menu_id` FROM `sys_role_menu`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_role_menu`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_role_menu`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role_menu backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role_menu backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_role_menu` s LEFT JOIN `bak_sw2kra_20261103_sys_role_menu` b ON s.`role_id`=b.`role_id` AND s.`menu_id`=b.`menu_id` WHERE b.`role_id` IS NULL OR NOT((BINARY s.`role_id` <=> BINARY b.`role_id`) AND (BINARY s.`menu_id` <=> BINARY b.`menu_id`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role_menu backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role_menu backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_role_dept` (`role_id`,`dept_id`) SELECT `role_id`,`dept_id` FROM `sys_role_dept`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_role_dept`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_role_dept`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role_dept backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role_dept backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_role_dept` s LEFT JOIN `bak_sw2kra_20261103_sys_role_dept` b ON s.`role_id`=b.`role_id` AND s.`dept_id`=b.`dept_id` WHERE b.`role_id` IS NULL OR NOT((BINARY s.`role_id` <=> BINARY b.`role_id`) AND (BINARY s.`dept_id` <=> BINARY b.`dept_id`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_role_dept backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_role_dept backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_user_post` (`user_id`,`post_id`) SELECT `user_id`,`post_id` FROM `sys_user_post`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_post`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_user_post`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_post backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_post backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_user_post` s LEFT JOIN `bak_sw2kra_20261103_sys_user_post` b ON s.`user_id`=b.`user_id` AND s.`post_id`=b.`post_id` WHERE b.`user_id` IS NULL OR NOT((BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`post_id` <=> BINARY b.`post_id`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_post backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_post backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_oper_log` (`oper_id`,`title`,`business_type`,`method`,`request_method`,`operator_type`,`oper_name`,`user_id`,`dept_id`,`dept_name`,`client_key`,`device_type`,`browser`,`os`,`oper_url`,`oper_ip`,`oper_location`,`oper_param`,`json_result`,`status`,`error_msg`,`oper_time`,`cost_time`) SELECT `oper_id`,`title`,`business_type`,`method`,`request_method`,`operator_type`,`oper_name`,`user_id`,`dept_id`,`dept_name`,`client_key`,`device_type`,`browser`,`os`,`oper_url`,`oper_ip`,`oper_location`,`oper_param`,`json_result`,`status`,`error_msg`,`oper_time`,`cost_time` FROM `sys_oper_log`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_oper_log`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_oper_log`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oper_log backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oper_log backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_oper_log` s LEFT JOIN `bak_sw2kra_20261103_sys_oper_log` b ON s.`oper_id`=b.`oper_id` WHERE b.`oper_id` IS NULL OR NOT((BINARY s.`oper_id` <=> BINARY b.`oper_id`) AND (BINARY s.`title` <=> BINARY b.`title`) AND (BINARY s.`business_type` <=> BINARY b.`business_type`) AND (BINARY s.`method` <=> BINARY b.`method`) AND (BINARY s.`request_method` <=> BINARY b.`request_method`) AND (BINARY s.`operator_type` <=> BINARY b.`operator_type`) AND (BINARY s.`oper_name` <=> BINARY b.`oper_name`) AND (BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`dept_id` <=> BINARY b.`dept_id`) AND (BINARY s.`dept_name` <=> BINARY b.`dept_name`) AND (BINARY s.`client_key` <=> BINARY b.`client_key`) AND (BINARY s.`device_type` <=> BINARY b.`device_type`) AND (BINARY s.`browser` <=> BINARY b.`browser`) AND (BINARY s.`os` <=> BINARY b.`os`) AND (BINARY s.`oper_url` <=> BINARY b.`oper_url`) AND (BINARY s.`oper_ip` <=> BINARY b.`oper_ip`) AND (BINARY s.`oper_location` <=> BINARY b.`oper_location`) AND (BINARY s.`oper_param` <=> BINARY b.`oper_param`) AND (BINARY s.`json_result` <=> BINARY b.`json_result`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`error_msg` <=> BINARY b.`error_msg`) AND (BINARY s.`oper_time` <=> BINARY b.`oper_time`) AND (BINARY s.`cost_time` <=> BINARY b.`cost_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oper_log backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oper_log backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_dict_type` (`dict_id`,`dict_name`,`dict_type`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `dict_id`,`dict_name`,`dict_type`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_dict_type`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_dict_type`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_dict_type`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dict_type backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dict_type backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_dict_type` s LEFT JOIN `bak_sw2kra_20261103_sys_dict_type` b ON s.`dict_id`=b.`dict_id` WHERE b.`dict_id` IS NULL OR NOT((BINARY s.`dict_id` <=> BINARY b.`dict_id`) AND (BINARY s.`dict_name` <=> BINARY b.`dict_name`) AND (BINARY s.`dict_type` <=> BINARY b.`dict_type`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dict_type backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dict_type backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_dict_data` (`dict_code`,`dict_sort`,`dict_label`,`dict_value`,`dict_type`,`css_class`,`list_class`,`is_default`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `dict_code`,`dict_sort`,`dict_label`,`dict_value`,`dict_type`,`css_class`,`list_class`,`is_default`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_dict_data`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_dict_data`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_dict_data`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dict_data backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dict_data backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_dict_data` s LEFT JOIN `bak_sw2kra_20261103_sys_dict_data` b ON s.`dict_code`=b.`dict_code` WHERE b.`dict_code` IS NULL OR NOT((BINARY s.`dict_code` <=> BINARY b.`dict_code`) AND (BINARY s.`dict_sort` <=> BINARY b.`dict_sort`) AND (BINARY s.`dict_label` <=> BINARY b.`dict_label`) AND (BINARY s.`dict_value` <=> BINARY b.`dict_value`) AND (BINARY s.`dict_type` <=> BINARY b.`dict_type`) AND (BINARY s.`css_class` <=> BINARY b.`css_class`) AND (BINARY s.`list_class` <=> BINARY b.`list_class`) AND (BINARY s.`is_default` <=> BINARY b.`is_default`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_dict_data backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_dict_data backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_config` (`config_id`,`config_name`,`config_key`,`config_value`,`config_type`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `config_id`,`config_name`,`config_key`,`config_value`,`config_type`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_config`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_config`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_config`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_config backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_config backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_config` s LEFT JOIN `bak_sw2kra_20261103_sys_config` b ON s.`config_id`=b.`config_id` WHERE b.`config_id` IS NULL OR NOT((BINARY s.`config_id` <=> BINARY b.`config_id`) AND (BINARY s.`config_name` <=> BINARY b.`config_name`) AND (BINARY s.`config_key` <=> BINARY b.`config_key`) AND (BINARY s.`config_value` <=> BINARY b.`config_value`) AND (BINARY s.`config_type` <=> BINARY b.`config_type`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_config backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_config backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_login_info` (`info_id`,`user_name`,`client_key`,`device_type`,`ipaddr`,`login_location`,`browser`,`os`,`status`,`msg`,`login_time`) SELECT `info_id`,`user_name`,`client_key`,`device_type`,`ipaddr`,`login_location`,`browser`,`os`,`status`,`msg`,`login_time` FROM `sys_login_info`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_login_info`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_login_info`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_login_info backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_login_info backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_login_info` s LEFT JOIN `bak_sw2kra_20261103_sys_login_info` b ON s.`info_id`=b.`info_id` WHERE b.`info_id` IS NULL OR NOT((BINARY s.`info_id` <=> BINARY b.`info_id`) AND (BINARY s.`user_name` <=> BINARY b.`user_name`) AND (BINARY s.`client_key` <=> BINARY b.`client_key`) AND (BINARY s.`device_type` <=> BINARY b.`device_type`) AND (BINARY s.`ipaddr` <=> BINARY b.`ipaddr`) AND (BINARY s.`login_location` <=> BINARY b.`login_location`) AND (BINARY s.`browser` <=> BINARY b.`browser`) AND (BINARY s.`os` <=> BINARY b.`os`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`msg` <=> BINARY b.`msg`) AND (BINARY s.`login_time` <=> BINARY b.`login_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_login_info backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_login_info backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_notice` (`notice_id`,`notice_title`,`notice_type`,`notice_content`,`status`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `notice_id`,`notice_title`,`notice_type`,`notice_content`,`status`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_notice`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_notice`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_notice`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_notice backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_notice backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_notice` s LEFT JOIN `bak_sw2kra_20261103_sys_notice` b ON s.`notice_id`=b.`notice_id` WHERE b.`notice_id` IS NULL OR NOT((BINARY s.`notice_id` <=> BINARY b.`notice_id`) AND (BINARY s.`notice_title` <=> BINARY b.`notice_title`) AND (BINARY s.`notice_type` <=> BINARY b.`notice_type`) AND (BINARY s.`notice_content` <=> BINARY b.`notice_content`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_notice backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_notice backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_message` (`message_id`,`category`,`type`,`source`,`title`,`message`,`content`,`data_json`,`path`,`send_user_ids`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`) SELECT `message_id`,`category`,`type`,`source`,`title`,`message`,`content`,`data_json`,`path`,`send_user_ids`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time` FROM `sys_message`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_message`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_message`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_message backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_message backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_message` s LEFT JOIN `bak_sw2kra_20261103_sys_message` b ON s.`message_id`=b.`message_id` WHERE b.`message_id` IS NULL OR NOT((BINARY s.`message_id` <=> BINARY b.`message_id`) AND (BINARY s.`category` <=> BINARY b.`category`) AND (BINARY s.`type` <=> BINARY b.`type`) AND (BINARY s.`source` <=> BINARY b.`source`) AND (BINARY s.`title` <=> BINARY b.`title`) AND (BINARY s.`message` <=> BINARY b.`message`) AND (BINARY s.`content` <=> BINARY b.`content`) AND (BINARY s.`data_json` <=> BINARY b.`data_json`) AND (BINARY s.`path` <=> BINARY b.`path`) AND (BINARY s.`send_user_ids` <=> BINARY b.`send_user_ids`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_message backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_message backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_oss` (`oss_id`,`file_name`,`original_name`,`file_suffix`,`url`,`ext1`,`create_dept`,`create_time`,`create_by`,`update_time`,`update_by`,`service`) SELECT `oss_id`,`file_name`,`original_name`,`file_suffix`,`url`,`ext1`,`create_dept`,`create_time`,`create_by`,`update_time`,`update_by`,`service` FROM `sys_oss`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_oss`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_oss`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oss backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oss backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_oss` s LEFT JOIN `bak_sw2kra_20261103_sys_oss` b ON s.`oss_id`=b.`oss_id` WHERE b.`oss_id` IS NULL OR NOT((BINARY s.`oss_id` <=> BINARY b.`oss_id`) AND (BINARY s.`file_name` <=> BINARY b.`file_name`) AND (BINARY s.`original_name` <=> BINARY b.`original_name`) AND (BINARY s.`file_suffix` <=> BINARY b.`file_suffix`) AND (BINARY s.`url` <=> BINARY b.`url`) AND (BINARY s.`ext1` <=> BINARY b.`ext1`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`service` <=> BINARY b.`service`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oss backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oss backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_oss_config` (`oss_config_id`,`config_key`,`access_key`,`secret_key`,`bucket_name`,`prefix`,`endpoint`,`domain_url`,`is_https`,`region`,`access_policy`,`status`,`ext1`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) SELECT `oss_config_id`,`config_key`,`access_key`,`secret_key`,`bucket_name`,`prefix`,`endpoint`,`domain_url`,`is_https`,`region`,`access_policy`,`status`,`ext1`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`,`remark` FROM `sys_oss_config`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_oss_config`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_oss_config`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oss_config backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oss_config backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_oss_config` s LEFT JOIN `bak_sw2kra_20261103_sys_oss_config` b ON s.`oss_config_id`=b.`oss_config_id` WHERE b.`oss_config_id` IS NULL OR NOT((BINARY s.`oss_config_id` <=> BINARY b.`oss_config_id`) AND (BINARY s.`config_key` <=> BINARY b.`config_key`) AND (BINARY s.`access_key` <=> BINARY b.`access_key`) AND (BINARY s.`secret_key` <=> BINARY b.`secret_key`) AND (BINARY s.`bucket_name` <=> BINARY b.`bucket_name`) AND (BINARY s.`prefix` <=> BINARY b.`prefix`) AND (BINARY s.`endpoint` <=> BINARY b.`endpoint`) AND (BINARY s.`domain_url` <=> BINARY b.`domain_url`) AND (BINARY s.`is_https` <=> BINARY b.`is_https`) AND (BINARY s.`region` <=> BINARY b.`region`) AND (BINARY s.`access_policy` <=> BINARY b.`access_policy`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`ext1` <=> BINARY b.`ext1`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`) AND (BINARY s.`remark` <=> BINARY b.`remark`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_oss_config backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_oss_config backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_client` (`id`,`client_id`,`client_key`,`client_secret`,`grant_type`,`device_type`,`access_path`,`ip_whitelist`,`active_timeout`,`timeout`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time`) SELECT `id`,`client_id`,`client_key`,`client_secret`,`grant_type`,`device_type`,`access_path`,`ip_whitelist`,`active_timeout`,`timeout`,`status`,`del_flag`,`create_dept`,`create_by`,`create_time`,`update_by`,`update_time` FROM `sys_client`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_client`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_client`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_client backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_client backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_client` s LEFT JOIN `bak_sw2kra_20261103_sys_client` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`client_id` <=> BINARY b.`client_id`) AND (BINARY s.`client_key` <=> BINARY b.`client_key`) AND (BINARY s.`client_secret` <=> BINARY b.`client_secret`) AND (BINARY s.`grant_type` <=> BINARY b.`grant_type`) AND (BINARY s.`device_type` <=> BINARY b.`device_type`) AND (BINARY s.`access_path` <=> BINARY b.`access_path`) AND (BINARY s.`ip_whitelist` <=> BINARY b.`ip_whitelist`) AND (BINARY s.`active_timeout` <=> BINARY b.`active_timeout`) AND (BINARY s.`timeout` <=> BINARY b.`timeout`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`del_flag` <=> BINARY b.`del_flag`) AND (BINARY s.`create_dept` <=> BINARY b.`create_dept`) AND (BINARY s.`create_by` <=> BINARY b.`create_by`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`update_by` <=> BINARY b.`update_by`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_client backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_client backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_user_bind_platform` (`user_id`,`source_db`,`platform_busier_id`,`platform_busier_account`,`status`,`reject_reason`,`create_time`,`review_time`,`review_account`) SELECT `user_id`,`source_db`,`platform_busier_id`,`platform_busier_account`,`status`,`reject_reason`,`create_time`,`review_time`,`review_account` FROM `sys_user_bind_platform`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_bind_platform`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_user_bind_platform`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_bind_platform backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_bind_platform backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_user_bind_platform` s LEFT JOIN `bak_sw2kra_20261103_sys_user_bind_platform` b ON s.`user_id`=b.`user_id` AND s.`source_db`=b.`source_db` AND s.`platform_busier_id`=b.`platform_busier_id` WHERE b.`user_id` IS NULL OR NOT((BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`source_db` <=> BINARY b.`source_db`) AND (BINARY s.`platform_busier_id` <=> BINARY b.`platform_busier_id`) AND (BINARY s.`platform_busier_account` <=> BINARY b.`platform_busier_account`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`reject_reason` <=> BINARY b.`reject_reason`) AND (BINARY s.`create_time` <=> BINARY b.`create_time`) AND (BINARY s.`review_time` <=> BINARY b.`review_time`) AND (BINARY s.`review_account` <=> BINARY b.`review_account`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_bind_platform backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_bind_platform backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_user_preference` (`user_id`,`include_sub_promote`,`idcard_dedup`,`update_time`) SELECT `user_id`,`include_sub_promote`,`idcard_dedup`,`update_time` FROM `sys_user_preference`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_preference`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_user_preference`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_preference backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_preference backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_user_preference` s LEFT JOIN `bak_sw2kra_20261103_sys_user_preference` b ON s.`user_id`=b.`user_id` WHERE b.`user_id` IS NULL OR NOT((BINARY s.`user_id` <=> BINARY b.`user_id`) AND (BINARY s.`include_sub_promote` <=> BINARY b.`include_sub_promote`) AND (BINARY s.`idcard_dedup` <=> BINARY b.`idcard_dedup`) AND (BINARY s.`update_time` <=> BINARY b.`update_time`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_user_preference backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_user_preference backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sj_job` (`id`,`namespace_id`,`biz_id`,`group_name`,`job_name`,`args_str`,`args_type`,`next_trigger_at`,`job_status`,`task_type`,`route_key`,`executor_type`,`executor_info`,`trigger_type`,`trigger_interval`,`block_strategy`,`executor_timeout`,`max_retry_times`,`parallel_num`,`retry_interval`,`bucket_index`,`resident`,`notify_ids`,`owner_id`,`labels`,`description`,`ext_attrs`,`deleted`,`create_dt`,`update_dt`) SELECT `id`,`namespace_id`,`biz_id`,`group_name`,`job_name`,`args_str`,`args_type`,`next_trigger_at`,`job_status`,`task_type`,`route_key`,`executor_type`,`executor_info`,`trigger_type`,`trigger_interval`,`block_strategy`,`executor_timeout`,`max_retry_times`,`parallel_num`,`retry_interval`,`bucket_index`,`resident`,`notify_ids`,`owner_id`,`labels`,`description`,`ext_attrs`,`deleted`,`create_dt`,`update_dt` FROM `sj_job`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sj_job`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sj_job`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sj_job backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sj_job backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sj_job` s LEFT JOIN `bak_sw2kra_20261103_sj_job` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`namespace_id` <=> BINARY b.`namespace_id`) AND (BINARY s.`biz_id` <=> BINARY b.`biz_id`) AND (BINARY s.`group_name` <=> BINARY b.`group_name`) AND (BINARY s.`job_name` <=> BINARY b.`job_name`) AND (BINARY s.`args_str` <=> BINARY b.`args_str`) AND (BINARY s.`args_type` <=> BINARY b.`args_type`) AND (BINARY s.`next_trigger_at` <=> BINARY b.`next_trigger_at`) AND (BINARY s.`job_status` <=> BINARY b.`job_status`) AND (BINARY s.`task_type` <=> BINARY b.`task_type`) AND (BINARY s.`route_key` <=> BINARY b.`route_key`) AND (BINARY s.`executor_type` <=> BINARY b.`executor_type`) AND (BINARY s.`executor_info` <=> BINARY b.`executor_info`) AND (BINARY s.`trigger_type` <=> BINARY b.`trigger_type`) AND (BINARY s.`trigger_interval` <=> BINARY b.`trigger_interval`) AND (BINARY s.`block_strategy` <=> BINARY b.`block_strategy`) AND (BINARY s.`executor_timeout` <=> BINARY b.`executor_timeout`) AND (BINARY s.`max_retry_times` <=> BINARY b.`max_retry_times`) AND (BINARY s.`parallel_num` <=> BINARY b.`parallel_num`) AND (BINARY s.`retry_interval` <=> BINARY b.`retry_interval`) AND (BINARY s.`bucket_index` <=> BINARY b.`bucket_index`) AND (BINARY s.`resident` <=> BINARY b.`resident`) AND (BINARY s.`notify_ids` <=> BINARY b.`notify_ids`) AND (BINARY s.`owner_id` <=> BINARY b.`owner_id`) AND (BINARY s.`labels` <=> BINARY b.`labels`) AND (BINARY s.`description` <=> BINARY b.`description`) AND (BINARY s.`ext_attrs` <=> BINARY b.`ext_attrs`) AND (BINARY s.`deleted` <=> BINARY b.`deleted`) AND (BINARY s.`create_dt` <=> BINARY b.`create_dt`) AND (BINARY s.`update_dt` <=> BINARY b.`update_dt`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sj_job backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sj_job backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_alert_rule` (`id`,`rule_name`,`rule_type`,`monitor_metric`,`stat_period_days`,`mom_direction`,`mom_threshold`,`unlogin_days`,`monitor_scope`,`warning_color`,`creator_id`,`enabled`,`is_deleted`,`created_at`,`updated_at`) SELECT `id`,`rule_name`,`rule_type`,`monitor_metric`,`stat_period_days`,`mom_direction`,`mom_threshold`,`unlogin_days`,`monitor_scope`,`warning_color`,`creator_id`,`enabled`,`is_deleted`,`created_at`,`updated_at` FROM `sys_alert_rule`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_alert_rule`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_alert_rule`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_rule backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_rule backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_alert_rule` s LEFT JOIN `bak_sw2kra_20261103_sys_alert_rule` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`rule_name` <=> BINARY b.`rule_name`) AND (BINARY s.`rule_type` <=> BINARY b.`rule_type`) AND (BINARY s.`monitor_metric` <=> BINARY b.`monitor_metric`) AND (BINARY s.`stat_period_days` <=> BINARY b.`stat_period_days`) AND (BINARY s.`mom_direction` <=> BINARY b.`mom_direction`) AND (BINARY s.`mom_threshold` <=> BINARY b.`mom_threshold`) AND (BINARY s.`unlogin_days` <=> BINARY b.`unlogin_days`) AND (BINARY s.`monitor_scope` <=> BINARY b.`monitor_scope`) AND (BINARY s.`warning_color` <=> BINARY b.`warning_color`) AND (BINARY s.`creator_id` <=> BINARY b.`creator_id`) AND (BINARY s.`enabled` <=> BINARY b.`enabled`) AND (BINARY s.`is_deleted` <=> BINARY b.`is_deleted`) AND (BINARY s.`created_at` <=> BINARY b.`created_at`) AND (BINARY s.`updated_at` <=> BINARY b.`updated_at`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_rule backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_rule backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_alert_record` (`id`,`platform_code`,`platform_name`,`rule_id`,`rule_name`,`rule_type`,`monitor_metric`,`target_promote_id`,`target_promote_name`,`business_id`,`business_name`,`alert_content`,`status`,`alert_date`,`created_at`,`updated_at`) SELECT `id`,`platform_code`,`platform_name`,`rule_id`,`rule_name`,`rule_type`,`monitor_metric`,`target_promote_id`,`target_promote_name`,`business_id`,`business_name`,`alert_content`,`status`,`alert_date`,`created_at`,`updated_at` FROM `sys_alert_record`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_alert_record`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_alert_record`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_record backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_record backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_alert_record` s LEFT JOIN `bak_sw2kra_20261103_sys_alert_record` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`platform_code` <=> BINARY b.`platform_code`) AND (BINARY s.`platform_name` <=> BINARY b.`platform_name`) AND (BINARY s.`rule_id` <=> BINARY b.`rule_id`) AND (BINARY s.`rule_name` <=> BINARY b.`rule_name`) AND (BINARY s.`rule_type` <=> BINARY b.`rule_type`) AND (BINARY s.`monitor_metric` <=> BINARY b.`monitor_metric`) AND (BINARY s.`target_promote_id` <=> BINARY b.`target_promote_id`) AND (BINARY s.`target_promote_name` <=> BINARY b.`target_promote_name`) AND (BINARY s.`business_id` <=> BINARY b.`business_id`) AND (BINARY s.`business_name` <=> BINARY b.`business_name`) AND (BINARY s.`alert_content` <=> BINARY b.`alert_content`) AND (BINARY s.`status` <=> BINARY b.`status`) AND (BINARY s.`alert_date` <=> BINARY b.`alert_date`) AND (BINARY s.`created_at` <=> BINARY b.`created_at`) AND (BINARY s.`updated_at` <=> BINARY b.`updated_at`) AND (BINARY s.`unclosed_flag` <=> BINARY b.`unclosed_flag`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_record backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_record backup bytes'; END IF; + +INSERT INTO `bak_sw2kra_20261103_sys_alert_operation_log` (`id`,`alert_id`,`operator_id`,`operator_name`,`log_type`,`action_detail`,`note_content`,`attachment_ids`,`created_at`) SELECT `id`,`alert_id`,`operator_id`,`operator_name`,`log_type`,`action_detail`,`note_content`,`attachment_ids`,`created_at` FROM `sys_alert_operation_log`; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_alert_operation_log`) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `sys_alert_operation_log`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_operation_log backup rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_operation_log backup rows'; END IF; + +SELECT (SELECT COUNT(*) FROM `sys_alert_operation_log` s LEFT JOIN `bak_sw2kra_20261103_sys_alert_operation_log` b ON s.`id`=b.`id` WHERE b.`id` IS NULL OR NOT((BINARY s.`id` <=> BINARY b.`id`) AND (BINARY s.`alert_id` <=> BINARY b.`alert_id`) AND (BINARY s.`operator_id` <=> BINARY b.`operator_id`) AND (BINARY s.`operator_name` <=> BINARY b.`operator_name`) AND (BINARY s.`log_type` <=> BINARY b.`log_type`) AND (BINARY s.`action_detail` <=> BINARY b.`action_detail`) AND (BINARY s.`note_content` <=> BINARY b.`note_content`) AND (BINARY s.`attachment_ids` <=> BINARY b.`attachment_ids`) AND (BINARY s.`created_at` <=> BINARY b.`created_at`))) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('backup','sys_alert_operation_log backup bytes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='sys_alert_operation_log backup bytes'; END IF; + +COMMIT; + +-- 四、按 Kra 当前定义创建目标表,并增加商务账号和岗位扩展字段。 + +CREATE TABLE `sys_schema_migrations` (`id` varchar(255),PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_users` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`uuid` char(36),`username` varchar(191),`password` longtext,`nick_name` longtext,`header_img` longtext,`authority_id` bigint unsigned,`dept_id` bigint unsigned,`phone` longtext,`email` longtext,`enable` bigint DEFAULT 1,`origin_setting` text,`password_updated_at` datetime(3) NULL,`must_change_password` boolean,PRIMARY KEY (`id`),INDEX `idx_sys_users_deleted_at` (`deleted_at`),INDEX `idx_sys_users_uuid` (`uuid`),INDEX `idx_sys_users_username` (`username`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_authorities` (`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`authority_id` bigint unsigned AUTO_INCREMENT,`authority_name` longtext,`parent_id` bigint unsigned,`data_scope` bigint DEFAULT 1,`default_router` varchar(191) DEFAULT 'dashboard',PRIMARY KEY (`authority_id`),INDEX `idx_sys_authorities_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_base_menus` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`menu_level` bigint unsigned,`parent_id` bigint unsigned,`path` longtext,`name` longtext,`hidden` boolean,`component` longtext,`sort` bigint,`active_name` longtext,`keep_alive` boolean,`default_menu` boolean,`title` longtext,`icon` longtext,`close_tab` boolean,`transition_type` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_base_menus_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_user_authority` (`sys_user_id` bigint unsigned,`sys_authority_authority_id` bigint unsigned) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_authority_menus` (`sys_authority_authority_id` bigint unsigned,`sys_base_menu_id` bigint unsigned) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_base_menu_parameters` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`sys_base_menu_id` bigint unsigned,`type` longtext,`key` longtext,`value` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_base_menu_parameters_deleted_at` (`deleted_at`),INDEX `idx_sys_base_menu_parameters_menu_id` (`sys_base_menu_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_apis` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`path` longtext,`description` longtext,`api_group` longtext,`method` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_apis_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_ignore_apis` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`path` longtext,`method` varchar(191) DEFAULT 'POST',PRIMARY KEY (`id`),INDEX `idx_sys_ignore_apis_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_authority_apis` (`authority_id` bigint unsigned,`api_id` bigint unsigned,PRIMARY KEY (`authority_id`,`api_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `casbin_rule` (`id` bigint unsigned AUTO_INCREMENT,`ptype` varchar(100),`v0` varchar(100),`v1` varchar(100),`v2` varchar(100),`v3` varchar(100),`v4` varchar(100),`v5` varchar(100),PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_base_menu_btns` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`desc` longtext,`sys_base_menu_id` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_sys_base_menu_btns_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_authority_btns` (`authority_id` bigint unsigned,`sys_menu_id` bigint unsigned,`sys_base_menu_btn_id` bigint unsigned) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_departments` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` varchar(191),`parent_id` bigint unsigned,`ancestors` longtext,`sort` bigint,`leader_id` bigint unsigned,`status` boolean DEFAULT true,PRIMARY KEY (`id`),INDEX `idx_sys_departments_deleted_at` (`deleted_at`),INDEX `idx_sys_departments_name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_positions` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` varchar(191),`code` longtext,`sort` bigint,`status` boolean DEFAULT true,`remark` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_positions_deleted_at` (`deleted_at`),INDEX `idx_sys_positions_name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_user_departments` (`sys_user_id` bigint unsigned,`sys_department_id` bigint unsigned) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_user_positions` (`sys_user_id` bigint unsigned,`sys_position_id` bigint unsigned) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_authority_departments` (`sys_authority_authority_id` bigint unsigned,`sys_department_id` bigint unsigned,INDEX `idx_sys_authority_departments_authority_id` (`sys_authority_authority_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_dictionaries` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`type` longtext,`status` boolean,`desc` longtext,`parent_id` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_sys_dictionaries_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_dictionary_details` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`label` longtext,`value` longtext,`extend` longtext,`status` boolean,`sort` bigint,`sys_dictionary_id` bigint unsigned,`parent_id` bigint unsigned,`level` bigint,`path` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_dictionary_details_deleted_at` (`deleted_at`),INDEX `idx_sys_dictionary_details_dictionary_id` (`sys_dictionary_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_params` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`key` longtext,`value` longtext,`desc` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_params_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_api_tokens` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`user_id` bigint unsigned,`authority_id` bigint unsigned,`token` text,`status` boolean,`expires_at` datetime(3) NULL,`remark` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_api_tokens_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `jwt_blacklists` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`jwt` text,PRIMARY KEY (`id`),INDEX `idx_jwt_blacklists_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_security_config` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`captcha_open` bigint DEFAULT 0,`captcha_timeout` bigint DEFAULT 3600,`key_long` bigint DEFAULT 6,`img_width` bigint DEFAULT 240,`img_height` bigint DEFAULT 80,`pwd_min_length` bigint DEFAULT 8,`pwd_require_upper` boolean DEFAULT false,`pwd_require_lower` boolean DEFAULT false,`pwd_require_digit` boolean DEFAULT false,`pwd_require_special` boolean DEFAULT false,`limit_enable` boolean DEFAULT false,`limit_window` bigint DEFAULT 60,`limit_count` bigint DEFAULT 30,`lock_enable` boolean DEFAULT false,`lock_threshold` bigint DEFAULT 5,`lock_duration` bigint DEFAULT 30,`pwd_expire_enable` boolean DEFAULT false,`pwd_expire_days` bigint DEFAULT 90,`force_new_user_change_password` boolean DEFAULT false,PRIMARY KEY (`id`),INDEX `idx_sys_security_config_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_versions` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`version_name` longtext,`version_code` longtext,`description` longtext,`version_data` text,PRIMARY KEY (`id`),INDEX `idx_sys_versions_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_export_templates` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`db_name` longtext,`name` longtext,`table_name` longtext,`template_id` longtext,`template_info` text,`sql` text,`import_sql` text,`limit` bigint,`order` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_export_templates_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_export_template_condition` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`template_id` varchar(191),`from` longtext,`column` longtext,`operator` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_export_template_condition_deleted_at` (`deleted_at`),INDEX `idx_sys_export_template_condition_template_id` (`template_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_export_template_join` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`template_id` varchar(191),`joins` longtext,`table` longtext,`on` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_export_template_join_deleted_at` (`deleted_at`),INDEX `idx_sys_export_template_join_template_id` (`template_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_operation_records` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`ip` longtext,`method` longtext,`path` longtext,`status` bigint,`latency_ms` bigint,`agent` text,`error_message` longtext,`body` text,`resp` text,`user_id` bigint unsigned,`request_id` varchar(191),`trace_id` varchar(191),`device_id` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_operation_records_deleted_at` (`deleted_at`),INDEX `idx_sys_operation_records_request_id` (`request_id`),INDEX `idx_sys_operation_records_trace_id` (`trace_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_login_logs` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`username` longtext,`ip` longtext,`status` boolean,`error_message` longtext,`agent` longtext,`user_id` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_sys_login_logs_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_data_access_logs` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`event_type` longtext,`target_table` longtext,`operation` longtext,`user_id` bigint unsigned,`authority_id` bigint unsigned,`scope` bigint,`request_id` longtext,`method` longtext,`path` longtext,`detail` longtext,PRIMARY KEY (`id`),INDEX `idx_sys_data_access_logs_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_error` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`form` text,`info` text,`level` longtext,`request_id` varchar(191),`trace_id` varchar(191),`solution` text,`status` varchar(191) DEFAULT '未处理',PRIMARY KEY (`id`),INDEX `idx_sys_error_deleted_at` (`deleted_at`),INDEX `idx_sys_error_request_id` (`request_id`),INDEX `idx_sys_error_trace_id` (`trace_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `media_file_upload_and_downloads` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`class_id` bigint,`url` longtext,`tag` longtext,`key` varchar(191),`size` bigint,`mime` longtext,`md5` varchar(191),`user_id` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_media_file_upload_and_downloads_deleted_at` (`deleted_at`),INDEX `idx_media_file_upload_and_downloads_key` (`key`),INDEX `idx_media_file_upload_and_downloads_md5` (`md5`),INDEX `idx_media_file_upload_and_downloads_user_id` (`user_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `media_attachment_category` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`pid` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_media_attachment_category_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `media_uploads` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`user_id` bigint unsigned,`file_name` longtext,`file_hash` varchar(191),`file_size` bigint,`chunk_size` bigint,`chunk_total` bigint,`status` longtext,`storage_key` longtext,`media_id` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_media_uploads_deleted_at` (`deleted_at`),INDEX `idx_media_uploads_user_id` (`user_id`),INDEX `idx_media_uploads_file_hash` (`file_hash`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `media_upload_chunks` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`upload_id` bigint unsigned,`chunk_index` bigint,`chunk_hash` longtext,`size` bigint,PRIMARY KEY (`id`),INDEX `idx_media_upload_chunks_deleted_at` (`deleted_at`),UNIQUE INDEX `idx_upload_chunk` (`upload_id`,`chunk_index`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_announcements` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`title` longtext,`content` text,`user_id` bigint,`attachments` JSON,PRIMARY KEY (`id`),INDEX `idx_sys_announcements_deleted_at` (`deleted_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_integration_configs` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`kind` varchar(32) NOT NULL,`provider` varchar(64) NOT NULL,`enabled` boolean,`config` text NOT NULL,PRIMARY KEY (`id`),UNIQUE INDEX `idx_integration_kind_provider` (`kind`,`provider`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_timed_tasks` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` varchar(191),`description` longtext,`spec` longtext,`with_seconds` boolean,`executor_type` longtext,`method_name` longtext,`params` JSON,`http_url` longtext,`http_method` longtext,`http_header` JSON,`http_body` text,`http_allow_private` boolean,`enabled` boolean,PRIMARY KEY (`id`),INDEX `idx_sys_timed_tasks_deleted_at` (`deleted_at`),INDEX `idx_sys_timed_tasks_name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `sys_timed_task_logs` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`task_id` bigint unsigned,`task_name` longtext,`trigger_type` longtext,`started_at` datetime(3) NULL,`finished_at` datetime(3) NULL,`duration_ms` bigint,`status` longtext,`error_msg` text,`output` text,PRIMARY KEY (`id`),INDEX `idx_sys_timed_task_logs_deleted_at` (`deleted_at`),INDEX `idx_sys_timed_task_logs_task_id` (`task_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `pay_orders` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`trade_no` varchar(128) NOT NULL,`provider` varchar(64) NOT NULL,`provider_trade_no` varchar(128),`query_id` varchar(128),`business_type` varchar(64) NOT NULL,`business_id` varchar(128) NOT NULL,`subject` varchar(256) NOT NULL,`payment_mode` varchar(32) NOT NULL DEFAULT 'external',`original_amount` bigint NOT NULL DEFAULT 0,`amount` bigint NOT NULL,`paid_amount` bigint NOT NULL DEFAULT 0,`payer_paid_amount` bigint NOT NULL DEFAULT 0,`cash_paid_amount` bigint NOT NULL DEFAULT 0,`point_paid_amount` bigint NOT NULL DEFAULT 0,`discount_amount` bigint NOT NULL DEFAULT 0,`provider_discount_amount` bigint NOT NULL DEFAULT 0,`merchant_discount_amount` bigint NOT NULL DEFAULT 0,`settlement_amount` bigint NOT NULL DEFAULT 0,`currency` varchar(16) NOT NULL,`payer_currency` varchar(16),`amount_breakdown_known` boolean NOT NULL DEFAULT false,`payment_status` varchar(32) NOT NULL,`provider_status` varchar(64),`fulfillment_status` varchar(32) NOT NULL,`refund_status` varchar(32) NOT NULL,`refunded_amount` bigint NOT NULL DEFAULT 0,`refund_requested_amount` bigint NOT NULL DEFAULT 0,`refund_no` varchar(128),`confirmation_id` varchar(36) NOT NULL,`request_fingerprint` varchar(64) NOT NULL,`create_payload` text,`extra` text,`last_event_id` varchar(128),`last_payload_hash` varchar(64),`last_error` varchar(512),`fulfillment_token` varchar(36),`refund_token` varchar(36),`version` bigint unsigned NOT NULL DEFAULT 1,`paid_at` datetime(3) NULL,`fulfilled_at` datetime(3) NULL,`refunded_at` datetime(3) NULL,`fulfillment_lease_until` datetime(3) NULL,`refund_lease_until` datetime(3) NULL,PRIMARY KEY (`id`),UNIQUE INDEX `idx_pay_orders_provider_trade` (`trade_no`,`provider`),UNIQUE INDEX `idx_pay_orders_provider_provider_trade` (`provider`,`provider_trade_no`),INDEX `idx_pay_orders_provider` (`provider`),INDEX `idx_pay_orders_business` (`business_type`,`business_id`),INDEX `idx_pay_orders_payment_mode` (`payment_mode`),INDEX `idx_pay_orders_payment_status` (`payment_status`),INDEX `idx_pay_orders_fulfillment_status` (`fulfillment_status`),INDEX `idx_pay_orders_refund_status` (`refund_status`),INDEX `idx_pay_orders_refund_no` (`refund_no`),UNIQUE INDEX `idx_pay_orders_confirmation_id` (`confirmation_id`),INDEX `idx_pay_orders_request_fingerprint` (`request_fingerprint`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE `pay_events` (`id` bigint unsigned AUTO_INCREMENT,`provider` varchar(64) NOT NULL,`trade_no` varchar(128) NOT NULL,`type` varchar(64) NOT NULL,`source` varchar(64) NOT NULL,`status` varchar(64),`provider_status` varchar(64),`message` varchar(512),`event_id` varchar(128),`payload_hash` varchar(64),`amount` bigint NOT NULL DEFAULT 0,`currency` varchar(16),`operator_id` bigint unsigned,`operator_name` varchar(128),`client_ip` varchar(64),`user_agent` varchar(512),`device_id` varchar(128),`created_at` datetime(3) NULL,PRIMARY KEY (`id`),INDEX `idx_pay_events_order` (`provider`,`trade_no`),INDEX `idx_pay_events_type` (`type`),INDEX `idx_pay_events_source` (`source`),INDEX `idx_pay_events_event_id` (`event_id`),INDEX `idx_pay_events_operator_id` (`operator_id`),INDEX `idx_pay_events_created_at` (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC; + +ALTER TABLE sys_users ADD COLUMN level TINYINT UNSIGNED NOT NULL DEFAULT 0,ADD COLUMN parent_id BIGINT UNSIGNED NOT NULL DEFAULT 0,ADD COLUMN id_path VARCHAR(255) NOT NULL DEFAULT '0',ADD COLUMN real_name VARCHAR(30) NOT NULL DEFAULT '',ADD COLUMN qq_number VARCHAR(20) NOT NULL DEFAULT '',ADD COLUMN business_status TINYINT UNSIGNED NOT NULL DEFAULT 0,ADD COLUMN reject_reason VARCHAR(200) NOT NULL DEFAULT '',ADD INDEX idx_sys_users_business_parent(parent_id); + +ALTER TABLE sys_positions ADD COLUMN dept_id BIGINT UNSIGNED NULL,ADD INDEX idx_sys_positions_department(dept_id); + +CREATE TABLE sw_kra_id_map (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,entity VARCHAR(32) NOT NULL,old_id VARCHAR(64) NOT NULL,new_id BIGINT UNSIGNED NULL,UNIQUE KEY uk_source(entity,old_id),UNIQUE KEY uk_target(entity,new_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +CREATE TABLE sw_kra_menu_permissions (source_menu_id BIGINT UNSIGNED PRIMARY KEY,source_permission VARCHAR(160),button_key VARCHAR(160)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE sw_kra_api_contract (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,method VARCHAR(12),source_path VARCHAR(255),target_path VARCHAR(255),source_permission VARCHAR(160),controller VARCHAR(80),implementation_status VARCHAR(32) NOT NULL DEFAULT 'pending_go_handler') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +START TRANSACTION; + +-- 五、写入 Kra 必需初始数据,并按 ID 映射转换用户、角色、部门及其他框架数据。 + +INSERT INTO `casbin_rule` (`id`,`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) VALUES +(1,'p','888','/integration/configs/:kind','GET','','',''), +(2,'p','888','/integration/configs/:kind/:provider','DELETE','','',''), +(3,'p','888','/integration/configs/:kind/:provider','GET','','',''), +(4,'p','888','/integration/configs/:kind/:provider','PUT','','',''), +(5,'p','888','/integration/configs/:kind/:provider/test','POST','','',''), +(6,'p','888','/integration/sms/:provider/send','POST','','',''), +(7,'p','888','/payment/create','POST','','',''), +(8,'p','888','/payment/fulfill','POST','','',''), +(9,'p','888','/payment/order','POST','','',''), +(10,'p','888','/payment/orders','GET','','',''), +(11,'p','888','/payment/orders/:provider/:tradeNo','GET','','',''), +(12,'p','888','/payment/orders/:provider/:tradeNo/events','GET','','',''), +(13,'p','888','/payment/orders/:provider/:tradeNo/fulfill','POST','','',''), +(14,'p','888','/payment/orders/:provider/:tradeNo/refund','POST','','',''), +(15,'p','888','/payment/orders/summary','GET','','',''), +(16,'p','888','/payment/providers/:provider/test','POST','','',''), +(17,'p','888','/payment/query','POST','','',''), +(18,'p','888','/payment/refund','POST','','',''), +(19,'p','888','/api/createApi','POST','','',''), +(20,'p','888','/api/deleteApi','POST','','',''), +(21,'p','888','/api/deleteApisByIds','DELETE','','',''), +(22,'p','888','/api/enterSyncApi','POST','','',''), +(23,'p','888','/api/getAllApis','POST','','',''), +(24,'p','888','/api/getApiById','POST','','',''), +(25,'p','888','/api/getApiGroups','GET','','',''), +(26,'p','888','/api/getApiList','POST','','',''), +(27,'p','888','/api/getApiRoles','GET','','',''), +(28,'p','888','/api/ignoreApi','POST','','',''), +(29,'p','888','/api/setApiRoles','POST','','',''), +(30,'p','888','/api/syncApi','GET','','',''), +(31,'p','888','/api/updateApi','POST','','',''), +(32,'p','888','/attachmentCategory/addCategory','POST','','',''), +(33,'p','888','/attachmentCategory/deleteCategory','POST','','',''), +(34,'p','888','/attachmentCategory/getCategoryList','GET','','',''), +(35,'p','888','/authority/copyAuthority','POST','','',''), +(36,'p','888','/authority/createAuthority','POST','','',''), +(37,'p','888','/authority/deleteAuthority','POST','','',''), +(38,'p','888','/authority/getAuthorityList','POST','','',''), +(39,'p','888','/authority/getDataScopeDepts','GET','','',''), +(40,'p','888','/authority/getUsersByAuthority','GET','','',''), +(41,'p','888','/authority/setDataScope','POST','','',''), +(42,'p','888','/authority/setRoleUsers','POST','','',''), +(43,'p','888','/authority/updateAuthority','PUT','','',''), +(44,'p','888','/authorityBtn/canRemoveAuthorityBtn','POST','','',''), +(45,'p','888','/authorityBtn/getAuthorityBtn','POST','','',''), +(46,'p','888','/authorityBtn/setAuthorityBtn','POST','','',''), +(47,'p','888','/casbin/getPolicyPathByAuthorityId','POST','','',''), +(48,'p','888','/casbin/updateCasbin','POST','','',''), +(49,'p','888','/dataAccessLog/deleteDataAccessLogByIds','DELETE','','',''), +(50,'p','888','/dataAccessLog/getDataAccessLogList','POST','','',''), +(51,'p','888','/department/createDepartment','POST','','',''), +(52,'p','888','/department/deleteDepartment','DELETE','','',''), +(53,'p','888','/department/findDepartment','GET','','',''), +(54,'p','888','/department/getDepartmentList','POST','','',''), +(55,'p','888','/department/getDepartmentUsers','GET','','',''), +(56,'p','888','/department/setDepartmentUsers','POST','','',''), +(57,'p','888','/department/updateDepartment','PUT','','',''), +(58,'p','888','/email/emailTest','POST','','',''), +(59,'p','888','/email/sendEmail','POST','','',''), +(60,'p','888','/fileUploadAndDownload/deleteFile','POST','','',''), +(61,'p','888','/fileUploadAndDownload/deleteFiles','POST','','',''), +(62,'p','888','/fileUploadAndDownload/editFileName','POST','','',''), +(63,'p','888','/fileUploadAndDownload/findFile','GET','','',''), +(64,'p','888','/fileUploadAndDownload/getFileList','POST','','',''), +(65,'p','888','/fileUploadAndDownload/importURL','POST','','',''), +(66,'p','888','/fileUploadAndDownload/listOssFiles','POST','','',''), +(67,'p','888','/fileUploadAndDownload/upload','POST','','',''), +(68,'p','888','/info/createInfo','POST','','',''), +(69,'p','888','/info/deleteInfo','DELETE','','',''), +(70,'p','888','/info/deleteInfoByIds','DELETE','','',''), +(71,'p','888','/info/findInfo','GET','','',''), +(72,'p','888','/info/getInfoDataSource','GET','','',''), +(73,'p','888','/info/getInfoList','GET','','',''), +(74,'p','888','/info/updateInfo','PUT','','',''), +(75,'p','888','/jwt/jsonInBlacklist','POST','','',''), +(76,'p','888','/logViewer/content','GET','','',''), +(77,'p','888','/logViewer/dates','GET','','',''), +(78,'p','888','/logViewer/files','GET','','',''), +(79,'p','888','/mediaUpload/:uploadId','DELETE','','',''), +(80,'p','888','/mediaUpload/chunk','POST','','',''), +(81,'p','888','/mediaUpload/complete','POST','','',''), +(82,'p','888','/mediaUpload/init','POST','','',''), +(83,'p','888','/menu/addBaseMenu','POST','','',''), +(84,'p','888','/menu/addMenuAuthority','POST','','',''), +(85,'p','888','/menu/deleteBaseMenu','POST','','',''), +(86,'p','888','/menu/getBaseMenuById','POST','','',''), +(87,'p','888','/menu/getBaseMenuTree','POST','','',''), +(88,'p','888','/menu/getMenu','POST','','',''), +(89,'p','888','/menu/getMenuAuthority','POST','','',''), +(90,'p','888','/menu/getMenuList','POST','','',''), +(91,'p','888','/menu/getMenuRoles','GET','','',''), +(92,'p','888','/menu/setMenuRoles','POST','','',''), +(93,'p','888','/menu/updateBaseMenu','POST','','',''), +(94,'p','888','/payment/callback/:provider','POST','','',''), +(95,'p','888','/position/createPosition','POST','','',''), +(96,'p','888','/position/deletePosition','DELETE','','',''), +(97,'p','888','/position/findPosition','GET','','',''), +(98,'p','888','/position/getPositionList','POST','','',''), +(99,'p','888','/position/getPositionUsers','GET','','',''), +(100,'p','888','/position/setPositionUsers','POST','','',''), +(101,'p','888','/position/updatePosition','PUT','','',''), +(102,'p','888','/securityConfig/getSecurityConfig','GET','','',''), +(103,'p','888','/securityConfig/setSecurityConfig','POST','','',''), +(104,'p','888','/sysApiToken/createApiToken','POST','','',''), +(105,'p','888','/sysApiToken/deleteApiToken','POST','','',''), +(106,'p','888','/sysApiToken/getApiTokenList','POST','','',''), +(107,'p','888','/sysDictionary/createSysDictionary','POST','','',''), +(108,'p','888','/sysDictionary/deleteSysDictionary','DELETE','','',''), +(109,'p','888','/sysDictionary/exportSysDictionary','GET','','',''), +(110,'p','888','/sysDictionary/findSysDictionary','GET','','',''), +(111,'p','888','/sysDictionary/getSysDictionaryList','GET','','',''), +(112,'p','888','/sysDictionary/getSysDictionaryListWithDetails','GET','','',''), +(113,'p','888','/sysDictionary/importSysDictionary','POST','','',''), +(114,'p','888','/sysDictionary/updateSysDictionary','PUT','','',''), +(115,'p','888','/sysDictionaryDetail/createSysDictionaryDetail','POST','','',''), +(116,'p','888','/sysDictionaryDetail/deleteSysDictionaryDetail','DELETE','','',''), +(117,'p','888','/sysDictionaryDetail/findSysDictionaryDetail','GET','','',''), +(118,'p','888','/sysDictionaryDetail/getDictionaryDetailsByParent','GET','','',''), +(119,'p','888','/sysDictionaryDetail/getDictionaryPath','GET','','',''), +(120,'p','888','/sysDictionaryDetail/getDictionaryTreeList','GET','','',''), +(121,'p','888','/sysDictionaryDetail/getDictionaryTreeListByType','GET','','',''), +(122,'p','888','/sysDictionaryDetail/getSysDictionaryDetailList','GET','','',''), +(123,'p','888','/sysDictionaryDetail/updateSysDictionaryDetail','PUT','','',''), +(124,'p','888','/sysError/createSysError','POST','','',''), +(125,'p','888','/sysError/deleteSysError','DELETE','','',''), +(126,'p','888','/sysError/deleteSysErrorByIds','DELETE','','',''), +(127,'p','888','/sysError/findSysError','GET','','',''), +(128,'p','888','/sysError/getSysErrorList','GET','','',''), +(129,'p','888','/sysError/updateSysError','PUT','','',''), +(130,'p','888','/sysExportTemplate/createSysExportTemplate','POST','','',''), +(131,'p','888','/sysExportTemplate/deleteSysExportTemplate','DELETE','','',''), +(132,'p','888','/sysExportTemplate/deleteSysExportTemplateByIds','DELETE','','',''), +(133,'p','888','/sysExportTemplate/exportExcel','GET','','',''), +(134,'p','888','/sysExportTemplate/exportExcelByToken','GET','','',''), +(135,'p','888','/sysExportTemplate/exportTemplate','GET','','',''), +(136,'p','888','/sysExportTemplate/exportTemplateByToken','GET','','',''), +(137,'p','888','/sysExportTemplate/findSysExportTemplate','GET','','',''), +(138,'p','888','/sysExportTemplate/getSysExportTemplateList','GET','','',''), +(139,'p','888','/sysExportTemplate/importExcel','POST','','',''), +(140,'p','888','/sysExportTemplate/previewSQL','GET','','',''), +(141,'p','888','/sysExportTemplate/updateSysExportTemplate','PUT','','',''), +(142,'p','888','/sysLoginLog/deleteLoginLog','DELETE','','',''), +(143,'p','888','/sysLoginLog/deleteLoginLogByIds','DELETE','','',''), +(144,'p','888','/sysLoginLog/findLoginLog','GET','','',''), +(145,'p','888','/sysLoginLog/getLoginLogList','GET','','',''), +(146,'p','888','/sysOperationRecord/deleteSysOperationRecord','DELETE','','',''), +(147,'p','888','/sysOperationRecord/deleteSysOperationRecordByIds','DELETE','','',''), +(148,'p','888','/sysOperationRecord/findSysOperationRecord','GET','','',''), +(149,'p','888','/sysOperationRecord/getSysOperationRecordList','GET','','',''), +(150,'p','888','/sysParams/createSysParams','POST','','',''), +(151,'p','888','/sysParams/deleteSysParams','DELETE','','',''), +(152,'p','888','/sysParams/deleteSysParamsByIds','DELETE','','',''), +(153,'p','888','/sysParams/findSysParams','GET','','',''), +(154,'p','888','/sysParams/getSysParam','GET','','',''), +(155,'p','888','/sysParams/getSysParamsList','GET','','',''), +(156,'p','888','/sysParams/updateSysParams','PUT','','',''), +(157,'p','888','/sysVersion/deleteSysVersion','DELETE','','',''), +(158,'p','888','/sysVersion/deleteSysVersionByIds','DELETE','','',''), +(159,'p','888','/sysVersion/downloadVersionJson','GET','','',''), +(160,'p','888','/sysVersion/exportVersion','POST','','',''), +(161,'p','888','/sysVersion/findSysVersion','GET','','',''), +(162,'p','888','/sysVersion/getSysVersionList','GET','','',''), +(163,'p','888','/sysVersion/importVersion','POST','','',''), +(164,'p','888','/system/getServerInfo','POST','','',''), +(165,'p','888','/system/getSystemConfig','POST','','',''), +(166,'p','888','/system/setSystemConfig','POST','','',''), +(167,'p','888','/timedTask/alertStream','GET','','',''), +(168,'p','888','/timedTask/createTimedTask','POST','','',''), +(169,'p','888','/timedTask/deleteTimedTask','DELETE','','',''), +(170,'p','888','/timedTask/getRegisteredMethods','GET','','',''), +(171,'p','888','/timedTask/getTimedTaskList','GET','','',''), +(172,'p','888','/timedTask/getTimedTaskLogList','GET','','',''), +(173,'p','888','/timedTask/toggleTimedTask','POST','','',''), +(174,'p','888','/timedTask/triggerTimedTask','POST','','',''), +(175,'p','888','/timedTask/updateTimedTask','PUT','','',''), +(176,'p','888','/user/admin_register','POST','','',''), +(177,'p','888','/user/changePassword','POST','','',''), +(178,'p','888','/user/deleteUser','DELETE','','',''), +(179,'p','888','/user/getUserInfo','GET','','',''), +(180,'p','888','/user/getUserList','POST','','',''), +(181,'p','888','/user/resetPassword','POST','','',''), +(182,'p','888','/user/setSelfInfo','PUT','','',''), +(183,'p','888','/user/setSelfSetting','PUT','','',''), +(184,'p','888','/user/setUserAuthorities','POST','','',''), +(185,'p','888','/user/setUserAuthority','POST','','',''), +(186,'p','888','/user/setUserDepartments','POST','','',''), +(187,'p','888','/user/setUserInfo','PUT','','',''), +(188,'p','888','/user/setUserPositions','POST','','',''); + +INSERT INTO `sys_apis` (`api_group`,`created_at`,`deleted_at`,`description`,`id`,`method`,`path`,`updated_at`) VALUES +('集成配置',NOW(3),NULL,'按类型获取集成配置',1,'GET','/integration/configs/:kind',NOW(3)), +('集成配置',NOW(3),NULL,'删除集成配置',2,'DELETE','/integration/configs/:kind/:provider',NOW(3)), +('集成配置',NOW(3),NULL,'获取指定集成配置',3,'GET','/integration/configs/:kind/:provider',NOW(3)), +('集成配置',NOW(3),NULL,'保存集成配置',4,'PUT','/integration/configs/:kind/:provider',NOW(3)), +('集成配置',NOW(3),NULL,'测试通信集成连接',5,'POST','/integration/configs/:kind/:provider/test',NOW(3)), +('短信服务',NOW(3),NULL,'发送单条模板短信',6,'POST','/integration/sms/:provider/send',NOW(3)), +('支付',NOW(3),NULL,'创建支付订单',7,'POST','/payment/create',NOW(3)), +('支付',NOW(3),NULL,'重试支付订单发货',8,'POST','/payment/fulfill',NOW(3)), +('支付',NOW(3),NULL,'查询支付订单',9,'POST','/payment/order',NOW(3)), +('支付',NOW(3),NULL,'分页查询支付订单',10,'GET','/payment/orders',NOW(3)), +('支付',NOW(3),NULL,'按路径查询支付订单',11,'GET','/payment/orders/:provider/:tradeNo',NOW(3)), +('支付',NOW(3),NULL,'查询支付订单事件流水',12,'GET','/payment/orders/:provider/:tradeNo/events',NOW(3)), +('支付',NOW(3),NULL,'按路径重试支付订单发货',13,'POST','/payment/orders/:provider/:tradeNo/fulfill',NOW(3)), +('支付',NOW(3),NULL,'按路径申请支付订单退款',14,'POST','/payment/orders/:provider/:tradeNo/refund',NOW(3)), +('支付',NOW(3),NULL,'汇总支付订单运营指标',15,'GET','/payment/orders/summary',NOW(3)), +('支付',NOW(3),NULL,'测试支付渠道配置与沙箱交易链路',16,'POST','/payment/providers/:provider/test',NOW(3)), +('支付',NOW(3),NULL,'同步支付订单状态',17,'POST','/payment/query',NOW(3)), +('支付',NOW(3),NULL,'申请支付订单退款',18,'POST','/payment/refund',NOW(3)), +('api',NOW(3),NULL,'创建api',19,'POST','/api/createApi',NOW(3)), +('api',NOW(3),NULL,'删除Api',20,'POST','/api/deleteApi',NOW(3)), +('api',NOW(3),NULL,'批量删除api',21,'DELETE','/api/deleteApisByIds',NOW(3)), +('api',NOW(3),NULL,'确认同步API',22,'POST','/api/enterSyncApi',NOW(3)), +('api',NOW(3),NULL,'刷新 Casbin 缓存',23,'GET','/api/freshCasbin',NOW(3)), +('api',NOW(3),NULL,'获取所有api',24,'POST','/api/getAllApis',NOW(3)), +('api',NOW(3),NULL,'获取api详细信息',25,'POST','/api/getApiById',NOW(3)), +('api',NOW(3),NULL,'获取路由组',26,'GET','/api/getApiGroups',NOW(3)), +('api',NOW(3),NULL,'获取api列表',27,'POST','/api/getApiList',NOW(3)), +('api',NOW(3),NULL,'获取指定API关联角色列表',28,'GET','/api/getApiRoles',NOW(3)), +('api',NOW(3),NULL,'忽略API',29,'POST','/api/ignoreApi',NOW(3)), +('api',NOW(3),NULL,'全量覆盖API关联角色列表',30,'POST','/api/setApiRoles',NOW(3)), +('api',NOW(3),NULL,'获取待同步API',31,'GET','/api/syncApi',NOW(3)), +('api',NOW(3),NULL,'更新Api',32,'POST','/api/updateApi',NOW(3)), +('媒体库分类',NOW(3),NULL,'添加/编辑分类',33,'POST','/attachmentCategory/addCategory',NOW(3)), +('媒体库分类',NOW(3),NULL,'删除分类',34,'POST','/attachmentCategory/deleteCategory',NOW(3)), +('媒体库分类',NOW(3),NULL,'分类列表',35,'GET','/attachmentCategory/getCategoryList',NOW(3)), +('角色',NOW(3),NULL,'拷贝角色',36,'POST','/authority/copyAuthority',NOW(3)), +('角色',NOW(3),NULL,'创建角色',37,'POST','/authority/createAuthority',NOW(3)), +('角色',NOW(3),NULL,'删除角色',38,'POST','/authority/deleteAuthority',NOW(3)), +('角色',NOW(3),NULL,'获取角色列表',39,'POST','/authority/getAuthorityList',NOW(3)), +('角色',NOW(3),NULL,'获取角色自定义部门集',40,'GET','/authority/getDataScopeDepts',NOW(3)), +('角色',NOW(3),NULL,'获取角色关联用户ID列表',41,'GET','/authority/getUsersByAuthority',NOW(3)), +('角色',NOW(3),NULL,'设置角色数据权限',42,'POST','/authority/setDataScope',NOW(3)), +('角色',NOW(3),NULL,'全量覆盖角色关联用户',43,'POST','/authority/setRoleUsers',NOW(3)), +('角色',NOW(3),NULL,'更新角色信息',44,'PUT','/authority/updateAuthority',NOW(3)), +('按钮权限',NOW(3),NULL,'删除按钮',45,'POST','/authorityBtn/canRemoveAuthorityBtn',NOW(3)), +('按钮权限',NOW(3),NULL,'获取已有按钮权限',46,'POST','/authorityBtn/getAuthorityBtn',NOW(3)), +('按钮权限',NOW(3),NULL,'设置按钮权限',47,'POST','/authorityBtn/setAuthorityBtn',NOW(3)), +('base',NOW(3),NULL,'获取验证码',48,'POST','/base/captcha',NOW(3)), +('base',NOW(3),NULL,'登录',49,'POST','/base/login',NOW(3)), +('casbin',NOW(3),NULL,'获取权限列表',50,'POST','/casbin/getPolicyPathByAuthorityId',NOW(3)), +('casbin',NOW(3),NULL,'更改角色api权限',51,'POST','/casbin/updateCasbin',NOW(3)), +('数据权限审计',NOW(3),NULL,'批量删除数据权限审计日志',52,'DELETE','/dataAccessLog/deleteDataAccessLogByIds',NOW(3)), +('数据权限审计',NOW(3),NULL,'获取数据权限审计日志',53,'POST','/dataAccessLog/getDataAccessLogList',NOW(3)), +('部门',NOW(3),NULL,'创建部门',54,'POST','/department/createDepartment',NOW(3)), +('部门',NOW(3),NULL,'删除部门',55,'DELETE','/department/deleteDepartment',NOW(3)), +('部门',NOW(3),NULL,'根据ID获取部门',56,'GET','/department/findDepartment',NOW(3)), +('部门',NOW(3),NULL,'获取部门树',57,'POST','/department/getDepartmentList',NOW(3)), +('部门',NOW(3),NULL,'获取部门成员ID列表',58,'GET','/department/getDepartmentUsers',NOW(3)), +('部门',NOW(3),NULL,'设置部门成员(反向分配)',59,'POST','/department/setDepartmentUsers',NOW(3)), +('部门',NOW(3),NULL,'更新部门',60,'PUT','/department/updateDepartment',NOW(3)), +('email',NOW(3),NULL,'发送测试邮件',61,'POST','/email/emailTest',NOW(3)), +('email',NOW(3),NULL,'发送邮件',62,'POST','/email/sendEmail',NOW(3)), +('文件上传与下载',NOW(3),NULL,'删除文件',63,'POST','/fileUploadAndDownload/deleteFile',NOW(3)), +('文件上传与下载',NOW(3),NULL,'批量删除文件',64,'POST','/fileUploadAndDownload/deleteFiles',NOW(3)), +('文件上传与下载',NOW(3),NULL,'文件名或者备注编辑',65,'POST','/fileUploadAndDownload/editFileName',NOW(3)), +('文件上传与下载',NOW(3),NULL,'获取文件详情',66,'GET','/fileUploadAndDownload/findFile',NOW(3)), +('文件上传与下载',NOW(3),NULL,'获取上传文件列表',67,'POST','/fileUploadAndDownload/getFileList',NOW(3)), +('文件上传与下载',NOW(3),NULL,'导入URL',68,'POST','/fileUploadAndDownload/importURL',NOW(3)), +('文件上传与下载',NOW(3),NULL,'获取对象存储文件列表',69,'POST','/fileUploadAndDownload/listOssFiles',NOW(3)), +('文件上传与下载',NOW(3),NULL,'文件上传(建议选择)',70,'POST','/fileUploadAndDownload/upload',NOW(3)), +('base',NOW(3),NULL,'健康检查',71,'GET','/health',NOW(3)), +('公告',NOW(3),NULL,'新建公告',72,'POST','/info/createInfo',NOW(3)), +('公告',NOW(3),NULL,'删除公告',73,'DELETE','/info/deleteInfo',NOW(3)), +('公告',NOW(3),NULL,'批量删除公告',74,'DELETE','/info/deleteInfoByIds',NOW(3)), +('公告',NOW(3),NULL,'根据ID获取公告',75,'GET','/info/findInfo',NOW(3)), +('公告',NOW(3),NULL,'获取公告数据源',76,'GET','/info/getInfoDataSource',NOW(3)), +('公告',NOW(3),NULL,'获取公告列表',77,'GET','/info/getInfoList',NOW(3)), +('公告',NOW(3),NULL,'获取公开公告',78,'GET','/info/getInfoPublic',NOW(3)), +('公告',NOW(3),NULL,'更新公告',79,'PUT','/info/updateInfo',NOW(3)), +('初始化',NOW(3),NULL,'检查数据库',80,'POST','/init/checkdb',NOW(3)), +('初始化',NOW(3),NULL,'初始化数据库',81,'POST','/init/initdb',NOW(3)), +('jwt',NOW(3),NULL,'jwt加入黑名单(退出,必选)',82,'POST','/jwt/jsonInBlacklist',NOW(3)), +('文件日志',NOW(3),NULL,'分块读取日志文件内容',83,'GET','/logViewer/content',NOW(3)), +('文件日志',NOW(3),NULL,'获取存在日志的日期',84,'GET','/logViewer/dates',NOW(3)), +('文件日志',NOW(3),NULL,'获取日期下的日志文件',85,'GET','/logViewer/files',NOW(3)), +('媒体上传',NOW(3),NULL,'取消大文件上传',86,'DELETE','/mediaUpload/:uploadId',NOW(3)), +('媒体上传',NOW(3),NULL,'上传分片',87,'POST','/mediaUpload/chunk',NOW(3)), +('媒体上传',NOW(3),NULL,'完成大文件上传',88,'POST','/mediaUpload/complete',NOW(3)), +('媒体上传',NOW(3),NULL,'初始化大文件上传',89,'POST','/mediaUpload/init',NOW(3)), +('菜单',NOW(3),NULL,'新增菜单',90,'POST','/menu/addBaseMenu',NOW(3)), +('菜单',NOW(3),NULL,'增加menu和角色关联关系',91,'POST','/menu/addMenuAuthority',NOW(3)), +('菜单',NOW(3),NULL,'删除菜单',92,'POST','/menu/deleteBaseMenu',NOW(3)), +('菜单',NOW(3),NULL,'根据id获取菜单',93,'POST','/menu/getBaseMenuById',NOW(3)), +('菜单',NOW(3),NULL,'获取用户动态路由',94,'POST','/menu/getBaseMenuTree',NOW(3)), +('菜单',NOW(3),NULL,'获取菜单树(必选)',95,'POST','/menu/getMenu',NOW(3)), +('菜单',NOW(3),NULL,'获取指定角色menu',96,'POST','/menu/getMenuAuthority',NOW(3)), +('菜单',NOW(3),NULL,'分页获取基础menu列表',97,'POST','/menu/getMenuList',NOW(3)), +('菜单',NOW(3),NULL,'获取菜单关联角色列表',98,'GET','/menu/getMenuRoles',NOW(3)), +('菜单',NOW(3),NULL,'全量覆盖菜单关联角色列表',99,'POST','/menu/setMenuRoles',NOW(3)), +('菜单',NOW(3),NULL,'更新菜单',100,'POST','/menu/updateBaseMenu',NOW(3)), +('支付',NOW(3),NULL,'处理支付回调',101,'POST','/payment/callback/:provider',NOW(3)), +('岗位',NOW(3),NULL,'创建岗位',102,'POST','/position/createPosition',NOW(3)), +('岗位',NOW(3),NULL,'删除岗位',103,'DELETE','/position/deletePosition',NOW(3)), +('岗位',NOW(3),NULL,'根据ID获取岗位',104,'GET','/position/findPosition',NOW(3)), +('岗位',NOW(3),NULL,'获取岗位列表',105,'POST','/position/getPositionList',NOW(3)), +('岗位',NOW(3),NULL,'获取岗位成员ID列表',106,'GET','/position/getPositionUsers',NOW(3)), +('岗位',NOW(3),NULL,'设置岗位成员(反向分配)',107,'POST','/position/setPositionUsers',NOW(3)), +('岗位',NOW(3),NULL,'更新岗位',108,'PUT','/position/updatePosition',NOW(3)), +('安全配置',NOW(3),NULL,'获取安全配置',109,'GET','/securityConfig/getSecurityConfig',NOW(3)), +('安全配置',NOW(3),NULL,'设置安全配置',110,'POST','/securityConfig/setSecurityConfig',NOW(3)), +('base',NOW(3),NULL,'Swagger 文档',111,'GET','/swagger/*any',NOW(3)), +('API Token',NOW(3),NULL,'签发API Token',112,'POST','/sysApiToken/createApiToken',NOW(3)), +('API Token',NOW(3),NULL,'作废API Token',113,'POST','/sysApiToken/deleteApiToken',NOW(3)), +('API Token',NOW(3),NULL,'获取API Token列表',114,'POST','/sysApiToken/getApiTokenList',NOW(3)), +('系统字典',NOW(3),NULL,'新增字典',115,'POST','/sysDictionary/createSysDictionary',NOW(3)), +('系统字典',NOW(3),NULL,'删除字典',116,'DELETE','/sysDictionary/deleteSysDictionary',NOW(3)), +('系统字典',NOW(3),NULL,'导出字典JSON',117,'GET','/sysDictionary/exportSysDictionary',NOW(3)), +('系统字典',NOW(3),NULL,'根据ID获取字典(建议选择)',118,'GET','/sysDictionary/findSysDictionary',NOW(3)), +('系统字典',NOW(3),NULL,'获取字典列表',119,'GET','/sysDictionary/getSysDictionaryList',NOW(3)), +('系统字典',NOW(3),NULL,'获取字典列表(含明细)',120,'GET','/sysDictionary/getSysDictionaryListWithDetails',NOW(3)), +('系统字典',NOW(3),NULL,'导入字典JSON',121,'POST','/sysDictionary/importSysDictionary',NOW(3)), +('系统字典',NOW(3),NULL,'更新字典',122,'PUT','/sysDictionary/updateSysDictionary',NOW(3)), +('系统字典详情',NOW(3),NULL,'新增字典内容',123,'POST','/sysDictionaryDetail/createSysDictionaryDetail',NOW(3)), +('系统字典详情',NOW(3),NULL,'删除字典内容',124,'DELETE','/sysDictionaryDetail/deleteSysDictionaryDetail',NOW(3)), +('系统字典详情',NOW(3),NULL,'根据ID获取字典内容',125,'GET','/sysDictionaryDetail/findSysDictionaryDetail',NOW(3)), +('系统字典详情',NOW(3),NULL,'根据父级ID获取字典详情',126,'GET','/sysDictionaryDetail/getDictionaryDetailsByParent',NOW(3)), +('系统字典详情',NOW(3),NULL,'获取字典详情的完整路径',127,'GET','/sysDictionaryDetail/getDictionaryPath',NOW(3)), +('系统字典详情',NOW(3),NULL,'获取字典数列表',128,'GET','/sysDictionaryDetail/getDictionaryTreeList',NOW(3)), +('系统字典详情',NOW(3),NULL,'根据分类获取字典数列表',129,'GET','/sysDictionaryDetail/getDictionaryTreeListByType',NOW(3)), +('系统字典详情',NOW(3),NULL,'获取字典内容列表',130,'GET','/sysDictionaryDetail/getSysDictionaryDetailList',NOW(3)), +('系统字典详情',NOW(3),NULL,'更新字典内容',131,'PUT','/sysDictionaryDetail/updateSysDictionaryDetail',NOW(3)), +('错误日志',NOW(3),NULL,'新建错误日志',132,'POST','/sysError/createSysError',NOW(3)), +('错误日志',NOW(3),NULL,'删除错误日志',133,'DELETE','/sysError/deleteSysError',NOW(3)), +('错误日志',NOW(3),NULL,'批量删除错误日志',134,'DELETE','/sysError/deleteSysErrorByIds',NOW(3)), +('错误日志',NOW(3),NULL,'根据ID获取错误日志',135,'GET','/sysError/findSysError',NOW(3)), +('错误日志',NOW(3),NULL,'获取错误日志列表',136,'GET','/sysError/getSysErrorList',NOW(3)), +('错误日志',NOW(3),NULL,'更新错误日志',137,'PUT','/sysError/updateSysError',NOW(3)), +('导出模板',NOW(3),NULL,'新增导出模板',138,'POST','/sysExportTemplate/createSysExportTemplate',NOW(3)), +('导出模板',NOW(3),NULL,'删除导出模板',139,'DELETE','/sysExportTemplate/deleteSysExportTemplate',NOW(3)), +('导出模板',NOW(3),NULL,'批量删除导出模板',140,'DELETE','/sysExportTemplate/deleteSysExportTemplateByIds',NOW(3)), +('导出模板',NOW(3),NULL,'导出Excel',141,'GET','/sysExportTemplate/exportExcel',NOW(3)), +('导出模板',NOW(3),NULL,'按令牌导出Excel',142,'GET','/sysExportTemplate/exportExcelByToken',NOW(3)), +('导出模板',NOW(3),NULL,'下载模板',143,'GET','/sysExportTemplate/exportTemplate',NOW(3)), +('导出模板',NOW(3),NULL,'按令牌下载模板',144,'GET','/sysExportTemplate/exportTemplateByToken',NOW(3)), +('导出模板',NOW(3),NULL,'根据ID获取导出模板',145,'GET','/sysExportTemplate/findSysExportTemplate',NOW(3)), +('导出模板',NOW(3),NULL,'获取导出模板列表',146,'GET','/sysExportTemplate/getSysExportTemplateList',NOW(3)), +('导出模板',NOW(3),NULL,'导入Excel',147,'POST','/sysExportTemplate/importExcel',NOW(3)), +('导出模板',NOW(3),NULL,'预览SQL',148,'GET','/sysExportTemplate/previewSQL',NOW(3)), +('导出模板',NOW(3),NULL,'更新导出模板',149,'PUT','/sysExportTemplate/updateSysExportTemplate',NOW(3)), +('登录日志',NOW(3),NULL,'删除登录日志',150,'DELETE','/sysLoginLog/deleteLoginLog',NOW(3)), +('登录日志',NOW(3),NULL,'批量删除登录日志',151,'DELETE','/sysLoginLog/deleteLoginLogByIds',NOW(3)), +('登录日志',NOW(3),NULL,'根据ID获取登录日志',152,'GET','/sysLoginLog/findLoginLog',NOW(3)), +('登录日志',NOW(3),NULL,'获取登录日志列表',153,'GET','/sysLoginLog/getLoginLogList',NOW(3)), +('操作记录',NOW(3),NULL,'删除操作记录',154,'DELETE','/sysOperationRecord/deleteSysOperationRecord',NOW(3)), +('操作记录',NOW(3),NULL,'批量删除操作历史',155,'DELETE','/sysOperationRecord/deleteSysOperationRecordByIds',NOW(3)), +('操作记录',NOW(3),NULL,'根据ID获取操作记录',156,'GET','/sysOperationRecord/findSysOperationRecord',NOW(3)), +('操作记录',NOW(3),NULL,'获取操作记录列表',157,'GET','/sysOperationRecord/getSysOperationRecordList',NOW(3)), +('参数管理',NOW(3),NULL,'新建参数',158,'POST','/sysParams/createSysParams',NOW(3)), +('参数管理',NOW(3),NULL,'删除参数',159,'DELETE','/sysParams/deleteSysParams',NOW(3)), +('参数管理',NOW(3),NULL,'批量删除参数',160,'DELETE','/sysParams/deleteSysParamsByIds',NOW(3)), +('参数管理',NOW(3),NULL,'根据ID获取参数',161,'GET','/sysParams/findSysParams',NOW(3)), +('参数管理',NOW(3),NULL,'按键获取参数',162,'GET','/sysParams/getSysParam',NOW(3)), +('参数管理',NOW(3),NULL,'获取参数列表',163,'GET','/sysParams/getSysParamsList',NOW(3)), +('参数管理',NOW(3),NULL,'更新参数',164,'PUT','/sysParams/updateSysParams',NOW(3)), +('版本控制',NOW(3),NULL,'删除版本',165,'DELETE','/sysVersion/deleteSysVersion',NOW(3)), +('版本控制',NOW(3),NULL,'批量删除版本',166,'DELETE','/sysVersion/deleteSysVersionByIds',NOW(3)), +('版本控制',NOW(3),NULL,'下载版本json',167,'GET','/sysVersion/downloadVersionJson',NOW(3)), +('版本控制',NOW(3),NULL,'创建版本',168,'POST','/sysVersion/exportVersion',NOW(3)), +('版本控制',NOW(3),NULL,'获取单一版本',169,'GET','/sysVersion/findSysVersion',NOW(3)), +('版本控制',NOW(3),NULL,'获取版本列表',170,'GET','/sysVersion/getSysVersionList',NOW(3)), +('版本控制',NOW(3),NULL,'同步版本',171,'POST','/sysVersion/importVersion',NOW(3)), +('系统服务',NOW(3),NULL,'获取服务器信息',172,'POST','/system/getServerInfo',NOW(3)), +('系统服务',NOW(3),NULL,'获取配置文件内容',173,'POST','/system/getSystemConfig',NOW(3)), +('系统服务',NOW(3),NULL,'重载系统配置',174,'POST','/system/reloadSystem',NOW(3)), +('系统服务',NOW(3),NULL,'设置配置文件内容',175,'POST','/system/setSystemConfig',NOW(3)), +('定时任务',NOW(3),NULL,'订阅定时任务失败告警(SSE)',176,'GET','/timedTask/alertStream',NOW(3)), +('定时任务',NOW(3),NULL,'创建定时任务',177,'POST','/timedTask/createTimedTask',NOW(3)), +('定时任务',NOW(3),NULL,'删除定时任务',178,'DELETE','/timedTask/deleteTimedTask',NOW(3)), +('定时任务',NOW(3),NULL,'获取已注册方法列表',179,'GET','/timedTask/getRegisteredMethods',NOW(3)), +('定时任务',NOW(3),NULL,'获取定时任务列表',180,'GET','/timedTask/getTimedTaskList',NOW(3)), +('定时任务',NOW(3),NULL,'获取定时任务执行日志',181,'GET','/timedTask/getTimedTaskLogList',NOW(3)), +('定时任务',NOW(3),NULL,'启用/停用定时任务',182,'POST','/timedTask/toggleTimedTask',NOW(3)), +('定时任务',NOW(3),NULL,'手动触发定时任务',183,'POST','/timedTask/triggerTimedTask',NOW(3)), +('定时任务',NOW(3),NULL,'更新定时任务',184,'PUT','/timedTask/updateTimedTask',NOW(3)), +('系统用户',NOW(3),NULL,'用户注册',185,'POST','/user/admin_register',NOW(3)), +('系统用户',NOW(3),NULL,'修改密码(建议选择)',186,'POST','/user/changePassword',NOW(3)), +('系统用户',NOW(3),NULL,'删除用户',187,'DELETE','/user/deleteUser',NOW(3)), +('系统用户',NOW(3),NULL,'获取自身信息(必选)',188,'GET','/user/getUserInfo',NOW(3)), +('系统用户',NOW(3),NULL,'获取用户列表',189,'POST','/user/getUserList',NOW(3)), +('系统用户',NOW(3),NULL,'重置用户密码',190,'POST','/user/resetPassword',NOW(3)), +('系统用户',NOW(3),NULL,'设置自身信息(必选)',191,'PUT','/user/setSelfInfo',NOW(3)), +('系统用户',NOW(3),NULL,'用户界面配置',192,'PUT','/user/setSelfSetting',NOW(3)), +('系统用户',NOW(3),NULL,'设置权限组',193,'POST','/user/setUserAuthorities',NOW(3)), +('系统用户',NOW(3),NULL,'修改用户角色(必选)',194,'POST','/user/setUserAuthority',NOW(3)), +('系统用户',NOW(3),NULL,'设置用户归属部门',195,'POST','/user/setUserDepartments',NOW(3)), +('系统用户',NOW(3),NULL,'设置用户信息',196,'PUT','/user/setUserInfo',NOW(3)), +('系统用户',NOW(3),NULL,'设置用户岗位',197,'POST','/user/setUserPositions',NOW(3)); + +INSERT INTO `sys_authorities` (`authority_id`,`authority_name`,`created_at`,`data_scope`,`default_router`,`deleted_at`,`parent_id`,`updated_at`) VALUES +(888,'超级管理员',NOW(3),1,'dashboard',NULL,0,NOW(3)); + +INSERT INTO `sys_authority_menus` (`sys_authority_authority_id`,`sys_base_menu_id`) VALUES +(888,1), +(888,9), +(888,13), +(888,16), +(888,20), +(888,28), +(888,2), +(888,10), +(888,14), +(888,17), +(888,21), +(888,29), +(888,3), +(888,11), +(888,15), +(888,18), +(888,22), +(888,4), +(888,12), +(888,19), +(888,23), +(888,30), +(888,5), +(888,24), +(888,31), +(888,6), +(888,25), +(888,34), +(888,26), +(888,35), +(888,27), +(888,32), +(888,33), +(888,7), +(888,8); + +INSERT INTO `sys_base_menus` (`active_name`,`close_tab`,`component`,`created_at`,`default_menu`,`deleted_at`,`hidden`,`icon`,`id`,`keep_alive`,`menu_level`,`name`,`parent_id`,`path`,`sort`,`title`,`transition_type`,`updated_at`) VALUES +('',0,'view/dashboard/index.vue',NOW(3),0,NULL,0,'odometer',1,0,0,'dashboard',0,'dashboard',1,'仪表盘','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'perm-kra',2,0,0,'permission',0,'permission',2,'权限管理','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'share',3,0,0,'org',0,'org',3,'组织管理','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'config-kra',4,0,0,'systemConfig',0,'systemConfig',4,'系统设置','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'monitor-kra',5,0,0,'monitor',0,'monitor',5,'运维监控','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'folder-opened',6,0,0,'media',0,'media',6,'媒体管理','',NOW(3)), +('',0,'view/routerHolder.vue',NOW(3),0,NULL,0,'cherry',7,0,0,'extensions',0,'extensions',10,'扩展功能','',NOW(3)), +('',0,'view/person/person.vue',NOW(3),0,NULL,1,'postcard',8,0,0,'person',0,'person',13,'个人信息','',NOW(3)), +('',0,'view/superAdmin/authority/authority.vue',NOW(3),0,NULL,0,'role-kra',9,0,1,'authority',2,'authority',1,'角色管理','',NOW(3)), +('',0,'view/superAdmin/menu/menu.vue',NOW(3),0,NULL,0,'tickets',10,1,1,'menu',2,'menu',2,'菜单管理','',NOW(3)), +('',0,'view/superAdmin/api/api.vue',NOW(3),0,NULL,0,'api-kra',11,1,1,'api',2,'api',3,'api管理','',NOW(3)), +('',0,'view/systemTools/apiToken/index.vue',NOW(3),0,NULL,0,'key',12,0,1,'apiToken',2,'apiToken',4,'API Token','',NOW(3)), +('',0,'view/superAdmin/user/user.vue',NOW(3),0,NULL,0,'user',13,0,1,'user',3,'user',1,'用户管理','',NOW(3)), +('',0,'view/superAdmin/department/department.vue',NOW(3),0,NULL,0,'office-building',14,0,1,'department',3,'department',2,'部门管理','',NOW(3)), +('',0,'view/superAdmin/position/position.vue',NOW(3),0,NULL,0,'postcard',15,0,1,'position',3,'position',3,'岗位管理','',NOW(3)), +('',0,'view/systemTools/system/system.vue',NOW(3),0,NULL,0,'config-file-kra',16,0,1,'system',4,'system',1,'配置文件','',NOW(3)), +('',0,'view/superAdmin/dictionary/sysDictionary.vue',NOW(3),0,NULL,0,'notebook',17,0,1,'dictionary',4,'dictionary',2,'字典管理','',NOW(3)), +('',0,'view/superAdmin/params/sysParams.vue',NOW(3),0,NULL,0,'set-up',18,0,1,'sysParams',4,'sysParams',3,'参数管理','',NOW(3)), +('',0,'view/system/security/index.vue',NOW(3),0,NULL,0,'security-kra',19,0,1,'security',4,'security',4,'安全配置','',NOW(3)), +('',0,'view/superAdmin/operation/sysOperationRecord.vue',NOW(3),0,NULL,0,'document',20,0,1,'operation',5,'operation',1,'操作历史','',NOW(3)), +('',0,'view/systemTools/loginLog/index.vue',NOW(3),0,NULL,0,'clock',21,0,1,'loginLog',5,'loginLog',2,'登录日志','',NOW(3)), +('',0,'view/systemTools/sysError/sysError.vue',NOW(3),0,NULL,0,'error-kra',22,0,1,'sysError',5,'sysError',3,'错误日志','',NOW(3)), +('',0,'view/systemTools/version/version.vue',NOW(3),0,NULL,0,'version-kra',23,0,1,'sysVersion',5,'sysVersion',4,'版本管理','',NOW(3)), +('',0,'view/system/state.vue',NOW(3),0,NULL,0,'server',24,0,1,'state',5,'state',5,'服务器状态','',NOW(3)), +('',0,'view/superAdmin/dataAccessLog/dataAccessLog.vue',NOW(3),0,NULL,0,'warning',25,0,1,'dataAccessLog',5,'dataAccessLog',6,'数据权限审计','',NOW(3)), +('',0,'view/systemTools/timedTask/index.vue',NOW(3),0,NULL,0,'timer',26,0,1,'timedTask',5,'timedTask',7,'定时任务','',NOW(3)), +('',0,'view/systemTools/logViewer/index.vue',NOW(3),0,NULL,0,'document',27,0,1,'logViewer',5,'logViewer',8,'文件日志','',NOW(3)), +('',0,'view/media/upload.vue',NOW(3),0,NULL,0,'upload',28,0,1,'upload',6,'upload',1,'媒体库(上传下载)','',NOW(3)), +('',0,'view/media/chunkUpload.vue',NOW(3),0,NULL,0,'folder-add',29,0,1,'chunkUpload',6,'chunkUpload',2,'大文件上传','',NOW(3)), +('',0,'modules/email/view/index.vue',NOW(3),0,NULL,0,'message',30,0,1,'email',7,'email',4,'邮件发送','',NOW(3)), +('',0,'modules/announcement/view/info.vue',NOW(3),0,NULL,0,'bell',31,0,1,'anInfo',7,'anInfo',5,'公告管理','',NOW(3)), +('',0,'view/systemTools/integration/config.vue',NOW(3),0,NULL,0,'connection',32,0,1,'integrationConfig',7,'integrationConfig',8,'通信集成','',NOW(3)), +('',0,'view/systemTools/sms/index.vue',NOW(3),0,NULL,0,'message',33,0,1,'sms',7,'sms',9,'短信服务','',NOW(3)), +('',0,'view/systemTools/payment/orders.vue',NOW(3),0,NULL,0,'wallet',34,0,1,'paymentOrders',7,'paymentOrders',6,'支付订单','',NOW(3)), +('',0,'view/systemTools/payment/config.vue',NOW(3),0,NULL,0,'credit-card',35,0,1,'paymentConfig',7,'paymentConfig',7,'支付配置','',NOW(3)); + +INSERT INTO `sys_export_templates` (`created_at`,`db_name`,`deleted_at`,`id`,`import_sql`,`limit`,`name`,`order`,`sql`,`table_name`,`template_id`,`template_info`,`updated_at`) VALUES +(NOW(3),'',NULL,1,'',NULL,'api','','','sys_apis','api','{ +"path":"路径", +"method":"方法(大写)", +"description":"方法介绍", +"api_group":"方法分组" +}',NOW(3)); + +INSERT INTO `sys_ignore_apis` (`created_at`,`deleted_at`,`id`,`method`,`path`,`updated_at`) VALUES +(NOW(3),NULL,1,'GET','/api/freshCasbin',NOW(3)), +(NOW(3),NULL,2,'GET','/health',NOW(3)), +(NOW(3),NULL,3,'GET','/swagger/*any',NOW(3)), +(NOW(3),NULL,4,'GET','/uploads/file/*filepath',NOW(3)), +(NOW(3),NULL,5,'HEAD','/uploads/file/*filepath',NOW(3)), +(NOW(3),NULL,6,'POST','/system/reloadSystem',NOW(3)), +(NOW(3),NULL,7,'POST','/base/login',NOW(3)), +(NOW(3),NULL,8,'POST','/base/captcha',NOW(3)), +(NOW(3),NULL,9,'POST','/init/initdb',NOW(3)), +(NOW(3),NULL,10,'POST','/init/checkdb',NOW(3)), +(NOW(3),NULL,11,'GET','/info/getInfoPublic',NOW(3)); + +INSERT INTO `sys_integration_configs` (`config`,`created_at`,`enabled`,`id`,`kind`,`provider`,`updated_at`) VALUES +('{"broker":"tcp://127.0.0.1:1883","clean_session":true,"client_id":"kra","connect_timeout":10,"keep_alive":30,"password":"","reconnect_interval":5,"username":""}',NOW(3),0,1,'mq','emqx',NOW(3)), +('{"allow_auto_topic_creation":false,"brokers":["127.0.0.1:9092"],"client_id":"kra","connect_timeout":10,"group_id":"kra","max_bytes":10485760,"max_wait":1,"min_bytes":1,"password":"","reconnect_interval":5,"start_offset":"earliest","tls":false,"tls_skip_verify":false,"username":""}',NOW(3),0,2,'mq','kafka',NOW(3)), +('{"auto_delete":false,"connect_timeout":10,"durable":true,"exchange":"kra","exchange_type":"topic","heartbeat":10,"host":"127.0.0.1","password":"guest","port":5672,"prefetch_count":10,"queue":"kra","reconnect_interval":5,"routing_key":"#","tls":false,"username":"guest","vhost":"/"}',NOW(3),0,3,'mq','rabbitmq',NOW(3)), +('{"allow_origins":[],"concurrent_message_handling":false,"max_message_size":0,"message_buffer_size":0,"path":"/ws","ping_period":"54s","pong_wait":"60s","write_wait":"10s"}',NOW(3),0,4,'websocket','melody',NOW(3)), +('{"app_id":"","environment":"production","gateway_url":"https://openapi.alipay.com/gateway.do","method":"alipay.trade.create","notify_url":"","private_key":"","public_key":"","return_url":"","sign_type":"RSA2","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,5,'payment','alipay',NOW(3)), +('{"api_base_url":"https://openapi.alipay.com","app_cert":"","app_id":"","environment":"production","gateway_url":"https://openapi.alipay.com/gateway.do","method":"alipay.trade.create","notify_url":"","private_key":"","public_cert":"","return_url":"","root_cert":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,6,'payment','alipay-v3',NOW(3)), +('{"app_id":"","currency":"CNY","cus_id":"","environment":"production","notify_url":"","org_id":"","pay_type":"W02","private_key":"","public_key":"","query_order_type":"reqsn","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,7,'payment','allinpay',NOW(3)), +('{"bundle_id":"","environment":"production","issuer_id":"","key_id":"","notify_url":"","price_divisor":10,"private_key":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false,"test_product_id":"","test_transaction_id":""}',NOW(3),0,8,'payment','apple-iap',NOW(3)), +('{"app_id":"","app_key":"","callback_provider_trade_no_field":"","callback_status_field":"","callback_success_values":"","callback_trade_no_field":"","create_url":"","merchant_id":"","notify_url":"","protocol_version":"v1","query_amount_field":"","query_amount_scale":1,"query_currency_field":"","query_provider_trade_no_field":"","query_status_field":"","query_success_values":"","query_trade_no_field":"","query_url":"","refund_failure_values":"","refund_status_field":"","refund_success_values":"","refund_url":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,9,'payment','chinaums',NOW(3)), +('{"api_key":"","app_id":"","environment":"production","merchant_id":"","notify_url":"","platform_cert":"","platform_serial_no":"","private_key":"","return_url":"","serial_no":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false,"trade_type":"jsapi"}',NOW(3),0,10,'payment','douyin',NOW(3)), +('{"app_id":"","app_key":"","callback_provider_trade_no_field":"","callback_status_field":"","callback_success_values":"","callback_trade_no_field":"","create_url":"","merchant_id":"","notify_url":"","protocol_version":"v1","query_amount_field":"","query_amount_scale":1,"query_currency_field":"","query_provider_trade_no_field":"","query_status_field":"","query_success_values":"","query_trade_no_field":"","query_url":"","refund_failure_values":"","refund_status_field":"","refund_success_values":"","refund_url":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,11,'payment','douyin-game-pay',NOW(3)), +('{"channel":"Wechat","credential_code":"","currency":"CNY","environment":"production","method":"jsapi","notify_url":"","partner_code":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,12,'payment','lakala',NOW(3)), +('{"auto_capture":true,"cancel_url":"","client_id":"","client_secret":"","environment":"sandbox","notify_url":"","return_url":"","test_amount":1,"test_currency":"USD","test_extra":"{}","test_mode":false,"webhook_id":""}',NOW(3),0,13,'payment','paypal',NOW(3)), +('{"api_key":"","cert_file":"","environment":"production","key_file":"","mch_id":"","notify_url":"","return_url":"","sign_type":"MD5","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false,"trade_type":"NATIVE"}',NOW(3),0,14,'payment','qq',NOW(3)), +('{"access_token":"","currency":"CNY","environment":"production","inst_no":"","key":"","merchant_no":"","notify_url":"","pay_type":"010","return_url":"","terminal_id":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,15,'payment','saobei',NOW(3)), +('{"app_id":"","app_key":"","callback_provider_trade_no_field":"","callback_status_field":"","callback_success_values":"","callback_trade_no_field":"","create_url":"","merchant_id":"","notify_url":"","protocol_version":"v1","query_amount_field":"","query_amount_scale":1,"query_currency_field":"","query_provider_trade_no_field":"","query_status_field":"","query_success_values":"","query_trade_no_field":"","query_url":"","refund_failure_values":"","refund_status_field":"","refund_success_values":"","refund_url":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,16,'payment','sft',NOW(3)), +('{"app_id":"","app_key":"","callback_provider_trade_no_field":"","callback_status_field":"","callback_success_values":"","callback_trade_no_field":"","create_url":"","merchant_id":"","notify_url":"","protocol_version":"v1","query_amount_field":"","query_amount_scale":1,"query_currency_field":"","query_provider_trade_no_field":"","query_status_field":"","query_success_values":"","query_trade_no_field":"","query_url":"","refund_failure_values":"","refund_status_field":"","refund_success_values":"","refund_url":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,17,'payment','supper-pay',NOW(3)), +('{"app_id":"","app_key":"","callback_provider_trade_no_field":"","callback_status_field":"","callback_success_values":"","callback_trade_no_field":"","create_url":"","merchant_id":"","notify_url":"","protocol_version":"v1","query_amount_field":"","query_amount_scale":1,"query_currency_field":"","query_provider_trade_no_field":"","query_status_field":"","query_success_values":"","query_trade_no_field":"","query_url":"","refund_failure_values":"","refund_status_field":"","refund_success_values":"","refund_url":"","return_url":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false}',NOW(3),0,18,'payment','wechat-game-pay',NOW(3)), +('{"app_id":"","client_cert":"","client_key":"","mch_key":"","merchant_id":"","notify_url":"","return_url":"","sign_type":"MD5","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false,"trade_type":"NATIVE"}',NOW(3),0,19,'payment','wechat-v2',NOW(3)), +('{"api_v3_key":"","app_id":"","merchant_id":"","notify_url":"","platform_cert":"","platform_serial_no":"","private_key":"","return_url":"","serial_no":"","test_amount":1,"test_currency":"CNY","test_extra":"{}","test_mode":false,"trade_type":"jsapi"}',NOW(3),0,20,'payment','wechat-v3',NOW(3)); + +INSERT INTO `sys_schema_migrations` (`id`) VALUES +('202608200002_system_schema'), +('202608210002_communication_surface'), +('202608210003_communication_test_surface'), +('202608200001_data_infrastructure'), +('202608210001_communication_integration_defaults'), +('202608200004_payment_defaults'), +('202608290004_kafka_integration_default'), +('202608290005_communication_integration_default_upgrade'), +('202609290001_sms_admin_surface'), +('202608220001_task_schema'), +('202608200003_payment_schema'), +('202608290001_payment_event_schema'), +('202608290002_payment_admin_surface'), +('202608290003_payment_order_read_permissions'); + +INSERT INTO `sys_security_config` (`captcha_open`,`captcha_timeout`,`created_at`,`deleted_at`,`force_new_user_change_password`,`id`,`img_height`,`img_width`,`key_long`,`limit_count`,`limit_enable`,`limit_window`,`lock_duration`,`lock_enable`,`lock_threshold`,`pwd_expire_days`,`pwd_expire_enable`,`pwd_min_length`,`pwd_require_digit`,`pwd_require_lower`,`pwd_require_special`,`pwd_require_upper`,`updated_at`) VALUES +(0,3600,NOW(3),NULL,0,1,80,240,6,30,0,60,30,0,5,90,0,8,0,0,0,0,NOW(3)); + +INSERT INTO `sys_timed_tasks` (`created_at`,`deleted_at`,`description`,`enabled`,`executor_type`,`http_allow_private`,`http_body`,`http_header`,`http_method`,`http_url`,`id`,`method_name`,`name`,`params`,`spec`,`updated_at`,`with_seconds`) VALUES +(NOW(3),NULL,'定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)',0,'method',0,'',NULL,'','',1,'ClearDB','ClearDB',NULL,'@daily',NOW(3),0), +(NOW(3),NULL,'定时清理过期大文件上传会话',0,'method',0,'',NULL,'','',2,'CleanStaleUploads','CleanStaleUploads',NULL,'@hourly',NOW(3),0); + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'user',CAST(`user_id` AS CHAR) FROM `sys_user` WHERE del_flag='0' ORDER BY `user_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'role',CAST(`role_id` AS CHAR) FROM `sys_role` WHERE del_flag='0' ORDER BY `role_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'dept',CAST(`dept_id` AS CHAR) FROM `sys_dept` WHERE del_flag='0' ORDER BY `dept_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'post',CAST(`post_id` AS CHAR) FROM `sys_post` WHERE del_flag='0' ORDER BY `post_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'file',CAST(`oss_id` AS CHAR) FROM `sys_oss` WHERE 1=1 ORDER BY `oss_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'menu',CAST(`menu_id` AS CHAR) FROM `sys_menu` WHERE menu_id IN ('2084194096229949441','2084194830778408961','2084202636386541569','2084224284976246785','2084224583841378306','2084449385185308674','2084453258847350785','2084458436631752706','2084466605093318657','2084475309624684546','2084478005203202049','2084480429515108353','2084480593562726401','2086033702105710593','2086034409621880833','2086621845875916801','2086623544338350082','2086643768701812738','2086643813702500354','2086643981961199618','2086644092393029633','2087069357514440706','2087385085920727041','2087385679095975937','2087385949666332673','2087386325354336258','2087444380922167298','2087447465111326721','2087831493110956033','2095702540414996481','2095702652168032258') ORDER BY `menu_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'dict',CAST(`dict_id` AS CHAR) FROM `sys_dict_type` WHERE 1=1 ORDER BY `dict_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'dict_item',CAST(`dict_code` AS CHAR) FROM `sys_dict_data` WHERE 1=1 ORDER BY `dict_code`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'notice',CAST(`notice_id` AS CHAR) FROM `sys_notice` WHERE 1=1 ORDER BY `notice_id`; + +INSERT INTO sw_kra_id_map(entity,old_id) SELECT 'task',CAST(`id` AS CHAR) FROM `sj_job` WHERE deleted=0 ORDER BY `id`; + +UPDATE sw_kra_id_map SET new_id=10000+id; + +UPDATE sw_kra_id_map m JOIN sys_role r ON m.entity='role' AND m.old_id=CAST(r.role_id AS CHAR) SET m.new_id=888 WHERE r.role_key='superadmin'; + +INSERT INTO sys_authorities(authority_id,created_at,updated_at,authority_name,parent_id,data_scope,default_router) SELECT m.new_id,r.create_time,r.update_time,r.role_name,888,CASE r.data_scope WHEN '1' THEN 1 WHEN '2' THEN 5 WHEN '3' THEN 3 WHEN '4' THEN 2 WHEN '5' THEN 4 END,'dashboard' FROM sys_role r JOIN sw_kra_id_map m ON m.entity='role' AND m.old_id=CAST(r.role_id AS CHAR) WHERE r.del_flag='0' AND r.role_key<>'superadmin'; + +INSERT INTO sys_departments(id,created_at,updated_at,name,parent_id,ancestors,sort,leader_id,status) SELECT m.new_id,d.create_time,d.update_time,d.dept_name,COALESCE(p.new_id,0),'0',d.order_num,COALESCE(u.new_id,0),d.status='0' FROM sys_dept d JOIN sw_kra_id_map m ON m.entity='dept' AND m.old_id=CAST(d.dept_id AS CHAR) LEFT JOIN sw_kra_id_map p ON p.entity='dept' AND p.old_id=CAST(d.parent_id AS CHAR) LEFT JOIN sw_kra_id_map u ON u.entity='user' AND u.old_id=CAST(d.leader AS CHAR) WHERE d.del_flag='0'; + +SET v_iter=0; WHILE v_iter<16 DO UPDATE sys_departments d JOIN sys_departments p ON p.id=d.parent_id SET d.ancestors=CONCAT(p.ancestors,',',p.id); SET v_iter=v_iter+1; END WHILE; + +INSERT INTO sys_positions(id,created_at,updated_at,name,code,sort,status,remark,dept_id) SELECT m.new_id,p.create_time,p.update_time,p.post_name,p.post_code,p.post_sort,p.status='0',p.remark,d.new_id FROM sys_post p JOIN sw_kra_id_map m ON m.entity='post' AND m.old_id=CAST(p.post_id AS CHAR) LEFT JOIN sw_kra_id_map d ON d.entity='dept' AND d.old_id=CAST(p.dept_id AS CHAR) WHERE p.del_flag='0'; + +INSERT INTO sys_users(id,created_at,updated_at,uuid,username,password,nick_name,header_img,authority_id,dept_id,phone,email,enable,origin_setting,password_updated_at,must_change_password,level,parent_id,id_path,real_name,qq_number,business_status,reject_reason) SELECT m.new_id,u.create_time,u.update_time,UUID(),u.user_name,u.password,u.nick_name,COALESCE(o.url,''),rm.new_id,COALESCE(dm.new_id,0),u.phone_number,u.email,u.status='0','{}',COALESCE(u.update_time,u.create_time,NOW(3)),0,u.level,COALESCE(pm.new_id,0),'0',COALESCE(u.real_name,''),COALESCE(u.qq_number,''),CAST(u.status AS UNSIGNED),COALESCE(u.reject_reason,'') FROM sys_user u JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(u.user_id AS CHAR) JOIN sys_user_role ur ON ur.user_id=u.user_id JOIN sw_kra_id_map rm ON rm.entity='role' AND rm.old_id=CAST(ur.role_id AS CHAR) LEFT JOIN sw_kra_id_map dm ON dm.entity='dept' AND dm.old_id=CAST(u.dept_id AS CHAR) LEFT JOIN sw_kra_id_map pm ON pm.entity='user' AND pm.old_id=CAST(u.parent_id AS CHAR) LEFT JOIN sys_oss o ON o.oss_id=u.avatar WHERE u.del_flag='0'; + +SET v_iter=0; WHILE v_iter<4 DO UPDATE sys_users u JOIN sys_users p ON p.id=u.parent_id SET u.id_path=CONCAT(p.id_path,',',p.id); SET v_iter=v_iter+1; END WHILE; + +INSERT INTO sys_user_authority(sys_user_id,sys_authority_authority_id) SELECT um.new_id,rm.new_id FROM sys_user_role x JOIN sw_kra_id_map um ON um.entity='user' AND um.old_id=CAST(x.user_id AS CHAR) JOIN sw_kra_id_map rm ON rm.entity='role' AND rm.old_id=CAST(x.role_id AS CHAR); + +INSERT INTO sys_user_departments(sys_user_id,sys_department_id) SELECT id,dept_id FROM sys_users WHERE dept_id<>0; + +INSERT INTO sys_user_positions(sys_user_id,sys_position_id) SELECT um.new_id,pm.new_id FROM sys_user_post x JOIN sw_kra_id_map um ON um.entity='user' AND um.old_id=CAST(x.user_id AS CHAR) JOIN sw_kra_id_map pm ON pm.entity='post' AND pm.old_id=CAST(x.post_id AS CHAR); + +INSERT INTO media_file_upload_and_downloads(id,created_at,updated_at,name,class_id,url,tag,`key`,size,mime,md5,user_id) SELECT m.new_id,o.create_time,o.update_time,o.original_name,0,o.url,o.file_suffix,o.file_name,COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.fileSize')) AS UNSIGNED),0),COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.contentType')),'null'),''),COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.md5')),'null'),''),COALESCE(um.new_id,0) FROM sys_oss o JOIN sw_kra_id_map m ON m.entity='file' AND m.old_id=CAST(o.oss_id AS CHAR) LEFT JOIN sw_kra_id_map um ON um.entity='user' AND um.old_id=CAST(o.create_by AS CHAR); + +INSERT INTO sys_dictionaries(id,created_at,updated_at,name,type,status,`desc`,parent_id) SELECT m.new_id,d.create_time,d.update_time,d.dict_name,d.dict_type,1,d.remark,0 FROM sys_dict_type d JOIN sw_kra_id_map m ON m.entity='dict' AND m.old_id=CAST(d.dict_id AS CHAR); + +INSERT INTO sys_dictionary_details(id,created_at,updated_at,label,value,extend,status,sort,sys_dictionary_id,parent_id,level,path) SELECT m.new_id,d.create_time,d.update_time,d.dict_label,d.dict_value,JSON_OBJECT('cssClass',d.css_class,'listClass',d.list_class,'isDefault',d.is_default),1,d.dict_sort,tm.new_id,0,0,'0' FROM sys_dict_data d JOIN sw_kra_id_map m ON m.entity='dict_item' AND m.old_id=CAST(d.dict_code AS CHAR) JOIN sys_dict_type t ON t.dict_type=d.dict_type JOIN sw_kra_id_map tm ON tm.entity='dict' AND tm.old_id=CAST(t.dict_id AS CHAR); + +INSERT INTO sys_announcements(id,created_at,updated_at,title,content,user_id,attachments) SELECT m.new_id,n.create_time,n.update_time,n.notice_title,CONVERT(n.notice_content USING utf8mb4),um.new_id,JSON_ARRAY() FROM sys_notice n JOIN sw_kra_id_map m ON m.entity='notice' AND m.old_id=CAST(n.notice_id AS CHAR) LEFT JOIN sw_kra_id_map um ON um.entity='user' AND um.old_id=CAST(n.create_by AS CHAR) WHERE n.status='0'; + +INSERT INTO sys_params(created_at,updated_at,name,`key`,value,`desc`) SELECT NOW(3),NOW(3),config_name,CONCAT('sw.legacy.',config_key),config_value,'Legacy non-secret setting; application adapter must consume it explicitly' FROM sys_config WHERE config_key IN('sys.account.registerUser','sys.oss.previewListResource'); + +-- 六、导入新增业务菜单及授权;原工作台页面和查询按钮不再导入。 + +INSERT INTO sw_kra_menu_permissions(source_menu_id,source_permission,button_key) VALUES +('2084194096229949441','',''), +('2084194830778408961','system:user:list','sw:business:list'), +('2084202636386541569','system:promote:list','sw:promote:list'), +('2084224284976246785','',''), +('2084224583841378306','system:channelData:list','sw:channelData:list'), +('2084449385185308674','system:spend:list','sw:spend:list'), +('2084453258847350785','system:register:list','sw:register:list'), +('2084458436631752706','system:gameData:list','sw:gameData:list'), +('2084466605093318657','system:serverAnalysis:list','sw:serverAnalysis:list'), +('2084475309624684546','system:rank:channel','sw:rank:channel'), +('2084478005203202049','',''), +('2084480429515108353','system:alertRecord:list','sw:alertRecord:list'), +('2084480593562726401','system:alertRule:list','sw:alertRule:list'), +('2086033702105710593','system:alertRecord:audit','sw:alertRecord:audit'), +('2086034409621880833','system:alertRecord:query','sw:alertRecord:query'), +('2086621845875916801','system:alertRecord:process','sw:alertRecord:process'), +('2086623544338350082','system:oss:upload','sw:oss:upload'), +('2086643768701812738','system:alertRule:query','sw:alertRule:query'), +('2086643813702500354','system:alertRule:add','sw:alertRule:add'), +('2086643981961199618','system:alertRule:edit','sw:alertRule:edit'), +('2086644092393029633','system:alertRule:remove','sw:alertRule:remove'), +('2087069357514440706','system:promote:add','sw:promote:add'), +('2087385085920727041','system:user:add','sw:business:add'), +('2087385679095975937','system:user:query','sw:business:query'), +('2087385949666332673','system:user:edit','sw:business:edit'), +('2087386325354336258','system:user:export','sw:business:export'), +('2087444380922167298','system:user:bind','sw:business:bind'), +('2087447465111326721','system:promote:list','sw:promote:list'), +('2087831493110956033','system:register:export','sw:register:export'), +('2095702540414996481','',''), +('2095702652168032258','system:promoteActivePool:list','sw:promoteActivePool:list'), +('2087072517465325569','system:workbench:query','sw:workbench:query'), +('2087365025688768514','system:workbench:query','sw:workbench:query'); + +INSERT INTO sys_base_menus(id,created_at,updated_at,menu_level,parent_id,path,name,hidden,component,sort,active_name,keep_alive,default_menu,title,icon,close_tab,transition_type) SELECT m.new_id,n.create_time,n.update_time,IF(n.parent_id=0,0,1),COALESCE(pm.new_id,0),IF(n.parent_id=0,CONCAT('sw-',n.path),n.path),CONCAT('sw_',n.path),n.visible='1',IF(n.menu_type='M','view/routerHolder.vue',CONCAT('view/sw/',n.path,'.vue')),n.order_num,'',n.is_cache='Y',0,n.menu_name,CASE n.path + WHEN 'BusMgmt' THEN 'Briefcase' + WHEN 'BusinessList' THEN 'UserFilled' + WHEN 'ChannelList' THEN 'Connection' + WHEN 'ChannelActivationList' THEN 'CircleCheck' + WHEN 'DataAnalysis' THEN 'DataAnalysis' + WHEN 'ChannelDataStatistics' THEN 'Histogram' + WHEN 'RechargeDetails' THEN 'Money' + WHEN 'RegistrationDetails' THEN 'User' + WHEN 'GameAnalytics' THEN 'TrendCharts' + WHEN 'ServerAnalysis' THEN 'Monitor' + WHEN 'RankingAnalysis' THEN 'Star' + WHEN 'AlertCenter' THEN 'Bell' + WHEN 'AlertList' THEN 'Warning' + WHEN 'AlertRules' THEN 'Setting' + ELSE 'Menu' END,0,'' FROM sys_menu n JOIN sw_kra_id_map m ON m.entity='menu' AND m.old_id=CAST(n.menu_id AS CHAR) LEFT JOIN sw_kra_id_map pm ON pm.entity='menu' AND pm.old_id=CAST(n.parent_id AS CHAR) WHERE n.menu_type IN('M','C'); + +UPDATE sys_base_menus SET sort=CASE name WHEN 'dashboard' THEN 1 WHEN 'sw_BusMgmt' THEN 2 WHEN 'sw_DataAnalysis' THEN 3 WHEN 'sw_AlertCenter' THEN 4 WHEN 'permission' THEN 5 WHEN 'org' THEN 6 WHEN 'systemConfig' THEN 7 WHEN 'monitor' THEN 8 WHEN 'media' THEN 9 WHEN 'extensions' THEN 13 WHEN 'person' THEN 16 ELSE sort END WHERE parent_id=0; + +INSERT INTO sys_base_menu_btns(id,created_at,updated_at,name,`desc`,sys_base_menu_id) SELECT m.new_id,n.create_time,n.update_time,p.button_key,n.menu_name,pm.new_id FROM sys_menu n JOIN sw_kra_id_map m ON m.entity='menu' AND m.old_id=CAST(n.menu_id AS CHAR) JOIN sw_kra_id_map pm ON pm.entity='menu' AND pm.old_id=CAST(n.parent_id AS CHAR) JOIN sw_kra_menu_permissions p ON p.source_menu_id=n.menu_id WHERE n.menu_type='F'; + +INSERT INTO sys_authority_menus(sys_authority_authority_id,sys_base_menu_id) SELECT rm.new_id,mm.new_id FROM sys_role_menu x JOIN sw_kra_id_map rm ON rm.entity='role' AND rm.old_id=CAST(x.role_id AS CHAR) JOIN sw_kra_id_map mm ON mm.entity='menu' AND mm.old_id=CAST(x.menu_id AS CHAR) JOIN sys_menu m ON m.menu_id=x.menu_id WHERE m.menu_type IN('M','C'); + +INSERT INTO sys_authority_btns(authority_id,sys_menu_id,sys_base_menu_btn_id) SELECT rm.new_id,pm.new_id,mm.new_id FROM sys_role_menu x JOIN sw_kra_id_map rm ON rm.entity='role' AND rm.old_id=CAST(x.role_id AS CHAR) JOIN sw_kra_id_map mm ON mm.entity='menu' AND mm.old_id=CAST(x.menu_id AS CHAR) JOIN sys_menu m ON m.menu_id=x.menu_id JOIN sw_kra_id_map pm ON pm.entity='menu' AND pm.old_id=CAST(m.parent_id AS CHAR) WHERE m.menu_type='F'; + +INSERT INTO sys_authority_menus(sys_authority_authority_id,sys_base_menu_id) SELECT 888,m.new_id FROM sw_kra_id_map m JOIN sys_menu n ON m.entity='menu' AND m.old_id=CAST(n.menu_id AS CHAR) WHERE n.menu_type IN('M','C'); + +INSERT INTO sys_authority_btns(authority_id,sys_menu_id,sys_base_menu_btn_id) SELECT 888,sys_base_menu_id,id FROM sys_base_menu_btns; + +INSERT INTO sys_authority_menus(sys_authority_authority_id,sys_base_menu_id) SELECT a.authority_id,m.id FROM sys_authorities a JOIN sys_base_menus m ON m.name='dashboard' WHERE a.authority_id<>888 AND a.default_router='dashboard'; + +INSERT INTO sw_kra_api_contract(method,source_path,target_path,source_permission,controller) VALUES +('GET','/system/user/preference','/sw/user/preference','__authenticated__','UserPreference'), +('PUT','/system/user/preference','/sw/user/preference','__authenticated__','UserPreference'), +('GET','/system/promote/list','/sw/promote/list','system:promote:list','Promote'), +('GET','/system/promote/portrait','/sw/promote/portrait','system:promote:list','Promote'), +('GET','/system/promote/game-distribution','/sw/promote/game-distribution','system:promote:list','Promote'), +('GET','/system/promote/bindOptions','/sw/promote/bindOptions','system:promote:add','Promote'), +('POST','/system/promote','/sw/promote','system:promote:add','Promote'), +('GET','/system/promoteActivePool/bindList','/sw/promoteActivePool/bindList','system:promoteActivePool:list','PromoteActivePool'), +('GET','/system/promoteActivePool/bindSpendDetail','/sw/promoteActivePool/bindSpendDetail','system:promoteActivePool:list','PromoteActivePool'), +('GET','/system/workbench/summary','/sw/workbench/summary','system:workbench:query','Workbench'), +('GET','/system/workbench/registerTop','/sw/workbench/registerTop','system:workbench:query','Workbench'), +('GET','/system/workbench/payRank','/sw/workbench/payRank','system:workbench:query','Workbench'), +('GET','/system/workbench/unloginTop','/sw/workbench/unloginTop','system:workbench:query','Workbench'), +('GET','/system/rank/channel','/sw/rank/channel','system:rank:channel','Rank'), +('GET','/system/rank/business','/sw/rank/business','system:rank:channel','Rank'), +('GET','/system/rank/game','/sw/rank/game','system:rank:channel','Rank'), +('GET','/system/gameData/list','/sw/gameData/list','system:gameData:list','GameData'), +('GET','/system/channelData/list','/sw/channelData/list','system:channelData:list','ChannelData'), +('GET','/system/register/list','/sw/register/list','system:register:list','Register'), +('POST','/system/register/export','/sw/register/export','system:register:export','Register'), +('GET','/system/spend/list','/sw/spend/list','system:spend:list','Spend'), +('POST','/system/spend/export','/sw/spend/export','system:spend:export','Spend'), +('GET','/system/serverAnalysis/games','/sw/serverAnalysis/games','system:serverAnalysis:list','ServerAnalysis'), +('GET','/system/serverAnalysis/servers','/sw/serverAnalysis/servers','system:serverAnalysis:list','ServerAnalysis'), +('GET','/system/serverAnalysis/list','/sw/serverAnalysis/list','system:serverAnalysis:list','ServerAnalysis'), +('GET','/system/alertRule/list','/sw/alertRule/list','system:alertRule:list','SysAlertRule'), +('GET','/system/alertRule/ruleTypes','/sw/alertRule/ruleTypes','system:alertRule:list','SysAlertRule'), +('GET','/system/alertRule/{id}','/sw/alertRule/:id','system:alertRule:query','SysAlertRule'), +('POST','/system/alertRule/add','/sw/alertRule/add','system:alertRule:add','SysAlertRule'), +('PUT','/system/alertRule/edit','/sw/alertRule/edit','system:alertRule:edit','SysAlertRule'), +('DELETE','/system/alertRule/{id}','/sw/alertRule/:id','system:alertRule:remove','SysAlertRule'), +('PUT','/system/alertRule/toggle/{id}','/sw/alertRule/toggle/:id','system:alertRule:edit','SysAlertRule'), +('GET','/system/alertRecord/list','/sw/alertRecord/list','system:alertRecord:list','SysAlertRecord'), +('GET','/system/alertRecord/detail/{id}','/sw/alertRecord/detail/:id','system:alertRecord:query','SysAlertRecord'), +('POST','/system/alertRecord/startProcess','/sw/alertRecord/startProcess','system:alertRecord:process','SysAlertRecord'), +('POST','/system/alertRecord/submitAudit','/sw/alertRecord/submitAudit','system:alertRecord:process','SysAlertRecord'), +('POST','/system/alertRecord/approve','/sw/alertRecord/approve','system:alertRecord:audit','SysAlertRecord'), +('POST','/system/alertRecord/reject','/sw/alertRecord/reject','system:alertRecord:audit','SysAlertRecord'), +('POST','/system/alertRecord/reProcess','/sw/alertRecord/reProcess','system:alertRecord:process','SysAlertRecord'), +('POST','/system/alertRecord/addNote','/sw/alertRecord/addNote','system:alertRecord:process','SysAlertRecord'), +('GET','/system/user/list','/sw/business/list','system:user:list','SysUser'), +('GET','/system/user/{userId}/promotes','/sw/business/:userId/promotes','system:user:query','SysUser'), +('GET','/system/user/{userId}/portrait','/sw/business/:userId/portrait','system:user:query','SysUser'), +('GET','/system/user/{userId}/bindPlatform','/sw/business/:userId/bindPlatform','system:user:query','SysUser'), +('POST','/system/user/bindPlatform','/sw/business/bindPlatform','system:user:bind','SysUser'), +('GET','/system/user/bindPlatform','/sw/business/bindPlatform','system:user:query','SysUser'), +('GET','/system/user/bindPlatform/portrait','/sw/business/bindPlatform/portrait','system:user:query','SysUser'), +('DELETE','/system/user/bindPlatform','/sw/business/bindPlatform','system:user:bind','SysUser'), +('POST','/system/user/export','/sw/business/export','system:user:export','SysUser'), +('POST','/system/user/importData','/sw/business/importData','system:user:import','SysUser'), +('GET','/system/user/','/sw/business','system:user:query','SysUser'), +('GET','/system/user/{userId}','/sw/business/:userId','system:user:query','SysUser'), +('POST','/system/user','/sw/business','system:user:add','SysUser'), +('PUT','/system/user','/sw/business','system:user:edit','SysUser'), +('DELETE','/system/user/{userIds}','/sw/business/:userIds','system:user:remove','SysUser'), +('GET','/system/user/optionselect','/sw/business/optionselect','system:user:query','SysUser'), +('PUT','/system/user/resetPwd','/sw/business/resetPwd','system:user:resetPwd','SysUser'), +('PUT','/system/user/changeStatus','/sw/business/changeStatus','system:user:edit','SysUser'), +('GET','/system/user/unlock/{userId}','/sw/business/unlock/:userId','system:user:edit','SysUser'), +('GET','/system/user/authRole/{userId}','/sw/business/authRole/:userId','system:user:query','SysUser'), +('PUT','/system/user/authRole','/sw/business/authRole','system:user:edit','SysUser'), +('GET','/system/user/deptTree','/sw/business/deptTree','system:user:list','SysUser'), +('GET','/system/user/list/dept/{deptId}','/sw/business/list/dept/:deptId','system:user:list','SysUser'); + +INSERT INTO sys_apis(created_at,updated_at,path,description,api_group,method) SELECT NOW(3),NOW(3),target_path,CONCAT('SW ',controller,' - Go handler pending'),'SW business',method FROM sw_kra_api_contract; + +INSERT INTO casbin_rule(ptype,v0,v1,v2,v3,v4,v5) SELECT DISTINCT 'p',CAST(rm.new_id AS CHAR),c.target_path,c.method,'','','' FROM sw_kra_api_contract c JOIN sw_kra_menu_permissions p ON p.source_permission=c.source_permission JOIN sys_role_menu x ON x.menu_id=p.source_menu_id JOIN sw_kra_id_map rm ON rm.entity='role' AND rm.old_id=CAST(x.role_id AS CHAR); + +INSERT INTO casbin_rule(ptype,v0,v1,v2,v3,v4,v5) SELECT 'p','888',target_path,method,'','','' FROM sw_kra_api_contract; + +INSERT INTO casbin_rule(ptype,v0,v1,v2,v3,v4,v5) SELECT 'p',CAST(a.authority_id AS CHAR),p.path,p.method,'','','' FROM sys_authorities a JOIN sys_apis p ON p.path IN('/menu/getMenu','/user/getUserInfo','/user/setSelfInfo','/user/setSelfSetting','/user/setUserAuthority','/user/changePassword') WHERE a.authority_id<>888; + +INSERT INTO casbin_rule(ptype,v0,v1,v2,v3,v4,v5) SELECT 'p',CAST(a.authority_id AS CHAR),c.target_path,c.method,'','','' FROM sys_authorities a JOIN sw_kra_api_contract c ON c.source_permission='__authenticated__' WHERE a.authority_id<>888; + +-- 七、转换旧任务定义,保留超时和重试参数;当前 Go 执行器读取 _execution,任务仍保持停用。 + +INSERT INTO sys_timed_tasks(id,created_at,updated_at,name,description,spec,with_seconds,executor_type,method_name,params,http_allow_private,enabled) SELECT m.new_id,j.create_dt,j.update_dt,CONCAT('SW/',j.job_name),j.description,CASE j.trigger_type WHEN 3 THEN CONCAT('CRON_TZ=Asia/Shanghai ',TRIM(j.trigger_interval)) WHEN 2 THEN CONCAT('@every ',TRIM(j.trigger_interval),'s') END,j.trigger_type=3,'method',j.executor_info,JSON_SET(IF(j.args_str IS NULL OR j.args_str='',JSON_OBJECT(),CAST(j.args_str AS JSON)),'$._execution',JSON_OBJECT('timeoutSeconds',j.executor_timeout,'maxRetryTimes',j.max_retry_times,'retryIntervalSeconds',j.retry_interval)),0,0 FROM sj_job j JOIN sw_kra_id_map m ON m.entity='task' AND m.old_id=CAST(j.id AS CHAR) WHERE j.deleted=0; + +-- 八、回填业务表中的用户及部门引用,保留业务主键和平台实体 ID。 + +UPDATE `sys_user_bind_platform` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`user_id` AS CHAR) SET x.`user_id`=m.new_id; + +UPDATE `sys_user_preference` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`user_id` AS CHAR) SET x.`user_id`=m.new_id; + +-- 显式保留更新时间,防止旧表 ON UPDATE CURRENT_TIMESTAMP 把 ID 转换当作业务编辑。 +UPDATE `sys_alert_rule` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`creator_id` AS CHAR) SET x.`creator_id`=m.new_id,x.`updated_at`=x.`updated_at`; + +UPDATE `sys_alert_record` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`business_id` AS CHAR) SET x.`business_id`=m.new_id,x.`updated_at`=x.`updated_at`; + +UPDATE `sys_alert_operation_log` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`operator_id` AS CHAR) SET x.`operator_id`=m.new_id; + +UPDATE `sys_message` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`create_by` AS CHAR) SET x.`create_by`=m.new_id; + +UPDATE `sys_message` x JOIN sw_kra_id_map m ON m.entity='user' AND m.old_id=CAST(x.`update_by` AS CHAR) SET x.`update_by`=m.new_id; + +UPDATE sys_message x JOIN sw_kra_id_map m ON m.entity='dept' AND m.old_id=CAST(x.create_dept AS CHAR) SET x.create_dept=m.new_id; + +BEGIN DECLARE done INT DEFAULT 0; DECLARE old_user VARCHAR(64); DECLARE new_user BIGINT UNSIGNED; DECLARE users_cursor CURSOR FOR SELECT old_id,new_id FROM sw_kra_id_map WHERE entity='user' ORDER BY id; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1; OPEN users_cursor; user_loop: LOOP FETCH users_cursor INTO old_user,new_user; IF done=1 THEN LEAVE user_loop; END IF; UPDATE sys_message SET send_user_ids=TRIM(BOTH ',' FROM REPLACE(CONCAT(',',BINARY send_user_ids,','),CONCAT(',',BINARY old_user,','),CONCAT(',',BINARY new_user,','))) WHERE FIND_IN_SET(BINARY old_user,BINARY send_user_ids)>0; END LOOP; CLOSE users_cursor; END; + +-- 九、核验账号密码、关联、业务引用、菜单和任务;不通过则回滚当前数据事务。 + +SELECT COUNT(*) INTO v_actual FROM sys_alert_rule n JOIN bak_sw2kra_20261103_sys_alert_rule o ON o.id=n.id WHERE NOT(n.created_at <=> o.created_at) OR NOT(n.updated_at <=> o.updated_at); +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','alert rule timestamps unchanged',0,v_actual,v_actual=0); +IF v_actual<>0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='alert rule timestamps changed'; END IF; + +SELECT COUNT(*) INTO v_actual FROM sys_alert_record n JOIN bak_sw2kra_20261103_sys_alert_record o ON o.id=n.id WHERE NOT(n.created_at <=> o.created_at) OR NOT(n.updated_at <=> o.updated_at); +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','alert record timestamps unchanged',0,v_actual,v_actual=0); +IF v_actual<>0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='alert record timestamps changed'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_users) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM sys_user WHERE del_flag='0') INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','user count',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='user count'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_users n JOIN sw_kra_id_map m ON m.entity='user' AND m.new_id=n.id JOIN sys_user o ON CAST(o.user_id AS CHAR)=m.old_id WHERE BINARY n.password=BINARY o.password) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM sys_user WHERE del_flag='0') INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','password bytes unchanged',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='password bytes unchanged'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_users n LEFT JOIN sys_authorities a ON a.authority_id=n.authority_id WHERE a.authority_id IS NULL) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','current role references',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='current role references'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_user_authority) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM sys_user_role) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','user role links',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='user role links'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_users n JOIN sw_kra_id_map m ON m.entity='user' AND m.new_id=n.id JOIN sys_user o ON CAST(o.user_id AS CHAR)=m.old_id LEFT JOIN sw_kra_id_map p ON p.entity='user' AND p.old_id=CAST(o.parent_id AS CHAR) WHERE n.parent_id<>COALESCE(p.new_id,0) OR n.level<>o.level) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','business parent and level',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='business parent and level'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_users u JOIN sys_users p ON p.id=u.parent_id WHERE u.id_path<>CONCAT(p.id_path,',',p.id)) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','business ancestor paths',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='business ancestor paths'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_user_bind_platform b LEFT JOIN sys_users u ON u.id=b.user_id WHERE u.id IS NULL) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','platform binding users',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='platform binding users'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_user_bind_platform) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_bind_platform`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','platform binding rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='platform binding rows'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_user_preference p LEFT JOIN sys_users u ON u.id=p.user_id WHERE u.id IS NULL) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','preference users',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='preference users'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_user_preference) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_user_preference`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','preference rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='preference rows'; END IF; + +SELECT (SELECT COUNT(*) FROM media_file_upload_and_downloads) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM sys_oss) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','file rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='file rows'; END IF; + +SELECT (SELECT COUNT(*) FROM media_file_upload_and_downloads n JOIN sw_kra_id_map m ON m.entity='file' AND m.new_id=n.id JOIN sys_oss o ON CAST(o.oss_id AS CHAR)=m.old_id WHERE n.size<>COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.fileSize')) AS UNSIGNED),0) OR BINARY n.mime<>BINARY COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.contentType')),'null'),'') OR BINARY n.md5<>BINARY COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(IF(JSON_VALID(o.ext1),o.ext1,JSON_OBJECT()),'$.md5')),'null'),'')) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','file size MIME and MD5',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='file size MIME and MD5'; END IF; + +SELECT (SELECT COUNT(*) FROM sw_kra_id_map WHERE entity='menu') INTO v_actual; + +SELECT (31) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','business source menu nodes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='business source menu nodes'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_base_menus n JOIN sw_kra_id_map m ON m.entity='menu' AND m.new_id=n.id) INTO v_actual; + +SELECT (14) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','business navigation nodes',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='business navigation nodes'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_base_menu_btns) INTO v_actual; + +SELECT (17) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','business buttons',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='business buttons'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_base_menus WHERE name='sw_workbench' OR path='sw-index') INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','workbench reuses dashboard',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='workbench reuses dashboard'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_authorities a WHERE NOT EXISTS(SELECT 1 FROM sys_base_menus m JOIN sys_authority_menus x ON x.sys_base_menu_id=m.id WHERE x.sys_authority_authority_id=a.authority_id AND m.name=a.default_router)) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','default routers accessible',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='default routers accessible'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_base_menus n LEFT JOIN sys_base_menus p ON p.id=n.parent_id WHERE n.parent_id<>0 AND p.id IS NULL) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','menu parent references',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='menu parent references'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_timed_tasks t JOIN sw_kra_id_map m ON m.entity='task' AND m.new_id=t.id) INTO v_actual; + +SELECT (7) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','SW task definitions',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='SW task definitions'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_timed_tasks WHERE enabled=1) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','all tasks disabled for rehearsal',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='all tasks disabled for rehearsal'; END IF; + +SELECT (SELECT COUNT(*) FROM sys_message) INTO v_actual; + +SELECT (SELECT COUNT(*) FROM `bak_sw2kra_20261103_sys_message`) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','message rows',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='message rows'; END IF; + +SELECT (SELECT COUNT(*) FROM (SELECT code FROM sys_positions GROUP BY code HAVING COUNT(*)>1) duplicate_codes) INTO v_actual; + +SELECT (0) INTO v_expected; + +INSERT INTO sw_kra_checks(stage,check_name,expected_value,actual_value,passed) VALUES ('migration','position codes unique',v_expected,v_actual,v_expected=v_actual); + +IF v_expected <> v_actual THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='position codes unique'; END IF; + +UPDATE sw_kra_state SET phase='data_validated' WHERE id=1; + +COMMIT; + +-- 十、数据核验通过后归档旧框架表,登记完成状态并释放迁移锁。 + +RENAME TABLE +`sys_social` TO `legacy_sw_20261103_sys_social`, +`sys_dept` TO `legacy_sw_20261103_sys_dept`, +`sys_user` TO `legacy_sw_20261103_sys_user`, +`sys_post` TO `legacy_sw_20261103_sys_post`, +`sys_role` TO `legacy_sw_20261103_sys_role`, +`sys_menu` TO `legacy_sw_20261103_sys_menu`, +`sys_user_role` TO `legacy_sw_20261103_sys_user_role`, +`sys_role_menu` TO `legacy_sw_20261103_sys_role_menu`, +`sys_role_dept` TO `legacy_sw_20261103_sys_role_dept`, +`sys_user_post` TO `legacy_sw_20261103_sys_user_post`, +`sys_oper_log` TO `legacy_sw_20261103_sys_oper_log`, +`sys_dict_type` TO `legacy_sw_20261103_sys_dict_type`, +`sys_dict_data` TO `legacy_sw_20261103_sys_dict_data`, +`sys_config` TO `legacy_sw_20261103_sys_config`, +`sys_login_info` TO `legacy_sw_20261103_sys_login_info`, +`sys_notice` TO `legacy_sw_20261103_sys_notice`, +`sys_oss` TO `legacy_sw_20261103_sys_oss`, +`sys_oss_config` TO `legacy_sw_20261103_sys_oss_config`, +`sys_client` TO `legacy_sw_20261103_sys_client`; + +UPDATE sw_kra_state SET phase='completed',completed_at=NOW(3) WHERE id=1; + +DO RELEASE_LOCK(v_lock_name); SET v_lock=0; +SELECT '完整框架迁移完成' AS result; +SELECT stage,COUNT(*) AS checks,SUM(passed) AS passed FROM sw_kra_checks GROUP BY stage; +END migration_run$$ +-- 只调用这一个入口,由库的实际状态决定执行完整迁移还是菜单调整。 +CALL sw_kra_migrate_20261103()$$ +DROP PROCEDURE sw_kra_migrate_20261103$$ +DELIMITER ; +-- 输出最终根菜单顺序,便于核对页面效果。 +SELECT id,name,title,sort FROM sys_base_menus WHERE parent_id=0 ORDER BY sort,id; diff --git a/docs/OA_MIGRATION_PLAN.md b/docs/OA_MIGRATION_PLAN.md deleted file mode 100644 index 772c114..0000000 --- a/docs/OA_MIGRATION_PLAN.md +++ /dev/null @@ -1,1355 +0,0 @@ -# OA 到 KRA 完整迁移执行计划 - -> 计划基线日期:2026-08-21 -> 源系统:`oa` -> 目标系统:`kra-new` -> 当前状态:仅完成代码盘点与迁移规划,尚未开始业务迁移 - -## 1. 目标与结论 - -目标是在 **不重构现有移动端、PC 端和旧后台调用协议** 的前提下,将 OA 的完整系统能力迁移到 KRA,并最终由 KRA 统一承载: - -- 管理后台接口与页面; -- 会长/渠道移动端接口(`/tg`); -- 玩家及客服端接口(`/imserver`); -- 对外 SDK 接口(`/sdk`); -- 游戏、渠道、玩家、订单、扶持、福利、结算、IM、分发、报表等业务; -- 中间件、定时任务、多数据库、Redis、消息队列、对象存储和第三方服务; -- 数据初始化、历史数据迁移、部署和回滚能力。 - -盘点后的结论是:**KRA 可以承载 OA,但不能直接复制 OA 目录或模型。** 两边虽然都保留了 GVA 风格接口,但后端分层、系统表、鉴权方式和任务机制已经不同。迁移必须以 KRA 的分层、模块目录、系统表和运行时为主,通过 OA 兼容 DTO、兼容中间件和显式数据迁移保持旧客户端协议不变。 - -## 2. 当前基线 - -以下数字用于评估工作量,不代表最终有效接口数;正式迁移前仍需从运行时路由和实际客户端请求中去重、排除未注册代码。 - -| 项目 | OA | KRA 当前状态 | -|---|---:|---:| -| 路由注册语句 | 约 756 | 约 191 | -| System 模型文件 | 32 | 已有完整基础后台模型 | -| Game 模型文件 | 52 | 未迁移 | -| User 玩家模型文件 | 12 | 未迁移 | -| IM 模型文件 | 16 | 仅有通用 WebSocket 基础设施 | -| Issue 模型文件 | 4 | 未迁移 | -| Report 模型文件 | 2 | 未迁移 | -| SW 模型文件 | 4 | 未迁移 | -| OA 管理端 API 文件 | 57 | 仅 KRA 自身后台 API | - -OA 路由注册语句按原目录的粗略分布: - -| 原路由域 | 注册语句数 | 主要用途 | -|---|---:|---| -| `system` | 198 | 用户、角色、部门、平台、配置、日志、黑名单等 | -| `game` | 303 | 游戏、渠道、订单、扶持、福利、结算、打包等 | -| `user` | 60 | 玩家、角色、补单、发放、协议、短信等 | -| `tg` | 49 | 会长/渠道移动端 | -| `im` | 32 | 管理后台客服 IM | -| `imserver` | 22 | 玩家端 IM 与回调 | -| `sdk` | 18 | 对外签名接口 | -| `issue` | 28 | 分发系统 | -| `report` | 2 | 报表 | -| `sw` | 12 | 商务用户与平台绑定 | -| `common` / `example` | 32 | 公共下载、上传及遗留工具/示例 | - -## 3. 不可变更的迁移规则 - -1. **KRA 是唯一目标代码库。** 不长期保留 OA 与 KRA 两套业务实现。 -2. **KRA 系统表优先。** KRA 已有语义等价表时,不再创建 OA 同义系统表;OA 多出的字段增量加入 KRA 表。 -3. **OA 独有业务表保留业务语义。** 没有 KRA 对应物的业务表可迁入模块,但必须进入 KRA 的迁移体系和数据层。 -4. **旧客户端不改协议。** HTTP 方法、路径、参数来源、字段名、字段类型、默认值、空值形态、响应码、消息、Header 和下载行为均以旧接口契约为准。 -5. **后台能力同步迁移。** OA 有而 KRA 没有的管理功能,必须同时迁移后端接口、KRA 后台页面、菜单、按钮权限和 API 权限数据。 -6. **接口域不能混用鉴权。** 管理后台、会长端、玩家端、SDK 和第三方回调必须使用各自的 token、签名和上下文。 -7. **逐文件核对。** 每一个 OA 源文件必须被标记为“已迁移、被 KRA 替代、确认废弃”之一,不允许无记录遗漏。 -8. **逐接口差异测试。** 不以“页面能打开”作为完成标准,必须比较响应字段和业务副作用。 -9. **只做显式数据库迁移。** 禁止用无审查的 `AutoMigrate` 修改生产旧表。 -10. **迁移定义永久保留。** 已执行的 migration ID 不得从 KRA Catalog 删除或改名。 - -## 4. 目标模块结构 - -业务模块建议使用独立目录,并在模块内部继续遵守 KRA 的 `service -> biz -> data` 分层: - -```text -internal/modules// - biz/ # 领域对象、状态机、用例、仓储接口 - data/ # PO、GORM 查询、事务、数据转换、迁移 - service/ # OA 请求/响应 DTO 与领域对象转换 - server/handler/ # Gin handler - server/router/ # 路由与接口域注册 - worker/ # 本模块定时任务注册 - definition.go # migrations / menus / APIs / default tasks - providers.go # Wire ProviderSet - -web/src/modules// - api/ - view/ - components/ -``` - -修改 KRA 既有系统表和系统行为的内容仍放在现有 `internal/biz/system`、`internal/data/system`、`internal/service`、`internal/server` 中,避免创建第二套用户、角色、部门和权限系统。 - -每个业务模块通过 KRA 现有的 `pkg/module` 机制贡献: - -- 数据库 migration; -- 后台菜单和 API 元数据; -- HTTP 路由; -- 默认定时任务; -- 运行时任务方法。 - -## 5. 接口域与鉴权边界 - -KRA 当前只有公共组和统一私有组,不足以直接表达 OA 的全部鉴权语义。基础阶段需要建立命名清晰的接口域: - -| 接口域 | 主要路径 | 客户端 | 必要鉴权/上下文 | -|---|---|---|---| -| Admin Public | `/base/*`、`/init/*`、公共下载 | 后台登录页 | 无登录、验证码、限流 | -| Admin Login | 菜单、用户信息、部分通讯录/IM | 后台端 | KRA/OA 兼容 JWT,不强制 Casbin | -| Admin RBAC | 系统管理和一般业务接口 | 后台端 | JWT + 多角色 Casbin + 操作审计 | -| Admin Business DB | 游戏、渠道、玩家、订单等 | 后台端 | JWT + Casbin + 数据库/平台范围 | -| TG | `/tg/*` | 移动会长端 | 管理用户 JWT + 当前渠道/平台上下文 + 层级权限 | -| IM Player Public | `/imserver/play/register*`、登录、回调 | 移动/PC 玩家端、腾讯回调 | 无登录或回调校验 | -| IM Player Auth | `/imserver/play/*` 登录后接口 | 移动/PC 玩家端 | 玩家专用 JWT,不能复用后台 claims | -| SDK Signed | `/sdk/*` | 游戏 SDK/外部服务 | OA 签名算法、重放保护、调用审计 | -| Callback | IM、支付、打包等回调 | 第三方系统 | 各供应商签名、幂等、原始 ACK | - -基础路由改造完成前,不得批量接入业务路由。 - -## 6. 系统表兼容策略 - -### 6.1 兼容结论 - -KRA 与 OA 的核心 GVA 表存在较高重合,可以兼容,但部门、公司、平台和扩展用户字段不能直接照搬。 - -| OA 表/能力 | KRA 目标 | 处理原则 | -|---|---|---| -| `sys_users` | `sys_users` | 保留 KRA 字段,增加 OA 的实名、主题、IM、注销、用户中心、用户类型等字段 | -| `sys_authorities` | `sys_authorities` | 保留 KRA `data_scope`,增加 OA 的启用、公司管理员、部门范围、JSON 配置等字段 | -| `sys_base_menus` | `sys_base_menus` | KRA ID/父 ID 类型为准;旧接口 DTO 负责输出 OA 需要的字段和类型 | -| `sys_apis` | `sys_apis` | 保留 KRA API 权限模型;补 OA 实际使用的菜单关联等字段 | -| `sys_user_authority` | `sys_user_authority` | 保留多角色;扩展 JWT 和 Casbin 为“任一角色通过即授权” | -| `sys_authority_menus` | `sys_authority_menus` | 使用 KRA 数值菜单 ID,迁移时转换 OA 旧值 | -| `sys_depart` | `sys_departments` | 只保留 KRA 表;`depart_name -> name`,补 OA 组织类别、客服、公司、企业微信等字段 | -| `sys_user_depart` | `sys_user_departments` | 转换表名和外键列名,不保留两套关系表 | -| OA 数据权限关系 | `sys_authority_departments` + KRA `data_scope` | 将 OA 部门/公司范围转换为 KRA 数据范围,同时保留旧接口表现 | -| `sys_operation_records` | `sys_operation_records` | 合并字段并保持 OA 查询响应兼容 | -| 字典表 | KRA 同名表 | 直接映射,核对唯一键、删除语义和返回字段 | -| `sys_config` | `sys_params` / `sys_integration_configs` / 模块配置表 | 按配置用途拆分,旧 `/sysConfig` API 作为兼容门面 | -| OA 独有平台、公司、黑名单、活跃池等表 | KRA 系统扩展表 | 没有等价结构时新增,但由 KRA 模块拥有和迁移 | - -### 6.2 数据库迁移硬性要求 - -KRA 当前的 `CreateMissingTables` 明确不会修改已存在表,因此必须先补充一套经过测试的显式增量 migration 能力: - -- `AddColumnIfMissing`; -- `CreateIndexIfMissing`; -- 可审计的数据回填步骤; -- 表名/列名转换脚本; -- 大表分批迁移和断点续传; -- 每个步骤可重复执行且结果一致; -- 迁移前后行数、主键范围、空值数、金额合计和关键关系校验; -- 禁止启动时自动删除列、改列类型或重建大表。 - -历史数据迁移默认保留原主键。确实不能保留时,必须建立永久 ID 映射表,并同步更新所有外键、缓存键和外部引用。 - -## 7. 每个模块必须生成的四份清单 - -每个模块在 `docs/migration//` 下维护以下文件,缺一不可: - -1. `source-ledger.md`:OA 源文件逐一映射到 KRA 目标文件,记录迁移/替代/废弃结论。 -2. `schema-map.md`:表、字段、类型、默认值、索引、关系、状态值和历史数据转换规则。 -3. `api-contract.md`:方法、路径、接口域、请求字段、响应字段、Header、错误码、消息和副作用。 -4. `verification.md`:单测、集成测试、差异测试、前端回归、数据核对结果及获批差异。 - -## 8. 总体任务阶段 - -### 阶段 0:基线冻结与自动核对工具 - -#### MIG-000 源代码与运行时清单冻结 - -- 从 OA 实际启动后的 `engine.Routes()` 导出有效路由,不能只统计路由文件。 -- 扫描 OA 管理端 57 个 API 文件及所有页面调用。 -- 收集移动端、PC 端、后台端当前发布版本的请求样本或网关日志。 -- 将所有 OA model/api/router/service/middleware/timer/config/frontend 文件登记到总 source ledger。 -- 标记未注册路由、注释代码、一次性脚本和 GVA 示例功能,但未经确认不得删除。 - -完成标准:OA 所有源文件和有效接口均有唯一编号,未分类数量为 0。 - -#### MIG-001 接口差异测试框架 - -- 使用同一份脱敏数据分别启动 OA 和 KRA。 -- 建立请求语料库,向两边发送相同请求。 -- 比较 HTTP 状态、Header、`code/data/msg`、字段存在性、JSON 类型、`null/[]/{}`、排序和分页。 -- 对时间、token、随机 ID 等非确定字段只做规则化处理,不允许整段忽略。 -- 比较数据库写入、Redis 键、消息队列消息和第三方适配器调用。 -- 输出机器可读差异报告和人工批准的 exception ledger。 - -完成标准:框架能对至少登录、菜单、用户列表、一个业务 CRUD 和一个下载接口出具差异报告。 - -#### MIG-002 模块脚手架与持续验证 - -- 建立 OA 业务模块目录和统一 ProviderSet/Definition/Routes/Tasks 接入方式。 -- 扩展 `internal/app`、Wire 和 module runtime,使新增模块无需修改系统业务实现。 -- 在持续检查中加入 `go test ./...`、`go vet ./...`、`go build ./...`、前端 lint/build 和契约测试。 -- 增加路由冲突、migration ID 冲突、菜单/API 重复和跨层 import 检查。 - -完成标准:空示例模块可独立贡献表、路由、菜单和定时任务,并通过完整构建。 - -### 阶段 1:KRA 兼容底座 - -#### FND-001 显式增量数据库迁移 - -- 实现第 6.2 节的增列、索引、回填和校验工具。 -- 增加 MySQL、PostgreSQL、SQL Server、Oracle、SQLite 的方言测试;生产实际使用的数据库优先达到完整覆盖。 -- 增加 dry-run/检查模式和失败后的恢复说明。 - -#### FND-002 OA 请求与响应兼容层 - -- 支持 OA `PageResult` 和 `OaPageResult`,包括 `maxCount`、`fialData`、`extend` 等历史字段。 -- 保持旧接口成功/失败 `code`、`msg` 和 HTTP 状态。 -- 兼容 `new-token`、`new-expires-at`、文件下载 Header 和 OA 上传响应。 -- 固化时间格式、金额精度、布尔/整数状态、空集合和软删除字段的 JSON 表现。 -- 共享路径发生 KRA/OA 契约冲突时,以仍在使用的旧客户端契约为准,并登记 KRA 侧调整。 - -#### FND-003 多接口域与中间件 - -- 建立第 5 节的命名路由域。 -- 扩展后台 JWT claims:多角色、手机号、公司、管理员标识等 OA 必需上下文。 -- Casbin 改为多角色任一命中;保留 `888` 超级管理员旁路。 -- 迁移/重写 `DbHandler`、`TgAuth`、`SignAuth`、操作记录、错误审计、限流、CORS 和初始化保护。 -- 对签名和回调增加时间窗、nonce/幂等键,保持旧签名算法输入输出不变。 - -#### FND-004 多数据库、公司、平台上下文 - -- 复用 KRA `database_list`,统一按 alias 获取业务库。 -- 将 OA 请求体中的 `dbName`、当前公司、当前平台和角色可见数据库解析为显式 request context。 -- 禁止业务层读取全局 DB 或直接从 Gin Context 取值。 -- 对单库、跨库查询、批量多库请求和无权限库建立测试。 - -#### FND-005 外部集成基础设施 - -- 直接复用 KRA 已有 Redis/cache、对象存储、邮件、支付、WebSocket 能力。 -- 补齐 RabbitMQ AMQP 运行时客户端;KRA 当前只有 RabbitMQ 配置定义,没有 OA 客服队列所需的完整 AMQP 生命周期实现。 -- 新增腾讯 IM、用户中心、短信、OCR、游戏业务站、PC 分包、契约/企业签及各 CP 扶持接口适配器。 -- 所有客户端放入 `internal/integration`,业务模块只依赖接口,不复制 OA 的全局单例和 `utils` 网络调用。 -- 所有密钥从安全配置注入,禁止复制 OA 配置文件中的明文凭据;上线前完成旧凭据轮换。 - -#### FND-006 历史数据迁移程序 - -- 建立独立迁移命令,支持源 OA 库到目标 KRA 库的结构检查、分批导入、续跑和校验。 -- 支持主系统库及 `db-list` 中各业务库。 -- 所有转换规则来源于各模块 `schema-map.md`。 -- 迁移日志不得打印密码、身份证、token、签名密钥等敏感值。 - -### 阶段 2:KRA 系统能力合并 - -#### SYS-001 用户、登录与账号生命周期 - -范围:账号密码登录、手机号登录、会长登录、注册、忘记密码、用户列表、个人信息、多角色、部门、公司子账号、切换公司、黑名单、注销、实名认证、常用菜单和 IM 客服字段。 - -子任务: - -- `SYS-001-A`:扩展 KRA `sys_users` DO/PO/repo 和显式 migration。 -- `SYS-001-B`:迁移 OA 登录、注册、用户中心同步、JWT claims 和 token 刷新行为。 -- `SYS-001-C`:补齐所有 `/base/*`、`/user/*` 兼容接口。 -- `SYS-001-D`:迁移 KRA 后台用户页面缺少的字段、筛选、批量操作和公司账户功能。 -- `SYS-001-E`:逐文件对照 OA `sys_user` model/request/response/api/service/router 及相关中间件。 - -#### SYS-002 角色、菜单、API、按钮和数据权限 - -- 合并 OA 角色扩展字段和多角色鉴权。 -- 对齐菜单树、默认路由、参数、按钮权限、API 权限和 Casbin 策略。 -- 迁移角色与平台、角色与部门/公司范围关系。 -- 验证超级管理员、普通角色、冻结角色、多角色用户和无菜单用户。 -- 同步 KRA 后台角色、菜单、API、按钮和成员分配页面。 - -#### SYS-003 部门、公司、岗位和通讯录 - -- 将 `sys_depart` 数据转换到 `sys_departments`,补齐 OA 字段。 -- 将 `sys_user_depart` 转换到 `sys_user_departments`。 -- 明确 OA 的角色、岗位、部门三种概念,禁止再次混用。 -- 迁移部门树、顶级公司、子公司、客服部门、账号数量、批量分配和通讯录接口。 -- 验证移动/删除部门后的祖先链、公司范围和孤儿关系。 - -#### SYS-004 系统配置、日志、文件、导出与遗留工具 - -- 迁移 `sysConfig` 兼容门面、操作日志、用户行为日志、黑名单、上传下载和 Excel 导出。 -- 复用 KRA 安全配置、登录日志、错误日志、数据访问日志、媒体库和附件分类。 -- 逐项评估 OA 代码生成器、断点续传、客户示例等遗留功能:仍被菜单/API/用户使用则迁移;确认不再使用后才进入废弃清单。 -- 对 OA 有而 KRA 没有的后台入口补齐菜单、API 元数据、按钮权限和页面。 - -### 阶段 3:平台、游戏与渠道主数据 - -#### PLT-001 联运平台、公司与商务身份 - -范围:`sys_platform`、角色平台关系、公司关系、CP session/support/gift、SW 用户/角色/偏好/平台绑定、商务用户管理。 - -依赖:`SYS-001`、`SYS-002`、`SYS-003`、`FND-004`。 - -#### GAM-001 游戏目录与基础配置 - -范围:`tab_game`、游戏类型、CP、游戏设置、原包、区服、合服、互通、禁用配置、改名日志、APP、平台配置、支付配置、系统选项和企业签日志。 - -重点验收: - -- 游戏改名涉及的所有冗余字段和日志; -- 配置 JSON、状态值和金额字段; -- 业务库选择; -- 后台列表筛选、详情、导出、配置页和按钮权限; -- OA 游戏相关源文件逐一完成 ledger。 - -#### CHN-001 渠道、商务、合同与公告 - -范围:`tab_promote`、商务归属、推广配置、登录记录、合同、公告、押金、平台币、渠道状态和上下级关系。 - -重点验收:渠道审核、驳回、商务改派、结算周期修改、不可推广游戏、层级关系、锁定/解锁和冗余快照更新。 - -#### PKG-001 渠道申请与多端打包 - -范围:`tab_promote_apply`、`tab_promote_app`、Android/iOS/H5/超级签/PC 打包、快手抖音配置、分享显示和隐藏游戏。 - -依赖:`GAM-001`、`CHN-001`、外部分包适配器。 - -重点验收: - -- `-1/0/1/2/3` 打包状态机; -- 原包与渠道申请关系; -- PC 异步提交、轮询、失败恢复和幂等; -- 旧移动端/PC 端下载 URL 和响应字段; -- KRA 后台打包管理页面。 - -### 阶段 4:玩家、订单、扶持与财务 - -#### PLY-001 玩家、角色与账号关联 - -范围:`tab_user`、登录记录、角色、角色详情、补链、补单、协议、短信日志、批量玩家、敏感字段查看和脱敏。 - -重点验收:渠道归因、跨库查询、敏感信息权限/审计、余额修改、补链跨表更新和导出字段。 - -#### ORD-001 充值、绑币、平台币与交易记录 - -范围:`tab_spend`、`tab_spend_bind`、`tab_spend_provide`、余额、渠道币、订单绑定、补单、支付/游戏通知状态、企业签日志和代金券记录。 - -依赖:`PLY-001`、`GAM-001`、`CHN-001`。 - -重点验收:金额精度、订单唯一性、重复回调、补单幂等、游戏到账、渠道归因、结算明细生成和导出。 - -#### SUP-001 扶持、福利与 CP 发放 - -范围:扶持申请、渠道扶持、禁止扶持、倍数/清零、福利配置/道具/记录、CP 礼包、自动审核与自动发放。 - -依赖:`PLY-001`、`GAM-001`、`CHN-001`。 - -重点验收:审核状态机、实际到账、失败补偿、外部 CP 调用、批量发放、周/月卡重复执行保护和后台审核页面。 - -#### FIN-001 结算、提现、代充与预支 - -范围:渠道结算、结算时间、周期单、提现、押金、代充折扣、绑币订单、预支白名单/游戏/额度/还款及相关日志。 - -依赖:`ORD-001`、`CHN-001`。 - -重点验收:结算汇总、周期确认、余额扣增、重复付款防护、跨表事务边界、提现状态和预支额度恢复。 - -### 阶段 5:移动端、PC 端、IM 与外部接口 - -#### TG-001 会长/渠道移动端完整接口 - -范围:全部 `/tg/promote`、`/tg/app`、`/tg/promoteApply`、`/tg/support` 接口。 - -依赖:`GAM-001`、`CHN-001`、`PKG-001`、`PLY-001`、`ORD-001`、`SUP-001`、`FIN-001`。 - -重点验收:平台切换、渠道层级菜单限制、首页统计、结算确认、提现、代充、福利、预支和移动端字段类型。 - -#### IM-001 管理后台客服 IM - -范围:群组、会话、联系记录、收藏、标签、快捷语、客服转接/退出、客服状态和会话容量。 - -依赖:`SYS-001`、`PLT-001`、腾讯 IM、RabbitMQ。 - -#### IM-002 玩家端与 SDK IM - -范围:全部 `/imserver`、`/sdk/im`、腾讯 IM callback、玩家注册登录、平台绑定、入群、会话、评价、未读数和 OCR。 - -重点验收: - -- 后台 JWT 与玩家 JWT 完全隔离; -- 腾讯回调原始 body、签名、ACK 和幂等; -- RabbitMQ reject/requeue/ack 语义; -- 客服并发分配和 `im_current_session` 原子更新; -- 移动端与 PC 端现有接口逐字段回归。 - -#### SDK-001 扶持及其他签名 SDK - -范围:`/sdk/support/*` 及除 IM 外的签名接口。 - -重点验收:旧签名算法、字段排序、空值参与规则、错误响应和重放保护。 - -### 阶段 6:分发与报表 - -#### ISS-001 Issue 分发系统 - -范围:Issue 玩家、补单、充值、角色和数据接口,以及相应 KRA 后台页面。 - -依赖:`PLY-001`、`ORD-001`。 - -#### REP-001 运营报表 - -范围:Top PID、每日 PID 及 OA 中由动态 SQL/跨库聚合产生的报表。 - -依赖:主业务写入模块稳定后执行。 - -重点验收:时间边界、时区、去重口径、金额合计、跨库汇总、分页和导出结果。 - -### 阶段 7:定时任务与运行保障 - -#### OPS-001 OA 定时任务迁移 - -OA 当前至少包含以下运行任务,必须逐个归属到对应模块并注册到 KRA 任务中心: - -| OA 任务 | 目标模块 | -|---|---| -| `ClearDB` | System | -| `CloseChat` | IM | -| `UnsubscribeSysUser` | System/User | -| `AutoSendSupport` | Support | -| `AutoSendGift` | Support | -| `PromoteActivePoolScan` | Platform/Channel | -| `PcRepackageScan` | Packaging | -| `LockChannelOverThreeMonth` | Channel | -| `AutoSendFuliRecord` | Welfare | -| `AutoSendWeekCard` | Welfare | -| `AutoBuLianInactiveUsers` | Player | -| `AutoAuditSupport` | Support | - -每个任务必须验证:默认 cron、秒级模式、启停配置、手动触发、并发防重、超时、失败重试、执行日志、告警、跨库范围和重复实例运行风险。 - -#### OPS-002 可观测性与故障恢复 - -- 为所有外部调用增加 request ID、耗时、结果码和脱敏错误日志。 -- 为订单、发放、提现、打包、IM 回调增加幂等键和补偿查询入口。 -- 增加任务积压、消息队列连接、第三方失败率、数据库连接和慢查询指标。 -- 编写常见故障恢复手册,覆盖 OA 维护文档中的主要故障场景。 - -### 阶段 8:全量回归、数据演练与切换 - -#### CUT-001 全量接口与后台回归 - -- 有效 OA 路由覆盖率 100%,或存在书面批准的废弃记录。 -- 旧前端 API 调用覆盖率 100%。 -- KRA 新增后台菜单、页面、按钮和权限均可按角色访问。 -- 逐接口差异报告无未批准差异。 - -#### CUT-002 数据迁移演练 - -- 使用生产结构副本和脱敏数据至少完成两次全量演练。 -- 核对行数、主键、外键、软删除、金额、状态分布和关键业务抽样。 -- 记录每张表迁移耗时、锁影响、失败恢复和增量追平方式。 - -#### CUT-003 灰度与正式切换 - -- 先影子读/流量回放,再灰度只读接口,最后灰度写接口。 -- 写接口切换前冻结或增量同步 OA 数据,防止双主写入。 -- 回调、定时任务和消费者在任一时刻只能有一套系统实际执行。 -- 保留可执行回滚方案、回滚数据点和外部回调切回步骤。 - -## 9. 单个模块的执行模板 - -每个模块都按以下顺序执行,不允许跳过最后两步: - -1. **盘点**:登记 OA model/request/response/api/router/service/frontend/timer/config 文件。 -2. **契约**:冻结路径、鉴权、请求、响应、Header、错误和副作用。 -3. **数据**:完成 schema map、migration、历史转换和校验脚本。 -4. **领域**:实现 KRA DO、状态机、用例和仓储接口。 -5. **数据层**:实现 PO、查询、事务、跨库访问和外部仓储。 -6. **接口层**:实现兼容 DTO、handler、路由和中间件绑定。 -7. **后台端**:迁移 API 文件、页面、菜单、按钮和权限种子。 -8. **任务/集成**:接入本模块定时任务、消息和第三方服务。 -9. **自动验证**:单测、集成测试、契约差异、前端构建和页面回归。 -10. **逐文件复核**:对 source ledger 每一项签署结论,生成 verification 报告。 - -## 10. 模块完成标准 - -模块只有同时满足以下条件才能标记完成: - -- source ledger 中无未处理文件; -- schema map 中无待定字段、状态或索引; -- 有效接口全部注册且鉴权域正确; -- 请求与响应字段、类型、消息、Header 和分页结构一致; -- 查询条件、Join、排序、默认值、软删除和事务行为一致; -- Redis/MQ/第三方调用及失败处理一致; -- KRA 后台页面、菜单、按钮权限完整; -- 单元测试、集成测试、差异测试、后端构建、前端 lint/build 全部通过; -- 历史数据转换通过自动校验和业务抽样; -- 所有差异均有书面原因和批准记录。 - -## 11. 推荐执行顺序与并行关系 - -```text -MIG-000/001/002 - | -FND-001..006 - | -SYS-001..004 - | -PLT-001 ---- GAM-001 ---- CHN-001 - | | | - | +---- PKG-001-+ - | | - +---- PLY-001 ---- ORD-001-+ - | | | - +-- SUP-001+-- FIN-001 - | - TG-001 / IM-001 / IM-002 / SDK-001 - | - ISS-001 / REP-001 - | - OPS-001 / OPS-002 - | - CUT-001..003 -``` - -可并行原则: - -- 基础阶段结束后,平台、游戏基础、玩家基础和 IM 适配器可由不同任务并行。 -- 同一张 KRA 系统表只允许一个任务负责 schema 和核心 repo,其他模块通过接口依赖。 -- 管理端页面可在接口契约冻结后并行迁移,但不得自行改变接口字段。 -- 定时任务代码随所属模块实现,最终在 `OPS-001` 统一做重复执行和调度验证。 - -## 12. 当前已识别的高风险点 - -1. OA 的有效接口数量远高于 KRA,且部分路由只在特定路由组注册,必须以运行时为准。 -2. KRA 当前私有路由统一经过 Casbin,而 OA 存在“仅登录即可访问”的接口,路由域必须先拆开。 -3. OA JWT 支持多角色和公司上下文,KRA 当前 claims 主要是单角色。 -4. OA 未登录响应与 KRA 当前 HTTP 401/错误码行为存在差异。 -5. OA 部门表与 KRA 表名、字段和关系表不同,不能双表共存作为长期方案。 -6. KRA 当前 migration 不会为旧表自动加列,系统字段扩展必须显式实现。 -7. OA 多数据库和跨库写入没有统一事务,迁移时要保持现有结果并补充幂等/补偿。 -8. OA 大量业务查询使用动态 map 和冗余快照字段,不能简单改为关联表实时查询。 -9. `sys_user` 服务混合了登录、IM、注销和多个 CP 发放适配,迁移时必须拆分但保持调用顺序和结果。 -10. RabbitMQ、腾讯 IM、PC 分包、用户中心、短信、OCR 和多个 CP 接口是完整迁移的外部阻塞项。 -11. 移动端和 PC 端源码不在当前两个仓库中时,必须用发布包、请求日志或抓包补齐契约基线。 -12. 回调消费者、定时任务和自动发放在双系统并行期间存在重复执行风险。 - -## 13. 第一批建议启动任务 - -正式编码时建议先只启动以下任务,完成后再大规模并行: - -- `MIG-000`:生成完整 source ledger 和有效路由清单; -- `MIG-001`:建立 OA/KRA 接口差异测试框架; -- `MIG-002`:建立第一个可插拔 OA 模块样板; -- `FND-001`:实现显式增量数据库 migration; -- `FND-002`:冻结兼容响应和分页结构; -- `FND-003`:拆分接口域和鉴权中间件; -- `SYS-001-A`:完成 `sys_users` 字段映射草案与 migration 评审; -- `SYS-003`:完成部门/公司/岗位映射草案,禁止在映射确定前导入用户关系数据。 - -这一批完成后,才具备安全拆分多个业务模块任务并行迁移的条件。 - -## 14. 细化执行目录 - -本节是对前面阶段任务的可执行拆分。编号规则如下: - -- `MIG/FND/SYS/...-主编号`:模块级里程碑; -- `...-主编号-A/B/C`:工作包,可单独分派给一个开发任务; -- `...-主编号-A1/A2`:同一工作包内的文件、接口或验收批次; -- 工作包没有通过自己的验收标准时,不得标记所属模块完成。 - -每个工作包默认包含四类动作:代码实现、数据库/配置变更、测试、文档更新。若某项不适用,必须在任务记录中写明“确认不适用”,不能留空。 - -### 14.1 任务状态和门禁 - -| 状态 | 含义 | 进入条件 | 退出条件 | -|---|---|---|---| -| `READY` | 可以开始 | 前置任务已完成,输入文件已冻结 | 开发者接单 | -| `DOING` | 开发中 | 已建立分支和任务目录 | 代码、迁移、测试全部提交 | -| `REVIEW` | 待复核 | source/schema/api/verification 四份清单已更新 | 通过代码和契约复核 | -| `BLOCKED` | 被阻塞 | 外部服务、客户端样本、数据权限或字段含义缺失 | 阻塞原因解除并补充证据 | -| `DONE` | 已完成 | 所有工作包验收通过 | 进入后续依赖 | -| `DEFERRED` | 明确延期 | 经过负责人确认不影响当前切换范围 | 重新评估,不视为完成 | - -阶段门禁: - -1. `MIG` 门禁:所有 OA 路由、前端 API、页面入口、配置和任务均已编号。 -2. `FND` 门禁:KRA 能表达 OA 的响应、鉴权、多库、外部调用和增量迁移。 -3. `SYS` 门禁:管理后台登录、用户、权限、部门、菜单和数据范围稳定。 -4. `业务主数据` 门禁:游戏、平台、渠道、玩家主键关系稳定。 -5. `交易与资金` 门禁:订单、扶持、结算和提现具备幂等及补偿能力。 -6. `端能力` 门禁:后台、TG、玩家端、PC/移动端和 SDK 契约全部通过。 -7. `CUT` 门禁:数据演练、流量回放、回滚和运维交接全部完成。 - -### 14.2 每个工作包的固定交付物 - -每个工作包至少交付以下内容,文件名可按模块替换 ``: - -```text -docs/migration//-source-ledger.md -docs/migration//-schema-map.md -docs/migration//-api-contract.md -docs/migration//-verification.md -``` - -代码交付至少应覆盖: - -- KRA `biz`:DO、状态常量、仓储接口和业务错误; -- KRA `data`:PO、转换函数、查询、事务和 migration; -- KRA `service`:请求 DTO、响应 DTO、字段转换和分页转换; -- KRA `server`:handler、router、中间件绑定和权限元数据; -- KRA `web`:API 封装、页面、菜单、按钮和路由; -- `*_test.go`:领域、数据、接口和回归测试; -- 配置/任务/集成:只在工作包确实拥有该能力时加入。 - -### 14.3 任务执行时的统一拆分动作 - -一个工作包不得直接从 OA 文件复制到 KRA。必须按以下 12 个子动作逐项销项: - -| 子动作 | 内容 | 产出 | -|---|---|---| -| `P01` 源文件登记 | model/request/response/api/service/router/web/config/timer 全量登记 | source ledger | -| `P02` 路由冻结 | 方法、路径、路由组、中间件、是否公开 | api-contract | -| `P03` 请求冻结 | JSON/form/query/header、必填、默认值、兼容别名 | api-contract | -| `P04` 响应冻结 | code/data/msg、字段、类型、空值、分页、下载 Header | api-contract | -| `P05` 数据表冻结 | 表、列、类型、默认值、索引、软删除、主键 | schema-map | -| `P06` 状态机冻结 | 状态值、允许转换、重复请求、失败回退 | schema-map | -| `P07` 领域实现 | DO、用例、错误和仓储接口 | KRA biz | -| `P08` 数据实现 | PO、查询、事务、跨库和外部仓储 | KRA data | -| `P09` 兼容接口实现 | DTO、handler、route、旧响应适配 | KRA service/server | -| `P10` 前端/后台实现 | API、页面、菜单、按钮、权限 | KRA web + seed | -| `P11` 对照验证 | OA/KRA 相同输入、相同数据、相同副作用 | verification | -| `P12` 文件签收 | 每个源文件写明迁移、替代或废弃理由 | signed ledger | - -## 15. 阶段 0 细化:盘点和工具 - -### MIG-000 源代码与运行时清单冻结 - -| 工作包 | 范围 | 主要 OA 输入 | KRA 产出 | 依赖 | 并行 | -|---|---|---|---|---|---| -| `MIG-000-A` | 后端文件清单 | `server/model`、`api/v1`、`service`、`router` | 总 source ledger | 无 | 可与 B/C 并行 | -| `MIG-000-B` | 前端调用清单 | `web/src/api`、`web/src/view`、`router` | API-to-page 映射表 | 无 | 可与 A/C 并行 | -| `MIG-000-C` | 路由运行时清单 | `initialize/router.go`、所有 `router/*` | 有效方法/路径/中间件清单 | OA 可启动环境 | 可与 A/B 并行 | -| `MIG-000-D` | 数据表清单 | `model/**/*.go`、初始化/建表逻辑 | 表字段/索引/关系目录 | A | 不与 schema 改造并行 | -| `MIG-000-E` | 定时任务清单 | `initialize/timer.go`、配置 | 任务/cron/依赖/副作用表 | A | 可与 D 并行 | -| `MIG-000-F` | 外部依赖清单 | IM、MQ、短信、UC、OCR、OSS、分包、CP 调用 | 外部服务矩阵 | A | 可与 D/E 并行 | -| `MIG-000-G` | 客户端样本冻结 | 后台页面、移动端、PC 端请求日志/抓包 | 脱敏请求语料库 | B/C | C 完成后进行 | -| `MIG-000-H` | 遗留代码归类 | 注释路由、测试、脚本、示例 | 保留/废弃审批表 | A/B | 可与 G 并行 | - -完成标准:A-H 全部 `DONE`,总清单中的 OA 文件、有效路由、页面 API、任务和外部依赖均有编号。 - -### MIG-001 接口差异测试框架 - -| 工作包 | 细化内容 | 最小验收 | -|---|---|---| -| `MIG-001-A` | 测试环境、数据库快照、脱敏种子 | OA/KRA 可用同一业务数据启动 | -| `MIG-001-B` | 请求重放器,支持 JSON/form/query/header/file | 可重放登录、列表、详情、写入 | -| `MIG-001-C` | 响应规范化,处理 token/时间/随机 ID | 不掩盖字段、类型和消息差异 | -| `MIG-001-D` | 数据库副作用快照 | 能比较新增、更新、删除、状态和金额 | -| `MIG-001-E` | Redis/MQ/外部调用断言 | 能检查 key、topic、ack、调用参数 | -| `MIG-001-F` | 差异报告和例外清单 | 每个差异有路径、字段、原因和审批人 | -| `MIG-001-G` | 端到端基线用例 | 登录、菜单、用户列表、业务 CRUD、下载各 1 组 | - -### MIG-002 模块脚手架与持续验证 - -| 工作包 | 细化内容 | -|---|---| -| `MIG-002-A` | 建立模块 Definition、Catalog、ProviderSet、RouteRegistrar、Task Contributor 样板 | -| `MIG-002-B` | 建立 module migration、菜单、API、定时任务注册样板 | -| `MIG-002-C` | 建立跨层依赖检查和路由冲突检查 | -| `MIG-002-D` | 建立 Go/前端/契约/数据校验 CI 入口 | -| `MIG-002-E` | 用空模块完成 Wire、构建、启动和初始化演练 | - -## 16. 阶段 1 细化:KRA 兼容底座 - -### FND-001 显式增量数据库迁移 - -| 工作包 | 细化内容 | 关键文件/产出 | -|---|---|---| -| `FND-001-A` | 表/列/索引存在性检查 | `pkg/database/migration` 工具和测试 | -| `FND-001-B` | MySQL 增列/索引/默认值方言 | 方言测试和 dry-run 输出 | -| `FND-001-C` | PostgreSQL/SQL Server/Oracle/SQLite 方言 | 各方言兼容测试 | -| `FND-001-D` | 批量回填、断点、限速、进度 | migration runner 扩展 | -| `FND-001-E` | 迁移前后校验、失败恢复 | checksum/校验报告 | -| `FND-001-F` | 禁止破坏性变更的静态检查 | 删除列/改类型/无 ID 检查 | -| `FND-001-G` | 用 `sys_users` 和 `sys_departments` 做演练 | 可重复执行、已有数据不丢失 | - -### FND-002 OA 请求与响应兼容层 - -| 工作包 | 细化内容 | -|---|---| -| `FND-002-A` | `{code,data,msg}` 基础响应和错误码 | 成功、失败、未登录、过期、权限不足 | -| `FND-002-B` | `PageResult`/`OaPageResult` 分页转换 | `list/total/page/pageSize/maxCount/fialData/extend` | -| `FND-002-C` | 字段类型和空值规则 | int/bool/string、null/[]/{}、时间和金额 | -| `FND-002-D` | token/Header/Cookie 兼容 | `x-token`、`new-token`、`new-expires-at` | -| `FND-002-E` | 上传、`common/download`、下载、导出和文件名兼容 | Content-Type、Content-Disposition、文件流 | -| `FND-002-F` | 全局错误和消息映射 | OA 原消息与 KRA 错误的固定映射表 | -| `FND-002-G` | 兼容层基线测试 | 至少 20 个典型接口通过差异测试 | - -### FND-003 多接口域与中间件 - -| 工作包 | 细化内容 | -|---|---| -| `FND-003-A` | Public/Admin Login/Admin RBAC/Admin DB 路由组 | 路由注册和中间件顺序固定 | -| `FND-003-B` | 后台 JWT claims 与多角色 | `Authorities`、公司、手机号、管理员标识 | -| `FND-003-C` | Casbin 多角色和超级管理员 | 任一角色命中、888 旁路、冻结角色 | -| `FND-003-D` | OA 未登录/过期/非法 token 行为 | code、HTTP 状态、reload 字段和 Header | -| `FND-003-E` | `DbHandler` 和数据范围 | body 四种 `dbName` 结构、跨库权限 | -| `FND-003-F` | `TgAuth` 平台切换和层级 | Redis 当前平台、渠道层级和登录记录 | -| `FND-003-G` | `SignAuth` 签名 | 参数排序、空值、重放、错误返回 | -| `FND-003-H` | 操作日志/错误审计/限流/CORS | 成功、失败、未授权请求的记录边界 | -| `FND-003-I` | `sys_captcha`、`sys_initdb`、初始化保护和健康检查 | `/base/captcha`、`/health`、`/init/checkdb`、`/init/initdb` | - -### FND-004 多数据库、公司、平台上下文 - -| 工作包 | 细化内容 | -|---|---| -| `FND-004-A` | OA `db-list` 到 KRA `database_list` 映射 | alias、连接、显示名、业务 URL | -| `FND-004-B` | 请求体 `dbName` 统一解析 | string/list/ids/data 四种结构 | -| `FND-004-C` | 当前公司/平台上下文 | Redis key、JWT、request context | -| `FND-004-D` | 多库只读查询 | 并发、超时、部分失败、结果合并 | -| `FND-004-E` | 多库写入和补偿 | 无跨库事务时的幂等、重试、补偿记录 | -| `FND-004-F` | 无权限库和不存在库 | 统一错误消息和审计 | - -### FND-005 外部集成基础设施 - -| 工作包 | 细化内容 | 依赖 | -|---|---|---| -| `FND-005-A` | RabbitMQ AMQP 连接、声明、发布、消费、ack/requeue | MQ 配置 | -| `FND-005-B` | 腾讯 IM client、UserSig、回调签名和请求封装 | 腾讯配置 | -| `FND-005-C` | 用户中心 UC 注册、登录、加密和短信接口 | UC 配置 | -| `FND-005-D` | 阿里云短信发送、验证码存储和限流 | 短信配置 | -| `FND-005-E` | OCR 身份证/营业执照适配器 | OCR 配置 | -| `FND-005-F` | 游戏业务站/CP 发放 HTTP client | 外部业务站矩阵 | -| `FND-005-G` | PC 重打包适配器和轮询 | 分包服务协议 | -| `FND-005-H` | 企业签/契约/支付扩展适配器 | 第三方协议 | -| `FND-005-I` | 所有适配器的超时、重试、脱敏和 mock | A-H | - -### FND-006 历史数据迁移程序 - -| 工作包 | 细化内容 | -|---|---| -| `FND-006-A` | 迁移命令、配置、dry-run、断点和日志 | 独立命令入口 | -| `FND-006-B` | 主系统表迁移 | 用户、角色、菜单、API、部门、字典、日志 | -| `FND-006-C` | 系统扩展表迁移 | 公司、平台、黑名单、活跃池、CP 配置 | -| `FND-006-D` | 业务库表迁移 | 按 db alias 分批迁移 | -| `FND-006-E` | 主键/外键/关系表校验 | 映射表和孤儿检查 | -| `FND-006-F` | 金额/状态/时间/软删除校验 | 汇总及抽样报告 | -| `FND-006-G` | 增量追平和回滚演练 | 两次以上完整演练 | - -## 17. 阶段 2 细化:系统能力合并 - -### SYS-001 用户、登录与账号生命周期 - -| 工作包 | OA 文件/能力范围 | 依赖 | -|---|---|---| -| `SYS-001-A` | `model/system/sys_user.go`、用户 PO/DO 字段扩展 | FND-001 | -| `SYS-001-B` | 用户扩展列 migration、索引、默认值、回填 | A | -| `SYS-001-C` | `/base/login` 账号密码登录 | FND-002/003 | -| `SYS-001-D` | `/base/register`、`/base/phoneLogin`、短信验证码 | FND-005-C/D | -| `SYS-001-E` | `/base/tgPhoneLogin` 会长登录及用户状态 | FND-003-F | -| `SYS-001-F` | `/base/forgetOnChangePassword`、重置/修改密码 | C | -| `SYS-001-G` | `/user/getUserInfo`、个人资料、主题、头像 | A/C | -| `SYS-001-H` | 用户列表、筛选、导出、批量角色/部门 | SYS-002/003 | -| `SYS-001-I` | 公司子账号、公司切换、公司用户列表 | SYS-003 | -| `SYS-001-J` | `sys_user_blacklist`、注销、实名认证、敏感字段 | FND-003-H、FND-005-E | -| `SYS-001-K` | IM 客服字段、状态、容量和会话数 | IM-001 前置接口先定义 | -| `SYS-001-L` | `sys_user_menus_often`、管理后台用户页面与按钮权限 | C-H | -| `SYS-001-M` | 用户全量差异回归和数据迁移验收 | A-L | - -### SYS-002 角色、菜单、API、按钮和数据权限 - -| 工作包 | OA 文件/能力范围 | 依赖 | -|---|---|---| -| `SYS-002-A` | `sys_authority` 字段和角色状态 | FND-001 | -| `SYS-002-B` | 角色 CRUD、树、复制、默认路由 | A | -| `SYS-002-C` | 多角色关联和 JWT authority 列表 | SYS-001-C | -| `SYS-002-D` | 菜单树、菜单参数、`sys_menu_btn`、动态路由 | FND-002 | -| `SYS-002-E` | API 表、运行时路由同步、API 分组 | MIG-000-C | -| `SYS-002-F` | `sys_casbin`、Casbin 策略、`sys_authority_btn`、按钮权限、角色菜单 | C-E | -| `SYS-002-G` | 角色平台关系和数据范围 | SYS-003、PLT-001 | -| `SYS-002-H` | KRA 后台角色/菜单/API/按钮页面 | B-F | -| `SYS-002-I` | 超管/普通角色/多角色/无权限回归 | B-G | - -### SYS-003 部门、公司、岗位和通讯录 - -| 工作包 | OA 文件/能力范围 | 依赖 | -|---|---|---| -| `SYS-003-A` | OA `sys_depart` 与 KRA `sys_departments` 字段映射 | FND-001 | -| `SYS-003-B` | 部门扩展列 migration 和祖先链回填 | A | -| `SYS-003-C` | `sys_user_depart` 到 `sys_user_departments` 关系转换 | A/B | -| `SYS-003-D` | 公司/顶级部门/子部门树和范围 | B | -| `SYS-003-E` | 岗位和用户岗位关系 | KRA position | -| `SYS-003-F` | 部门 CRUD、移动、删除、批量分配 | C-E | -| `SYS-003-G` | 通讯录、客服部门、账号数量、企业微信字段 | D-F | -| `SYS-003-H` | 部门/公司/岗位后台页面 | F/G | -| `SYS-003-I` | 祖先链、孤儿关系、公司权限和用户查询回归 | A-H | - -### SYS-004 系统配置、日志、文件、导出与遗留工具 - -| 工作包 | 范围 | 依赖 | -|---|---|---| -| `SYS-004-A` | `sysConfig`/`sys_system` 兼容门面、参数和集成配置映射 | FND-002 | -| `SYS-004-B` | 操作记录、`sys_user_action_log`、登录日志 | FND-003-H | -| `SYS-004-C` | `sys_jwt_blacklist`、`sys_member_blacklist`、`sys_member_blacklist_pay` | SYS-001 | -| `SYS-004-D` | 文件上传下载、断点续传、媒体库、附件分类 | FND-005 | -| `SYS-004-E` | Excel 导入导出和模板 | FND-002-E | -| `SYS-004-F` | `sys_auto_code`、`sys_auto_code_history`、`sys_autocode_history` 和生成器使用性评估 | MIG-000-H | -| `SYS-004-G` | `exa_breakpoint_continue`、`exa_customer`、`exa_file_upload_download`、GitHub/example/test 页面使用性评估 | MIG-000-H | -| `SYS-004-H` | `sys_dictionary`、`sys_dictionary_detail` 兼容复核及后台监控、日志、媒体和配置入口 | A-E | - -## 18. 阶段 3 细化:平台、游戏、渠道和打包 - -### PLT-001 联运平台、公司和商务身份 - -| 工作包 | OA 表/文件范围 | 依赖 | -|---|---|---| -| `PLT-001-A` | `sys_platform` 平台主数据 | SYS-003 | -| `PLT-001-B` | `sys_authority_platform` 角色平台关系 | SYS-002 | -| `PLT-001-C` | `sys_user_company` 公司关系 | SYS-001/003 | -| `PLT-001-D` | `sys_platform_cp_session`、`sys_platform_cp_support`、`sys_platform_cp_gift_config`、gift 记录 | A | -| `PLT-001-E` | SW `business_user_api`、`sys_user_role`、`sys_user_preference`、`sys_user_bind_platform` | SYS-001 | -| `PLT-001-F` | 平台/公司/商务后台页面和权限 | A-E | -| `PLT-001-G` | `sys_platform_promote_active_pool`、`sys_promote_active_pool_batch*` 和当前平台/业务库映射回归 | FND-004 | - -### GAM-001 游戏目录与基础配置拆分 - -| 工作包 | OA 表/文件范围 | 主要接口/页面 | -|---|---|---| -| `GAM-001-A` | `tab_game_type` | 游戏类型 CRUD | -| `GAM-001-B` | `tab_game_cp` | CP 商 CRUD | -| `GAM-001-C` | `tab_game` 基础字段 | 游戏 CRUD、列表、详情 | -| `GAM-001-D` | 游戏状态、配置 JSON、数据字段 | `changeStatus`、`setGameConfig`、`setGameDataField` | -| `GAM-001-E` | 游戏改名和 `tab_game_change_name_log` | 改名、历史引用、日志 | -| `GAM-001-F` | `tab_game_set`、`tab_game_source` | 对接设置、原包管理 | -| `GAM-001-G` | `tab_game_server`、`tab_game_server_merge` | 区服、合服 | -| `GAM-001-H` | `tab_game_interflow` | 互通关联和跨游戏关系 | -| `GAM-001-I` | `tab_game_ban_set`、`tab_game_ban_support` | 禁推/禁扶持 | -| `GAM-001-J` | `tab_app`、`tab_platform_config` | APP 包和平台配置 | -| `GAM-001-K` | `tab_pay_config`、`sys_option` | 支付配置和系统选项 | -| `GAM-001-L` | `tab_platform_qys_pay_log`、`tab_platform_qys_consume_log`、`art_opus_index_order` | 企业签日志、排序 | -| `GAM-001-M` | 游戏后台页面、菜单、按钮和导出 | A-L | -| `GAM-001-N` | 游戏表迁移、状态机和跨表回归 | A-M | - -每个 `GAM-001-*` 工作包必须独立完成 P01-P12;`GAM-001-C` 和 `GAM-001-D` 不允许并行修改同一个 `tab_game` PO、migration 或状态常量。 - -### CHN-001 渠道、商务、合同和公告拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `CHN-001-A` | `tab_promote` 主表和层级关系 | 渠道 CRUD、上下级、状态 | -| `CHN-001-B` | 渠道审核、驳回、冻结、解冻 | 状态机和审计 | -| `CHN-001-C` | `tab_promote_business` | 商务归属、批量改派 | -| `CHN-001-D` | `tab_promote_config` | 渠道推广配置 | -| `CHN-001-E` | `tab_promote_login_record` | 登录记录和连续未登录 | -| `CHN-001-F` | `tab_promote_contract` | 合同列表、签署、状态 | -| `CHN-001-G` | `tab_promote_notice` | 渠道公告 | -| `CHN-001-H` | `tab_promote_deposit`、`tab_promote_coin`、平台币/渠道币 | 押金、余额和币种 | -| `CHN-001-I` | 渠道导出、后台页面和权限 | 前端全量迁移 | -| `CHN-001-J` | 层级、商务、状态和快照回归 | 全量差异测试 | - -### PKG-001 渠道申请与多端打包拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `PKG-001-A` | `tab_promote_apply` 基础申请 | 申请 CRUD、审核基础字段 | -| `PKG-001-B` | 申请审核、分成比例、可见性 | 审核、比例、隐藏游戏 | -| `PKG-001-C` | Android 普通包状态机 | `allPackage`、下载、失败重试 | -| `PKG-001-D` | PC 重打包状态机 | `allPcPackage`、外部任务、轮询 | -| `PKG-001-E` | H5/超级签/免分包 | URL、分享显示、渠道参数 | -| `PKG-001-F` | `tab_promote_app` 包自定义 | 图标、名称、启动图 | -| `PKG-001-G` | 快手/抖音配置 | 保存、查询、校验 | -| `PKG-001-H` | 打包下载、删除、导出 | 文件和权限 | -| `PKG-001-I` | 后台打包管理页面和任务中心入口 | 页面、按钮、进度 | -| `PKG-001-J` | 状态机、幂等、异步失败和下载回归 | 全量验收 | - -## 19. 阶段 4 细化:玩家、订单、扶持和财务 - -### PLY-001 玩家、角色和账号关联拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `PLY-001-A` | `tab_user` 玩家主表 | 玩家列表、详情、编辑 | -| `PLY-001-B` | 玩家注册归因、设备、IP、补链 | 渠道归因和 `buLian` | -| `PLY-001-C` | `tab_user_play`、`tab_user_play_info` | 角色列表、角色详情 | -| `PLY-001-D` | `tab_user_login_record` | 玩家登录历史和导出 | -| `PLY-001-E` | 玩家补单 `tab_user_mend` | 补单申请、处理和状态 | -| `PLY-001-F` | `tab_user_agree_record`、`tab_sms_log` | 协议记录、短信查询 | -| `PLY-001-G` | 敏感信息脱敏和 reveal | 权限、审计、字段展示 | -| `PLY-001-H` | 玩家批量添加、导出和小号 | 批量导入、列表导出 | -| `PLY-001-I` | `tab_user_deduct_bind`、余额调整 | 绑币回收、平台币余额 | -| `PLY-001-J` | 玩家后台页面、筛选、按钮 | 前端全量迁移 | -| `PLY-001-K` | 归因、补链、角色和敏感字段回归 | 数据/接口验收 | - -### ORD-001 充值、绑币、平台币和交易记录拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `ORD-001-A` | `tab_spend` 订单主表 | 列表、详情、CRUD | -| `ORD-001-B` | 支付状态和游戏通知状态 | 状态机、回调、查询 | -| `ORD-001-C` | 订单绑定和补单 | `bind`、`repair`、幂等 | -| `ORD-001-D` | `tab_spend_bind`、`tab_promote_bind_update_record` | 绑币充值、换绑和变更记录 | -| `ORD-001-E` | `tab_spend_provide` | 平台币发放 | -| `ORD-001-F` | `tab_spend_balance`、`tab_spend_promote_coin` | 余额/渠道币变更 | -| `ORD-001-G` | 代金券记录和 QYS 交易日志 | `tab_coupon_record`、企业签 | -| `ORD-001-H` | 支付适配器与回调兼容 | 支付渠道、签名、ACK | -| `ORD-001-I` | 订单导出、后台页面、权限 | 前端和导出 | -| `ORD-001-J` | 金额、唯一性、回调、补单、到账回归 | 全量验收 | - -### SUP-001 扶持、福利和 CP 发放拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `SUP-001-A` | `tab_support` | 扶持申请、审核、状态 | -| `SUP-001-B` | `tab_promote_support` | 渠道扶持 | -| `SUP-001-C` | `tab_support_beishu`、`tab_support_zero_log` | 倍数、清零、日志 | -| `SUP-001-D` | `tab_fuli_config`、`tab_fuli_prop` | 福利配置、道具 | -| `SUP-001-E` | `tab_fuli_record` | 福利记录、批量审核、发放 | -| `SUP-001-F` | CP 礼包配置和发放 | `sys_platform_cp_gift*` | -| `SUP-001-G` | CP 外部发放适配器 | 996、盛和等业务站 | -| `SUP-001-H` | 自动审核、自动扶持、自动福利 | 任务和幂等 | -| `SUP-001-I` | 扶持/福利后台页面和权限 | 前端全量迁移 | -| `SUP-001-J` | 到账、失败、补偿、重复发放回归 | 全量验收 | - -### FIN-001 结算、提现、代充和预支拆分 - -| 工作包 | OA 表/文件范围 | 主要业务 | -|---|---|---| -| `FIN-001-A` | `tab_promote_settlement_time` | 结算时间配置 | -| `FIN-001-B` | `tab_promote_settlement` | 结算单、汇总、确认 | -| `FIN-001-C` | `tab_promote_settlement_period` | 周期单和详情 | -| `FIN-001-D` | `tab_promote_withdraw`、`tab_promote_withdraw_ip`、`tab_user_hzb_withdraw` | 渠道/玩家提现、风控、状态 | -| `FIN-001-E` | `tab_promote_agent`、`tab_promote_bind` | 代充折扣、绑币订单 | -| `FIN-001-F` | `tab_promote_advance_whitelist`、`tab_promote_advance_game` | 预支白名单和游戏 | -| `FIN-001-G` | `tab_promote_advance_limit_log`、`tab_promote_advance_return_log` | 预支额度、还款、限额和返还日志 | -| `FIN-001-H` | 余额、结算、提现后台页面和权限 | 前端全量迁移 | -| `FIN-001-I` | 金额、状态、重复付款、额度恢复回归 | 全量验收 | - -## 20. 阶段 5 细化:TG、IM、SDK 和客户端 - -### TG-001 会长/渠道移动端拆分 - -| 工作包 | API 族 | 依赖 | -|---|---|---| -| `TG-001-A` | `/tg/promote/info`、`base_info`、`update` | SYS-001、CHN-001 | -| `TG-001-B` | `/tg/promote/getAll`、`switch`、当前平台 | FND-003-F、PLT-001 | -| `TG-001-C` | `data_index`、`summary`、统计口径 | GAM/CHN/PLY/ORD | -| `TG-001-D` | `period`、`confirmPeriod`、`showDetail` | FIN-001-B/C | -| `TG-001-E` | `withdraw`、`withdraw_info`、`bank` | FIN-001-D | -| `TG-001-F` | `profit`、`profitRecord`、`recharge`、`register` | ORD/FIN | -| `TG-001-G` | `bindPay*`、预支代充 | FIN-001-E/F/G | -| `TG-001-H` | 福利、扶持和申请接口 | SUP-001、PKG-001 | -| `TG-001-I` | `tab_promote_sms`、TG 菜单、层级限制、短信和错误返回 | FND-003-F | -| `TG-001-J` | 移动端全量请求回放和字段回归 | A-I | - -### IM-001 管理后台客服 IM 拆分 - -| 工作包 | OA 表/API 族 | 主要业务 | -|---|---|---| -| `IM-001-A` | `im_group`、`im_group_member`、群组 CRUD | 后台群组和成员管理 | -| `IM-001-B` | `im_groups`、`im_messages`、`im_messages_content` | 客服群、消息、平台和玩家关系 | -| `IM-001-C` | `im_groups_contact`、`im_robots` | 会话联系、机器人和状态 | -| `IM-001-D` | 客服分配、转接、结束 | 用户 IM 字段和并发计数 | -| `IM-001-E` | `im_collect`、`im_collect_tag`、`im_collect_rel_tag`、`im_shortcut`、`im_shortcut_group` | 收藏、标签、快捷语和分组 | -| `IM-001-F` | RabbitMQ 入群队列 | 发布、消费、ack/requeue | -| `IM-001-G` | 腾讯 IM 同步、群事件和消息 | 第三方适配器 | -| `IM-001-H` | 后台 IM 页面、WebSocket、权限 | 前端和实时通信 | -| `IM-001-I` | 并发分配、断线、转接和会话回归 | 全量验收 | - -### IM-002 玩家端和 SDK IM 拆分 - -| 工作包 | API 族 | 主要业务 | -|---|---|---| -| `IM-002-A` | 玩家注册、注册登录、账号登录 | 玩家 JWT | -| `IM-002-B` | 玩家密码、登出、userinfo | 玩家会话 | -| `IM-002-C` | `im_play_users`、`im_play_users_bind_platform` | 平台账号绑定/解绑/列表 | -| `IM-002-D` | `im_play_group`、`im_play_group_tag`、游戏/角色/绑定列表 | 玩家群和跨业务库查询 | -| `IM-002-E` | enterGroup、enterContact、评价 | 客服会话 | -| `IM-002-F` | `im_call_back`、腾讯 IM callback | 消息/群事件/ACK/幂等 | -| `IM-002-G` | `/sdk/im/saveUser`、`userSig` | SDK 基础接口 | -| `IM-002-H` | SDK 群组成员和群主操作 | create/add/del/change | -| `IM-002-I` | SDK 消息修改、未读数、评价 | 消息能力 | -| `IM-002-J` | OCR 和 SDK 错误兼容 | 签名、敏感数据 | -| `IM-002-K` | 移动端/PC 端请求回放 | A-J | - -### SDK-001 其他签名 SDK - -| 工作包 | API 族 | 依赖 | -|---|---|---| -| `SDK-001-A` | `/sdk/support/list` | SUP-001 | -| `SDK-001-B` | `/sdk/support/update` | SUP-001 | -| `SDK-001-C` | 旧签名算法和字段排序 | FND-003-G | -| `SDK-001-D` | 幂等、重放、错误和审计 | A-C | -| `SDK-001-E` | 外部客户端回放 | D | - -## 21. 阶段 6 细化:Issue 和报表 - -### ISS-001 Issue 分发系统 - -| 工作包 | OA 文件/表范围 | 主要业务 | -|---|---|---| -| `ISS-001-A` | `tab_issue_user` | 分发玩家 | -| `ISS-001-B` | `sys_issue_user_mend` | 分发补单 | -| `ISS-001-C` | `tab_issue_spend` | 分发充值 | -| `ISS-001-D` | `tab_issue_user_play_role` | 分发角色 | -| `ISS-001-E` | `tab_issue_data` | 数据分发和导出 | -| `ISS-001-F` | 后台页面、权限和接口回归 | A-E | - -### REP-001 报表 - -| 工作包 | OA 文件/表范围 | 主要业务 | -|---|---|---| -| `REP-001-A` | `tab_datareport_top_pid` | Top PID | -| `REP-001-B` | `tab_datareport_every_pid` | 每日 PID | -| `REP-001-C` | 动态 SQL、跨库聚合和时间边界 | 报表查询引擎 | -| `REP-001-D` | 报表页面、导出、权限和差异回归 | 全量验收 | - -## 22. 阶段 7 细化:定时任务、运维和切换 - -### OPS-001 定时任务逐项迁移 - -每个任务都独立建工作包,不能把所有 cron 放在一个提交中: - -| 工作包 | OA 任务 | 目标模块 | 关键验收 | -|---|---|---|---| -| `OPS-001-A` | `ClearDB` | System | 操作日志/JWT/任务日志清理范围一致 | -| `OPS-001-B` | `CloseChat` | IM | 超 30 分钟会话、计数和状态 | -| `OPS-001-C` | `UnsubscribeSysUser` | System | 15 天注销窗口、软删除和回滚 | -| `OPS-001-D` | `AutoSendSupport` | Support | 多业务库、外部发放、失败重试 | -| `OPS-001-E` | `AutoSendGift` | Support | 礼包状态和重复发放 | -| `OPS-001-F` | `PromoteActivePoolScan` | Platform/Channel | 活跃池扫描和渠道状态 | -| `OPS-001-G` | `PcRepackageScan` | Packaging | 外部任务轮询和失败回收 | -| `OPS-001-H` | `LockChannelOverThreeMonth` | Channel | 登录时间、上下级锁定和原因 | -| `OPS-001-I` | `AutoSendFuliRecord` | Welfare | 审核/待发放记录和幂等 | -| `OPS-001-J` | `AutoSendWeekCard` | Welfare | 周卡/月卡周期和重复执行 | -| `OPS-001-K` | `AutoBuLianInactiveUsers` | Player | inactive 条件、补链目标和跨库 | -| `OPS-001-L` | `AutoAuditSupport` | Support | dbNames、limit、审核状态和日志 | -| `OPS-001-M` | 调度总体验收 | Worker | 启停、手动触发、并发防重、告警 | - -### OPS-002 可观测性与故障恢复 - -| 工作包 | 细化内容 | -|---|---| -| `OPS-002-A` | 请求、任务、外部调用和消息统一 request/trace ID | -| `OPS-002-B` | 订单、发放、提现、打包、IM 回调幂等查询 | -| `OPS-002-C` | MQ、第三方、数据库、任务积压监控 | -| `OPS-002-D` | 敏感字段脱敏和审计检查 | -| `OPS-002-E` | OA 维护文档故障场景逐项演练 | -| `OPS-002-F` | 运维手册、告警阈值和应急联系人交接 | - -### CUT-001 全量接口和后台回归 - -| 工作包 | 细化内容 | -|---|---| -| `CUT-001-A` | 后台公共/登录/RBAC/多库接口全量回归 | -| `CUT-001-B` | 游戏/渠道/玩家/订单/扶持/财务接口全量回归 | -| `CUT-001-C` | TG 移动端全量回放 | -| `CUT-001-D` | IM 玩家端、客服端和 SDK 全量回放 | -| `CUT-001-E` | PC/Android/iOS/H5 下载、打包和回调回归 | -| `CUT-001-F` | KRA 后台页面、菜单、按钮、角色回归 | -| `CUT-001-G` | 未注册/未迁移/已批准废弃项审计 | - -### CUT-002 数据迁移演练 - -| 工作包 | 细化内容 | -|---|---| -| `CUT-002-A` | 主系统库全量迁移演练 | -| `CUT-002-B` | 各业务库全量迁移演练 | -| `CUT-002-C` | 增量追平、停写窗口和耗时测量 | -| `CUT-002-D` | 行数、金额、状态、关系、软删除抽样 | -| `CUT-002-E` | 失败恢复、断点续跑和回滚演练 | - -### CUT-003 灰度与正式切换 - -| 工作包 | 细化内容 | -|---|---| -| `CUT-003-A` | 影子读和请求回放,不产生业务写入 | -| `CUT-003-B` | 只读接口灰度和差异阈值 | -| `CUT-003-C` | 管理后台写接口灰度 | -| `CUT-003-D` | TG/玩家/SDK/回调写接口灰度 | -| `CUT-003-E` | 停止 OA 定时任务、消费者和外部回调入口 | -| `CUT-003-F` | KRA 正式切换、DNS/网关/配置切换 | -| `CUT-003-G` | 回滚到 OA 的数据点、路由和外部系统切换 | -| `CUT-003-H` | 切换后 24 小时、72 小时和 7 天观察 | - -## 23. 后台页面与 API 迁移批次 - -后台页面必须和接口工作包绑定,不能只迁后端。以下批次是 KRA 后台新增页面的建议交付顺序: - -| 批次 | OA 页面/API 族 | 绑定任务 | 页面交付 | -|---|---|---|---| -| `WEB-001` | 用户、角色、菜单、API、按钮、部门、岗位 | SYS-001/002/003 | 登录后基础管理闭环 | -| `WEB-002` | 平台、公司、商务、黑名单、配置、日志 | SYS-004/PLT-001/CHN-001 | 系统运维闭环 | -| `WEB-003` | 游戏类型、CP、游戏、设置、原包、区服、合服 | GAM-001-A-H | 游戏主数据闭环 | -| `WEB-004` | 游戏禁推、APP、平台配置、支付、企业签日志 | GAM-001-I-L | 游戏扩展闭环 | -| `WEB-005` | 渠道、商务、推广配置、合同、公告、登录记录 | CHN-001 | 渠道主数据闭环 | -| `WEB-006` | 渠道申请、打包、APP 包、快手抖音 | PKG-001 | 打包闭环 | -| `WEB-007` | 玩家、角色、补单、协议、短信、敏感信息 | PLY-001 | 玩家运营闭环 | -| `WEB-008` | 订单、绑币、平台币、代金券、支付日志 | ORD-001 | 交易闭环 | -| `WEB-009` | 扶持、福利、CP 礼包、自动审核 | SUP-001 | 发放闭环 | -| `WEB-010` | 结算、周期、提现、代充、预支 | FIN-001 | 财务闭环 | -| `WEB-011` | 后台客服 IM、收藏、快捷语、会话 | IM-001 | 客服闭环 | -| `WEB-012` | Issue、报表和导出 | ISS-001/REP-001 | 分析闭环 | -| `WEB-013` | 定时任务、运行日志、错误日志、版本和集成配置 | SYS-004/OPS-001/002 | 运维闭环 | - -每个 WEB 批次必须同时完成:API 文件、页面组件、动态菜单、按钮权限、列表/表单/详情/导出/批量操作、loading/空态/错误态和角色权限验证。 - -## 24. 推荐的任务分派方式 - -为了避免多个任务互相覆盖,建议按以下角色边界分派: - -| 角色 | 负责范围 | 不应直接修改 | -|---|---|---| -| 基础设施任务 | FND、MIG、公共响应、认证、多库、迁移工具 | 业务模块状态机 | -| 系统任务 | SYS、平台、用户权限和后台基础页面 | 游戏/订单业务表 | -| 主数据任务 | 游戏、平台、渠道、打包 | 玩家余额和结算核心逻辑 | -| 玩家任务 | 玩家、角色、补链、敏感字段 | 渠道结算状态机 | -| 交易任务 | 订单、支付、绑币、余额 | IM 会话分配 | -| 发放财务任务 | 扶持、福利、结算、提现、预支 | 登录和权限核心表 | -| 端适配任务 | TG、玩家端、SDK、PC/移动端兼容 | KRA 系统表 schema | -| IM 集成任务 | 腾讯 IM、RabbitMQ、WebSocket、客服 | 订单金额计算 | -| 运维切换任务 | 定时任务、观测、数据演练、灰度 | 未经模块负责人确认的业务代码 | - -同一文件、同一 PO、同一 migration、同一状态常量只能有一个任务负责人。跨任务修改必须先登记依赖变更,不允许通过后续冲突解决“顺手合并”。 - -## 25. 细化后的验收批次 - -每完成一个工作包,按以下批次保存证据: - -| 批次 | 验收内容 | 必须回答的问题 | -|---|---|---| -| `V-A` 文件 | OA 文件是否逐一处理 | 是否还有未归类文件?是否误删逻辑? | -| `V-B` Schema | 表和字段是否兼容 | 是否保留主键、索引、默认值、软删除和状态? | -| `V-C` Request | 请求是否兼容 | 方法、路径、参数位置、必填、别名是否一致? | -| `V-D` Response | 响应是否兼容 | code、msg、字段、类型、空值、分页和 Header 是否一致? | -| `V-E` Logic | 业务是否一致 | 状态转换、事务、幂等、跨表更新是否一致? | -| `V-F` Side Effect | 副作用是否一致 | Redis、MQ、第三方调用、日志和任务是否一致? | -| `V-G` Web | 后台是否完整 | 菜单、按钮、列表、表单、导出和权限是否完整? | -| `V-H` Data | 历史数据是否正确 | 行数、金额、状态、关系、抽样是否通过? | -| `V-I` Ops | 能否运行和恢复 | 监控、告警、重试、补偿、回滚是否可执行? | - -只有 `V-A` 到 `V-I` 全部有证据,工作包才可以进入 `REVIEW`。 - -## 26. 细化后的并行规则 - -### 允许并行 - -- `MIG-000-A/B/C/E/F` 可以并行;`MIG-000-D` 在文件清单稳定后开始。 -- `FND-005-A/B/C/D/E` 可由不同集成任务并行,但共享的配置加载和错误规范由一个负责人维护。 -- `GAM-001-A/B` 可并行;`GAM-001-C/D/E` 必须按游戏主表、状态、改名顺序执行。 -- `CHN-001-C/D/E/F/G` 可在 `CHN-001-A` 的主键和状态冻结后并行。 -- `PLY-001-C/D/F` 可在玩家主表冻结后并行。 -- `WEB-001` 之后,各业务 WEB 批次可在相应 API 契约冻结后并行开发。 - -### 禁止并行 - -- 不允许同时修改同一张表的 migration 和历史迁移脚本。 -- 不允许同时修改同一个 API 的旧响应 DTO 和前端调用封装。 -- 不允许在 `SYS-001/002/003` 未完成前迁移用户、角色、部门关系数据。 -- 不允许在 `GAM-001-C` 未完成前迁移渠道申请、玩家归因和订单外键。 -- 不允许在 `ORD-001` 未完成前迁移结算、提现、预支金额数据。 -- 不允许在 `IM-001-F/G` 未完成前启用真实 RabbitMQ 消费和腾讯 IM 回调。 -- 不允许在 `CUT-003-E` 完成前关闭 OA 的任务/消费者,也不允许双系统同时处理同一回调。 - -## 27. 细化后的第一轮实际执行顺序 - -第一轮不要直接进入全部业务,按以下 20 个工作包顺序执行: - -1. `MIG-000-A` 后端源文件登记。 -2. `MIG-000-B` 前端 API/页面登记。 -3. `MIG-000-C` 运行时路由导出。 -4. `MIG-000-D` 数据表字段目录。 -5. `MIG-000-G` 移动端/PC/后台请求样本冻结。 -6. `MIG-001-A/B/C` 差异测试基础框架。 -7. `MIG-002-A/B/E` 模块接入样板。 -8. `FND-001-A/B/G` 增量迁移最小闭环。 -9. `FND-002-A/B/C/D` 响应和 token 兼容闭环。 -10. `FND-003-A/B/C/D` 后台路由和 JWT/Casbin 闭环。 -11. `FND-004-A/B/C` 多库和平台上下文闭环。 -12. `SYS-001-A/B` 用户表扩展和 migration 评审。 -13. `SYS-003-A/B/C` 部门表和关系表映射评审。 -14. `SYS-002-A/C/D/E` 角色、多角色、菜单和 API 关系。 -15. `SYS-001-C/G` 登录和个人信息兼容接口。 -16. `SYS-003-D/F` 公司/部门接口。 -17. `WEB-001` 系统基础后台页面。 -18. `MIG-001-G` 登录、菜单、用户、部门差异回归。 -19. `FND-006-B` 主系统表迁移演练。 -20. 阶段门禁评审,确认是否进入 `PLT/GAM/CHN` 并行开发。 - -## 28. 任务记录模板 - -后续每个任务建议按以下格式记录,避免只提交代码而没有迁移证据: - -```markdown -# <任务名称> - -## 任务状态 -- 状态:READY / DOING / REVIEW / BLOCKED / DONE -- 负责人: -- 评审人: -- 前置任务: -- 影响任务: - -## 范围 -- OA 文件: -- OA 路由/API: -- OA 表: -- KRA 目标文件: -- KRA 后台页面: - -## 实施清单 -- [ ] P01 源文件登记 -- [ ] P02 路由冻结 -- [ ] P03 请求冻结 -- [ ] P04 响应冻结 -- [ ] P05 数据表冻结 -- [ ] P06 状态机冻结 -- [ ] P07 领域实现 -- [ ] P08 数据实现 -- [ ] P09 兼容接口实现 -- [ ] P10 前端/后台实现 -- [ ] P11 对照验证 -- [ ] P12 文件签收 - -## 验收证据 -- [ ] V-A 文件 -- [ ] V-B Schema -- [ ] V-C Request -- [ ] V-D Response -- [ ] V-E Logic -- [ ] V-F Side Effect -- [ ] V-G Web -- [ ] V-H Data -- [ ] V-I Ops - -## 已知差异与批准 -| 差异 | 原因 | 影响 | 批准人 | 是否阻塞 | -|---|---|---|---|---| -``` - -## 29. OA 前端 API 和页面精确归属索引 - -本索引用于 `MIG-000-B` 的初始归类。最终仍以实际 import、页面调用和运行时请求为准;同名 API 文件不能仅凭文件名判断已完成。 - -### 29.1 系统基础前端 - -| OA `web/src/api` 文件 | 目标任务 | 目标页面/说明 | -|---|---|---| -| `user.js` | SYS-001 | 用户、登录和个人信息 | -| `jwt.js` | SYS-001 / FND-003 | 登出、黑名单和 token | -| `authority.js` | SYS-002 | 角色管理 | -| `authorityBtn.js` | SYS-002 | 按钮权限 | -| `menu.js` | SYS-002 | 菜单与动态路由 | -| `api.js` | SYS-002 | API 管理 | -| `casbin.js` | SYS-002 | Casbin 策略 | -| `sysDepart.js` | SYS-003 | OA 部门兼容接口 | -| `sysUserDepart.js` | SYS-003 | 用户部门关系 | -| `sysUserMenusOften.js` | SYS-001-L | 常用菜单 | -| `sysDictionary.js` | SYS-004-H | 字典 | -| `sysDictionaryDetail.js` | SYS-004-H | 字典项 | -| `sysOperationRecord.js` | SYS-004-B | 操作记录 | -| `sysUserActionLog.js` | SYS-004-B | 用户行为日志 | -| `sysUserBlacklist.js` | SYS-001-J / SYS-004-C | 用户黑名单 | -| `system.js` | SYS-004-A | 系统配置兼容门面 | -| `initdb.js` | FND-003-I | 数据库初始化 | - -对应 OA 页面目录:`login`、`person`、`superAdmin`、`sysDepart`、`sysUserDepart`、`sysUserMenusOften`、`sysUserActionLog`、`sysUserBlacklist`、`system`、`systemTools`。 - -### 29.2 平台和公司前端 - -| OA `web/src/api` 文件 | 目标任务 | 目标页面目录 | -|---|---|---| -| `sysPlatform.js` | PLT-001-A | `sysPlatform` | -| `sysAuthorityPlatform.js` | PLT-001-B | `sysAuthorityPlatform` | -| `sysPlatformCpSession.js` | PLT-001-D | `sysPlatformCpSession` | -| `sysPlatformCpSupport.js` | PLT-001-D | `sysPlatformCpSupport` | - -### 29.3 游戏主数据前端 - -| OA `web/src/api` 文件 | 目标任务 | 目标页面目录 | -|---|---|---| -| `tabGameType.js` | GAM-001-A | `tabGameType` | -| `tabGameCp.js` | GAM-001-B | `tabGameCp` | -| `tabGame.js` | GAM-001-C/D/E | `tabGame` | -| `tabGameSet.js` | GAM-001-F | `tabGameSet` | -| `tabGameSource.js` | GAM-001-F | `tabGameSource` | -| `tabGameServer.js` | GAM-001-G | `tabGameServer` | -| `tabGameServerMerge.js` | GAM-001-G | `tabGameServerMerge` | -| `tabGameBanSet.js` | GAM-001-I | `tabGameBanSet` | -| `tabGameChangeNameLog.js` | GAM-001-E | `tabGameChangeNameLog` | -| `tabApp.js` | GAM-001-J / PKG-001-F | `tabApp` | -| `tabPlatformConfig.js` | GAM-001-J | `tabPlatformConfig` | -| `tabPlatformQysPayLog.js` | GAM-001-L / ORD-001-G | `tabPlatformQysPayLog` | -| `tabPlatformQysConsumeLog.js` | GAM-001-L / ORD-001-G | `tabPlatformQysConsumeLog` | - -### 29.4 渠道、打包和财务前端 - -| OA `web/src/api` 文件 | 目标任务 | 目标页面目录 | -|---|---|---| -| `tabPromote.js` | CHN-001-A/B/H | `tabPromote` | -| `tabPromoteBusiness.js` | CHN-001-C | `tabPromoteBusiness` | -| `tabPromoteConfig.js` | CHN-001-D | `tabPromoteConfig` | -| `tabPromoteLoginRecord.js` | CHN-001-E | `tabPromoteLoginRecord` | -| `tabPromoteContract.js` | CHN-001-F | `tabPromoteContract` | -| `tabPromoteApply.js` | PKG-001 | `tabPromoteApply` | -| `tabPromoteSettlement.js` | FIN-001-B | `tabPromoteSettlement` | -| `tabPromoteSettlementTime.js` | FIN-001-A | `tabPromoteSettlementTime` | -| `tabPromoteSupport.js` | SUP-001-B | `tabPromoteSupport` | - -### 29.5 玩家、订单和扶持前端 - -| OA `web/src/api` 文件 | 目标任务 | 目标页面目录 | -|---|---|---| -| `tabUser.js` | PLY-001-A/B/G/H | `tabUser` | -| `tabUserMend.js` | PLY-001-E | `tabUserMend` | -| `tabUserPlayInfo.js` | PLY-001-C | `tabUserPlayInfo` | -| `tabSmsLog.js` | PLY-001-F | `tabSmsLog` | -| `tabSpend.js` | ORD-001-A/B/C/I | `tabSpend` | -| `tabCouponRecord.js` | ORD-001-G | `tabCouponRecord` | -| `tabSupport.js` | SUP-001-A | `tabSupport` | - -### 29.6 IM、公共能力和遗留前端 - -| OA `web/src/api` 文件 | 目标任务 | 处理原则 | -|---|---|---| -| `imGroupMember.js` | IM-001-A | 迁移接口和 `imGroupMember` 页面 | -| `fileUploadAndDownload.js` | SYS-004-D | 对接 KRA 媒体库并保持旧协议 | -| `breakpoint.js` | SYS-004-D / SYS-004-G | 有调用则迁移,未调用则审批废弃 | -| `email.js` | SYS-004-A | 复用 KRA 邮件模块,保持旧 API | -| `autoCode.js` | SYS-004-F | 使用性评估后迁移或废弃 | -| `customer.js` | SYS-004-G | 使用性评估后迁移或废弃 | -| `github.js` | SYS-004-G | 使用性评估后迁移或废弃 | - -### 29.7 无独立 OA 页面但仍必须迁移的接口 - -以下能力不能因为当前 `web/src/view` 中没有对应目录而漏迁: - -- `/tg/*`:移动会长端使用,归 `TG-001`; -- `/imserver/*`:移动/PC 玩家端使用,归 `IM-002`; -- `/sdk/*`:游戏 SDK 和外部服务使用,归 `IM-002`、`SDK-001`; -- Issue 全部接口:可能由外部后台或独立页面使用,归 `ISS-001`; -- Report 全部接口:归 `REP-001`; -- SW 商务用户接口:归 `PLT-001-E`; -- OA 当前未展示页面的 Game/User/System 接口:仍按运行时路由和调用日志迁移。 - -## 30. OA 后端源文件覆盖检查规则 - -为了保证“逐文件确认”可以自动执行,在 `MIG-000-A` 中增加以下检查: - -1. 扫描 `server/model//*.go`,每个文件基名必须出现在 source ledger 或本计划的任务归属中。 -2. 扫描 `server/model//request/*.go` 和 `response/*.go`,必须关联至少一个 API contract。 -3. 扫描 `server/api/v1//*.go`,必须关联 handler 工作包或批准废弃项。 -4. 扫描 `server/router//*.go`,必须关联运行时有效路由或明确未注册。 -5. 扫描 `server/service//*.go`,必须关联业务用例、集成适配器或批准废弃项。 -6. 扫描 `web/src/api/*.js`,必须关联页面、外部客户端或批准废弃项。 -7. 扫描 `web/src/view/**`,必须关联一个 `WEB-*` 批次或批准废弃项。 -8. 扫描配置、中间件、定时任务和第三方工具目录,必须关联 `FND-*`、`OPS-*` 或业务任务。 - -自动检查输出至少包含: - -```text -source_path -source_type -domain -owner_task_id -target_path -decision=migrate|replace|retire -contract_ids -verification_status -``` - -合并门禁:`owner_task_id`、`decision`、`target_path` 或 `verification_status` 任何一项为空,迁移覆盖检查失败。 diff --git a/docs/README.md b/docs/README.md index 06daff1..36b2c5e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,5 @@ # 文档入口 - [后台说明](ADMIN_BACKEND.md)、[支付接入](PAYMENT.md)、[短信接入](SMS_INTEGRATION.md)。 -- [OA 迁移计划](OA_MIGRATION_PLAN.md):独立 OA 工作的设计材料,不是当前商务迁移的完成证明。 此目录仅保留持续维护的文档。临时迁移笔记、验证报告和脚本用完即删,不纳入版本控制。 diff --git a/internal/data/business/alert_v2_migration.go b/internal/data/business/alert_v2_migration.go index 52f71c2..32d1df9 100644 --- a/internal/data/business/alert_v2_migration.go +++ b/internal/data/business/alert_v2_migration.go @@ -55,16 +55,7 @@ func migrateAlertV2(db *gorm.DB) error { if err := migrateAlertV2UniqueKey(db); err != nil { return err } - return migrateQueryIndexes(db, []businessQueryIndex{ - {"sys_alert_record", "idx_alert_owner_status_time", []string{"business_id", "status", "created_at", "id"}}, - {"sys_alert_record", "idx_alert_channel_type_time", []string{"platform_code", "target_promote_id", "rule_type", "created_at", "id"}}, - {"sys_alert_record", "idx_alert_owner_time", []string{"business_id", "created_at", "id"}}, - {"sys_alert_record", "idx_alert_created", []string{"created_at", "id"}}, - {"sys_alert_operation_log", "idx_alert_log_time", []string{"alert_id", "created_at", "id"}}, - {"tab_spend", "idx_alert_spend_window", []string{"source_db", "promote_id", "pay_status", "pay_time", "pay_amount"}}, - {"tab_user", "idx_alert_user_window", []string{"source_db", "promote_id", "sw_register_time_unix", "puid"}}, - {"tab_promote", "idx_alert_promote_tree", []string{"source_db", "top_promote_id", "id"}}, - }) + return migrateQueryIndexes(db, alertV2QueryIndexes) } func migrateAlertV2UniqueKey(db *gorm.DB) error { @@ -131,3 +122,14 @@ func migrateAlertV2UniqueKey(db *gorm.DB) error { } return db.Exec("ALTER TABLE sys_alert_record " + strings.Join(clauses, ", ") + ", ALGORITHM=INPLACE, LOCK=NONE").Error } + +var alertV2QueryIndexes = []businessQueryIndex{ + {"sys_alert_record", "idx_alert_owner_status_time", []string{"business_id", "status", "created_at", "id"}}, + {"sys_alert_record", "idx_alert_channel_type_time", []string{"platform_code", "target_promote_id", "rule_type", "created_at", "id"}}, + {"sys_alert_record", "idx_alert_owner_time", []string{"business_id", "created_at", "id"}}, + {"sys_alert_record", "idx_alert_created", []string{"created_at", "id"}}, + {"sys_alert_operation_log", "idx_alert_log_time", []string{"alert_id", "created_at", "id"}}, + {"tab_spend", "idx_alert_spend_window", []string{"source_db", "promote_id", "pay_status", "pay_time", "pay_amount"}}, + {"tab_user", "idx_alert_user_window", []string{"source_db", "promote_id", "sw_register_time_unix", "puid"}}, + {"tab_promote", "idx_alert_promote_tree", []string{"source_db", "top_promote_id", "id"}}, +} diff --git a/internal/data/business/business_names.go b/internal/data/business/business_names.go index 718d26b..0627e2d 100644 --- a/internal/data/business/business_names.go +++ b/internal/data/business/business_names.go @@ -14,7 +14,7 @@ func businessDisplayPaths(db *gorm.DB, owners map[uint]string) (map[uint][]strin for owner, path := range owners { seen := map[uint]bool{} for _, part := range strings.Split(path, ",") { - id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32) + id, err := strconv.ParseUint(strings.TrimSpace(part), 10, strconv.IntSize) if err == nil && id > 0 && uint(id) != owner && !seen[uint(id)] { paths[owner] = append(paths[owner], uint(id)) ids[uint(id)], seen[uint(id)] = true, true diff --git a/internal/data/business/id_capacity.go b/internal/data/business/id_capacity.go new file mode 100644 index 0000000..4ae0a13 --- /dev/null +++ b/internal/data/business/id_capacity.go @@ -0,0 +1,62 @@ +package business + +import ( + "fmt" + "strings" + + "gorm.io/gorm" +) + +func migrateBindingStatusIndex(db *gorm.DB) error { + const name = "idx_sys_user_bind_platform_status" + if !db.Migrator().HasTable(&bindingPO{}) { + return nil + } + if db.Migrator().HasIndex(&bindingPO{}, name) { + return nil + } + return db.Migrator().CreateIndex(&bindingPO{}, name) +} + +// Preserve IDs and the high-water mark: lowering AUTO_INCREMENT could reuse +// a deleted ID that still appears in historical records or external systems. +func migratePromoteIDCapacity(db *gorm.DB) error { + if db.Dialector.Name() != "mysql" || !db.Migrator().HasTable("tab_promote") { + return nil + } + var column struct { + DataType, ColumnType, IsNullable, Extra, ColumnComment string + } + if err := db.Raw("SELECT data_type,column_type,is_nullable,extra,column_comment FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&column).Error; err != nil { + return err + } + if column.DataType == "bigint" { + return nil + } + if column.DataType != "int" || column.IsNullable != "NO" || column.Extra != "auto_increment" { + return fmt.Errorf("unexpected tab_promote.id definition; capacity migration stopped") + } + var next uint64 + if err := db.Raw("SELECT auto_increment FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='tab_promote'").Scan(&next).Error; err != nil { + return err + } + typeName := "BIGINT" + if strings.Contains(column.ColumnType, "unsigned") { + typeName += " UNSIGNED" + } + // MySQL 5.7 rebuilds the table when widening an integer. Execute before + // starting writers and retain the original next ID and column comment. + return db.Connection(func(conn *gorm.DB) error { + var mode string + if err := conn.Raw("SELECT @@session.sql_mode").Scan(&mode).Error; err != nil { + return err + } + comment := column.ColumnComment + if !strings.Contains(mode, "NO_BACKSLASH_ESCAPES") { + comment = strings.ReplaceAll(comment, `\`, `\\`) + } + comment = "'" + strings.ReplaceAll(comment, "'", "''") + "'" + // MySQL does not permit a prepared-statement placeholder in COMMENT. + return conn.Session(&gorm.Session{NewDB: true}).Exec(fmt.Sprintf("ALTER TABLE tab_promote MODIFY COLUMN id %s NOT NULL AUTO_INCREMENT COMMENT %s, AUTO_INCREMENT=%d", typeName, comment, next)).Error + }) +} diff --git a/internal/data/business/id_capacity_test.go b/internal/data/business/id_capacity_test.go new file mode 100644 index 0000000..bf08092 --- /dev/null +++ b/internal/data/business/id_capacity_test.go @@ -0,0 +1,51 @@ +package business + +import ( + "testing" +) + +func TestMySQLPromoteCapacityPreservesIDsAndNextValue(t *testing.T) { + db := openRehearsalMySQL(t) + if err := db.Exec("CREATE TABLE tab_promote(id INT NOT NULL AUTO_INCREMENT COMMENT '渠道主键',source_db VARCHAR(10) NOT NULL,PRIMARY KEY(id,source_db)) AUTO_INCREMENT=2147483647").Error; err != nil { + t.Fatal(err) + } + if err := db.Exec("INSERT INTO tab_promote(id,source_db) VALUES(29701,'lx'),(29701,'hw')").Error; err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + if err := migratePromoteIDCapacity(db); err != nil { + t.Fatal(err) + } + } + var next uint64 + if err := db.Raw("SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='tab_promote'").Scan(&next).Error; err != nil || next != 2147483647 { + t.Fatalf("high-water mark changed: %d, %v", next, err) + } + if err := db.Exec("INSERT INTO tab_promote(source_db) VALUES('lx'),('lx')").Error; err != nil { + t.Fatal(err) + } + var ids []uint64 + if err := db.Table("tab_promote").Order("id").Pluck("id", &ids).Error; err != nil || len(ids) != 4 || ids[0] != 29701 || ids[1] != 29701 || ids[2] != 2147483647 || ids[3] != 2147483648 { + t.Fatalf("IDs changed or overflowed: %v, %v", ids, err) + } + var comment string + if err := db.Raw("SELECT column_comment FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&comment).Error; err != nil || comment != "渠道主键" { + t.Fatalf("comment changed: %q, %v", comment, err) + } +} + +func TestBindingStatusIndexRepairsAnExistingLegacyTable(t *testing.T) { + db := openRehearsalMySQL(t) + const index = "idx_sys_user_bind_platform_status" + if err := db.Migrator().DropIndex(&bindingPO{}, index); err != nil { + t.Fatal(err) + } + for i := 0; i < 2; i++ { + if err := migrateBindingStatusIndex(db); err != nil { + t.Fatal(err) + } + } + if !db.Migrator().HasIndex(&bindingPO{}, index) { + t.Fatal("legacy status index was not created") + } +} diff --git a/internal/data/business/ledger.go b/internal/data/business/ledger.go index a986138..f16feae 100644 --- a/internal/data/business/ledger.go +++ b/internal/data/business/ledger.go @@ -385,7 +385,7 @@ func fillLedgerNames(db *gorm.DB, scope business.LedgerScope, filter business.Le paths := map[business.PlatformKey][]uint{} for key, binding := range names { for _, part := range strings.Split(binding.IDPath, ",") { - id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32) + id, err := strconv.ParseUint(strings.TrimSpace(part), 10, strconv.IntSize) if err == nil && id > 0 && uint(id) != binding.UserID { paths[key] = append(paths[key], uint(id)) ancestorIDs[uint(id)] = true diff --git a/internal/data/business/migration_schema.go b/internal/data/business/migration_schema.go new file mode 100644 index 0000000..634b134 --- /dev/null +++ b/internal/data/business/migration_schema.go @@ -0,0 +1,91 @@ +package business + +import ( + "fmt" + "strings" + + "gorm.io/gorm" + "kra/pkg/database/migration" +) + +// These source tables carry history. An incomplete copy must not be replaced +// with empty tables. SDK and OA schemas belong to their respective databases. +var importedTables = []string{ + "tab_game", "tab_game_server", "tab_promote", "tab_promote_apply", + "tab_spend", "tab_user", "tab_user_day_login", "tab_user_play_info", + "tab_sw_report_day", "tab_sw_report_day_key", "tab_sw_report_server_day", "tab_sw_report_server_day_key", +} + +func CheckPrerequisites(db *gorm.DB) error { + if db.Dialector.Name() != "mysql" { + return nil + } + if db.Migrator().HasTable("sys_user") && !db.Migrator().HasTable("sys_users") { + return fmt.Errorf("legacy framework conversion must run before incremental migrations") + } + var missing []string + for _, table := range importedTables { + if !db.Migrator().HasTable(table) { + missing = append(missing, table) + } + } + if len(missing) > 0 { + return fmt.Errorf("incomplete legacy copy, missing tables: %s", strings.Join(missing, ", ")) + } + return nil +} + +func CheckSchema(db *gorm.DB) error { + if err := CheckPrerequisites(db); err != nil { + return err + } + if err := migration.CheckModels(db, + &preferencePO{}, &bindingPO{}, &bindingLockPO{}, &resetCodePO{}, &resetTokenPO{}, + &alertRulePO{}, &alertRecordPO{}, &alertLogPO{}, &alertGapPO{}, &alertExemptionPO{}, &alertChannelLockPO{}, + &alertTriggerLockPO{}, &reportBuildLockPO{}, &businessGroupPO{}, &businessGroupMemberPO{}, + &businessMemberProfilePO{}, &businessGroupStatePO{}, &performanceGoalPO{}, &effectivenessStatePO{}, &effectivenessPO{}, + ); err != nil { + return err + } + if db.Dialector.Name() != "mysql" { + return nil + } + indexes := append([]businessQueryIndex{}, businessQueryIndexes...) + var idType string + if err := db.Raw("SELECT data_type FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_promote' AND column_name='id'").Scan(&idType).Error; err != nil { + return err + } + if idType != "bigint" { + return fmt.Errorf("tab_promote.id must be BIGINT after capacity migration") + } + indexes = append(indexes, reportBuildQueryIndexes...) + indexes = append(indexes, alertV2QueryIndexes...) + indexes = append(indexes, + businessQueryIndex{"tab_spend", "idx_sw_spend_effective_scan", []string{"promote_id", "source_db", "pay_status", "pay_time", "id", "pay_amount"}}, + businessQueryIndex{"tab_promote_apply", "idx_sw_apply_game_scope", []string{"game_id", "source_db", "status", "promote_id"}}, + businessQueryIndex{"tab_user", "idx_sw_user_registration", []string{"source_db", "puid", "sw_register_time_unix", "id"}}, + businessQueryIndex{"tab_user_play_info", "idx_sw_play_create_time", []string{"create_time"}}, + businessQueryIndex{"sys_alert_record", "uk_alert_record_v2_open", []string{"business_id", "platform_code", "target_promote_id", "rule_type", "v2_open_key"}}, + ) + for _, index := range indexes { + var existing []struct { + ColumnName string + NonUnique int + SubPart *int + } + if err := db.Raw("SELECT column_name,non_unique,sub_part FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND index_name=? ORDER BY seq_in_index", index.table, index.name).Scan(&existing).Error; err != nil { + return err + } + columns := []string{} + for _, field := range existing { + if field.SubPart != nil || index.name == "uk_alert_record_v2_open" && field.NonUnique != 0 { + return fmt.Errorf("invalid index %s.%s", index.table, index.name) + } + columns = append(columns, field.ColumnName) + } + if strings.Join(columns, ",") != strings.Join(index.columns, ",") { + return fmt.Errorf("missing or incompatible index %s.%s", index.table, index.name) + } + } + return nil +} diff --git a/internal/data/business/migrations.go b/internal/data/business/migrations.go index 31b0239..29121fc 100644 --- a/internal/data/business/migrations.go +++ b/internal/data/business/migrations.go @@ -6,7 +6,7 @@ import ( ) func Migrations() []migration.Step { - return []migration.Step{{ID: "202611050020_business_alert_v2", Migrate: migrateAlertV2}, {ID: "202611040017_business_groups_goals_effectiveness", Migrate: migrateBusinessManagement}, {ID: "202611040018_business_effectiveness_query_index", Migrate: migrateEffectivenessQueryIndex}, {ID: "202611040015_business_approved_game_index", Migrate: migrateApprovedGameIndexes}, {ID: "202611040014_business_report_build_indexes", Migrate: migrateReportBuildQueryIndexes}, {ID: "202611040013_business_query_indexes", Migrate: migrateBusinessQueryIndexes}, {ID: "202611040012_business_server_report_index", Migrate: migrateServerReportIndex}, {ID: "202611040011_business_registration_index", Migrate: migrateRegistrationIndex}, {ID: "202611040010_business_alert_trigger_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &alertTriggerLockPO{}) }}, {ID: "202611040009_business_menu_icons", Migrate: migrateBusinessMenuIcons}, {ID: "202611040008_business_report_build_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &reportBuildLockPO{}) }}, {ID: "202611030007_business_alert_attachments", Migrate: migrateAlertAttachments}, {ID: "202611030006_business_alert_schema", Migrate: migrateAlertSchema}, {ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error { + return []migration.Step{{ID: "202611060023_business_binding_status_index", Migrate: migrateBindingStatusIndex}, {ID: "202611060022_business_promote_id_capacity", Migrate: migratePromoteIDCapacity}, {ID: "202611050020_business_alert_v2", Migrate: migrateAlertV2}, {ID: "202611040017_business_groups_goals_effectiveness", Migrate: migrateBusinessManagement}, {ID: "202611040018_business_effectiveness_query_index", Migrate: migrateEffectivenessQueryIndex}, {ID: "202611040015_business_approved_game_index", Migrate: migrateApprovedGameIndexes}, {ID: "202611040014_business_report_build_indexes", Migrate: migrateReportBuildQueryIndexes}, {ID: "202611040013_business_query_indexes", Migrate: migrateBusinessQueryIndexes}, {ID: "202611040012_business_server_report_index", Migrate: migrateServerReportIndex}, {ID: "202611040011_business_registration_index", Migrate: migrateRegistrationIndex}, {ID: "202611040010_business_alert_trigger_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &alertTriggerLockPO{}) }}, {ID: "202611040009_business_menu_icons", Migrate: migrateBusinessMenuIcons}, {ID: "202611040008_business_report_build_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &reportBuildLockPO{}) }}, {ID: "202611030007_business_alert_attachments", Migrate: migrateAlertAttachments}, {ID: "202611030006_business_alert_schema", Migrate: migrateAlertSchema}, {ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &resetCodePO{}, &resetTokenPO{}) }}, {ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error { return migration.CreateMissingTables(db, &bindingLockPO{}) diff --git a/internal/data/config_store.go b/internal/data/config_store.go index cce2678..675d12f 100644 --- a/internal/data/config_store.go +++ b/internal/data/config_store.go @@ -145,6 +145,9 @@ func (d *Data) reloadConfig(ctx context.Context) error { var undo rollback defer undo.run() databaseReady := databaseConnectionConfigured(next.Data.Database) + if !databaseReady && next.Admin.System != nil && next.Admin.System.DisableAutoMigrate { + return fmt.Errorf("automatic migration is disabled; an existing migrated database is required") + } var candidateDB *gorm.DB if databaseReady { candidateDB, err = openDatabase(next.Data.Database, false, "", d.logger()) @@ -170,6 +173,11 @@ func (d *Data) reloadConfig(ctx context.Context) error { return fmt.Errorf("reload database migrations: %w", err) } } + if databaseReady && next.Admin.System != nil && next.Admin.System.DisableAutoMigrate { + if err = checkMigratedSchema(candidateDB.WithContext(ctx), d.catalog); err != nil { + return fmt.Errorf("reload database schema: %w", err) + } + } storageConfig, err := dataintegration.ResolveStorageConfig(candidateDB.WithContext(ctx), next.Admin.Storage) if err != nil { return fmt.Errorf("reload storage configuration: %w", err) diff --git a/internal/data/data.go b/internal/data/data.go index bd6854c..67ea3d9 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -246,12 +246,20 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor undo.add(cleanup) defer undo.run() usingFallback := !databaseConnectionConfigured(c.Database) + admin := runtime.Admin() + if admin == nil { + admin = &config.Admin{} + } + disableAutoMigrate := admin.System != nil && admin.System.DisableAutoMigrate var db *gorm.DB var err error if !usingFallback { db, err = openDatabase(c.Database, false, "", appLogger) } if usingFallback || err != nil { + if disableAutoMigrate { + return nil, nil, fmt.Errorf("automatic migration is disabled; an available migrated database is required") + } // The initialization endpoint must remain available when the configured // target database has not been created yet. if err != nil { @@ -274,16 +282,16 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor for _, item := range d.dbList { registerDataScopeCallbacks(item, d.enqueueDataScopeAudit) } - admin := runtime.Admin() - if admin == nil { - admin = &config.Admin{} - } - disableAutoMigrate := admin.System != nil && admin.System.DisableAutoMigrate if !usingFallback && !disableAutoMigrate { if err = migrateAll(db, catalog); err != nil { return nil, nil, fmt.Errorf("migrate tables: %w", err) } } + if !usingFallback && disableAutoMigrate { + if err = checkMigratedSchema(db, catalog); err != nil { + return nil, nil, err + } + } if !usingFallback { storageConfig, storageErr := dataintegration.ResolveStorageConfig(db, admin.Storage) if storageErr != nil { diff --git a/internal/data/integration/migrations.go b/internal/data/integration/migrations.go index f7c6507..15500c2 100644 --- a/internal/data/integration/migrations.go +++ b/internal/data/integration/migrations.go @@ -151,3 +151,5 @@ func ensurePaymentIntegrationConfigs(db *gorm.DB) error { } return nil } + +func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, &ConfigPO{}) } diff --git a/internal/data/migrate.go b/internal/data/migrate.go new file mode 100644 index 0000000..4d653c7 --- /dev/null +++ b/internal/data/migrate.go @@ -0,0 +1,79 @@ +package data + +import ( + "context" + "fmt" + "log/slog" + "time" + + "gorm.io/gorm" + "kra/internal/config" + "kra/pkg/database/migration" + "kra/pkg/module" +) + +// MigrateDatabase opens only the configured primary database. It does not +// create a database, start a scheduler, open SDK clients, or initialize users. +func MigrateDatabase(ctx context.Context, c *config.Database, catalog module.Catalog, logger *slog.Logger, checkOnly bool) error { + if !databaseConnectionConfigured(c) { + return fmt.Errorf("migration requires an existing configured database") + } + migrationConfig := *c + // One connection holds the advisory lock, a second executes migrations. + if !checkOnly && migrationConfig.MaxOpenConns > 0 && migrationConfig.MaxOpenConns < 2 { + migrationConfig.MaxOpenConns = 2 + } + db, err := openDatabase(&migrationConfig, false, "", logger) + if err != nil { + return fmt.Errorf("open migration database: %w", err) + } + defer closeGormDB(db) + db = db.WithContext(ctx) + if checkOnly { + return checkMigratedSchema(db, catalog) + } + // A dedicated connection owns the lock even when a migration uses a + // transaction or temporarily changes connection-local SQL compatibility. + run := func() error { + if err := catalog.CheckPrerequisites(db); err != nil { + return err + } + if err := migrateAll(db, catalog); err != nil { + return err + } + return checkMigratedSchema(db, catalog) + } + if db.Dialector.Name() != "mysql" { + return run() + } + return db.Connection(func(conn *gorm.DB) error { + var name string + if err := conn.Raw("SELECT CONCAT('sw_to_kra:', MD5(DATABASE()))").Scan(&name).Error; err != nil { + return err + } + var locked int + if err := conn.Session(&gorm.Session{NewDB: true}).Raw("SELECT GET_LOCK(?, 0)", name).Scan(&locked).Error; err != nil { + return err + } + if locked != 1 { + return fmt.Errorf("another migration owns the database lock") + } + defer func() { + cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + conn.Session(&gorm.Session{NewDB: true}).WithContext(cleanup).Exec("DO RELEASE_LOCK(?)", name) + }() + return run() + }) +} + +func checkMigratedSchema(db *gorm.DB, catalog module.Catalog) error { + db = db.Session(&gorm.Session{NewDB: true}) + if err := migration.CheckApplied(db, catalog.MigrationSteps()); err != nil { + return fmt.Errorf("run -migrate before starting the service: %w", err) + } + if err := catalog.CheckSchema(db); err != nil { + return fmt.Errorf("database schema is incomplete: %w", err) + } + return nil +} diff --git a/internal/data/migrations_test.go b/internal/data/migrations_test.go index 7461aba..5f57afa 100644 --- a/internal/data/migrations_test.go +++ b/internal/data/migrations_test.go @@ -25,6 +25,9 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) { if err = migrateAll(db, testCatalog()); err != nil { t.Fatalf("second migration run: %v", err) } + if err := checkMigratedSchema(db, testCatalog()); err != nil { + t.Fatalf("complete migration must satisfy every module schema: %v", err) + } for _, table := range []string{"sys_integration_configs", "sys_users", "sys_base_menus", "pay_orders", "pay_events"} { if !db.Migrator().HasTable(table) { t.Fatalf("migration did not create %s", table) diff --git a/internal/data/payment/migrations.go b/internal/data/payment/migrations.go index f0b39a7..24ae21b 100644 --- a/internal/data/payment/migrations.go +++ b/internal/data/payment/migrations.go @@ -16,3 +16,7 @@ func Migrations() []migration.Step { }}, } } + +func CheckSchema(db *gorm.DB) error { + return migration.CheckModels(db, &paymentOrderPO{}, &paymentEventPO{}) +} diff --git a/internal/data/system/migrations.go b/internal/data/system/migrations.go index ba18591..3b6762b 100644 --- a/internal/data/system/migrations.go +++ b/internal/data/system/migrations.go @@ -16,16 +16,7 @@ func Migrations() []migration.Step { { ID: "202608200002_system_schema", Migrate: func(db *gorm.DB) error { - return migration.CreateMissingTables(db, - &userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{}, - &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{}, - &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, - &dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{}, - &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, - &operationPO{}, &loginLogPO{}, &DataAccessLogPO{}, &errorRecordPO{}, - &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, - &announcementPO{}, - ) + return migration.CreateMissingTables(db, schemaModels()...) }, }, {ID: "202608210002_communication_surface", Migrate: ensureCommunicationSurface}, diff --git a/internal/data/system/schema.go b/internal/data/system/schema.go new file mode 100644 index 0000000..0531023 --- /dev/null +++ b/internal/data/system/schema.go @@ -0,0 +1,20 @@ +package system + +import ( + "gorm.io/gorm" + "kra/pkg/database/migration" +) + +func schemaModels() []any { + return []any{ + &userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{}, + &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{}, + &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, + &dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{}, + &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, + &operationPO{}, &loginLogPO{}, &DataAccessLogPO{}, &errorRecordPO{}, + &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{}, + } +} + +func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, schemaModels()...) } diff --git a/internal/data/task/migrations.go b/internal/data/task/migrations.go index c9f408c..5adaf70 100644 --- a/internal/data/task/migrations.go +++ b/internal/data/task/migrations.go @@ -38,3 +38,5 @@ func SeedDefaults(ctx context.Context, db *gorm.DB, defaults []platformmodule.Ti return nil }) } + +func CheckSchema(db *gorm.DB) error { return migration.CheckModels(db, &taskPO{}, &taskLogPO{}) } diff --git a/internal/modules/business/definition.go b/internal/modules/business/definition.go index b2020fc..a70edee 100644 --- a/internal/modules/business/definition.go +++ b/internal/modules/business/definition.go @@ -106,7 +106,7 @@ func Definition() module.Definition { }) }}) migrations = append(migrations, migration.Step{ID: "202611050021_business_alert_surface", Migrate: migrateAlertSurface}) - definition := module.Definition{Name: "business", Migrations: migrations, TimedTasks: append([]module.TimedTask{alertDetectionTask}, tasks...), Surface: module.Surface{ + definition := module.Definition{Name: "business", Migrations: migrations, Prerequisites: databusiness.CheckPrerequisites, ValidateSchema: databusiness.CheckSchema, TimedTasks: append([]module.TimedTask{alertDetectionTask}, tasks...), Surface: module.Surface{ Menus: []module.Menu{ alertExemptionMenu, performanceMenu, diff --git a/internal/modules/catalog_test.go b/internal/modules/catalog_test.go index 4890689..a3bdb48 100644 --- a/internal/modules/catalog_test.go +++ b/internal/modules/catalog_test.go @@ -25,8 +25,8 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) { t.Fatalf("definition[%d] = %q, want %q", index, got, name) } } - if got := catalog.MigrationSteps(); len(got) != 35 { - t.Fatalf("module migrations = %d, want 35", len(got)) + if got := catalog.MigrationSteps(); len(got) != 37 { + t.Fatalf("module migrations = %d, want 37", len(got)) } var menus, apis int for _, definition := range catalog.Definitions { diff --git a/internal/modules/integration/definition.go b/internal/modules/integration/definition.go index 3f4f678..85ed749 100644 --- a/internal/modules/integration/definition.go +++ b/internal/modules/integration/definition.go @@ -20,8 +20,9 @@ func Definition() module.Definition { }, 888) }}) return module.Definition{ - Name: "integration", - Migrations: migrations, + Name: "integration", + ValidateSchema: dataintegration.CheckSchema, + Migrations: migrations, Surface: module.Surface{ Menus: []module.Menu{ {Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}, diff --git a/internal/modules/payment/definition.go b/internal/modules/payment/definition.go index 82cacb1..3feb2dd 100644 --- a/internal/modules/payment/definition.go +++ b/internal/modules/payment/definition.go @@ -56,8 +56,9 @@ func Definition() module.Definition { return datasystem.GrantMenuAPIs(db, "paymentOrders", orderReadAPIs) }}) return module.Definition{ - Name: "payment", - Migrations: migrations, - Surface: adminSurface, + Name: "payment", + ValidateSchema: datapayment.CheckSchema, + Migrations: migrations, + Surface: adminSurface, } } diff --git a/internal/modules/system/definition.go b/internal/modules/system/definition.go index c1a2a75..f082ea1 100644 --- a/internal/modules/system/definition.go +++ b/internal/modules/system/definition.go @@ -11,7 +11,8 @@ import ( // catalog. Other business modules can expose the same shape independently. func Definition() module.Definition { return module.Definition{ - Name: "system", - Migrations: datasystem.Migrations(), + Name: "system", + Migrations: datasystem.Migrations(), + ValidateSchema: datasystem.CheckSchema, } } diff --git a/internal/modules/task/definition.go b/internal/modules/task/definition.go index 7968f38..1e3fd90 100644 --- a/internal/modules/task/definition.go +++ b/internal/modules/task/definition.go @@ -8,9 +8,10 @@ import ( func Definition() module.Definition { return module.Definition{ - Name: "task", - Migrations: datatask.Migrations(), - TimedTasks: TimedTasks(), + Name: "task", + ValidateSchema: datatask.CheckSchema, + Migrations: datatask.Migrations(), + TimedTasks: TimedTasks(), } } diff --git a/internal/service/business/binding.go b/internal/service/business/binding.go index e93ea7a..7e53bb4 100644 --- a/internal/service/business/binding.go +++ b/internal/service/business/binding.go @@ -117,5 +117,5 @@ func (s *BindingService) Unbind(ctx context.Context, req *dto.BusinessBindingReq if err != nil { return err } - return s.uc.Unbind(ctx, business.PlatformKey{SourceDB: source, BusierID: req.PlatformBusierID}) + return s.uc.Unbind(ctx, business.PlatformKey{SourceDB: source, BusierID: uint64(req.PlatformBusierID)}) } diff --git a/internal/service/business/ledger_excel.go b/internal/service/business/ledger_excel.go index 68fe101..6038c2b 100644 --- a/internal/service/business/ledger_excel.go +++ b/internal/service/business/ledger_excel.go @@ -21,6 +21,12 @@ func optionalNumber[T ~int | ~int64](value *T) ledgerCell { } return numericCell(strconv.FormatInt(int64(*value), 10)) } +func optionalID(value *int64) ledgerCell { + if value == nil { + return textCell("") + } + return textCell(strconv.FormatInt(*value, 10)) +} func optionalMoney(value *string) ledgerCell { if value == nil { return textCell("") @@ -83,7 +89,7 @@ func (s *LedgerService) ExportRegistrations(ctx context.Context, req *dto.Ledger rows := make([][]ledgerCell, 0, len(items)) for _, item := range items { r := registrationResponse(item) - rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), numericCell(strconv.FormatUint(r.ID, 10)), numericCell(strconv.FormatUint(r.UserID, 10)), optionalNumber(r.RegisterTime), optionalNumber(r.FirstGameID), textCell(r.FirstGameName), optionalMoney(r.TotalPayAmount), textCell(r.RegisterIP), textCell(r.DeviceName), textCell(r.PromoteAccount), optionalNumber(r.PromoteID), textCell(r.BusierName)}) + rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), textCell(strconv.FormatUint(r.ID, 10)), textCell(strconv.FormatUint(r.UserID, 10)), optionalNumber(r.RegisterTime), optionalID(r.FirstGameID), textCell(r.FirstGameName), optionalMoney(r.TotalPayAmount), textCell(r.RegisterIP), textCell(r.DeviceName), textCell(r.PromoteAccount), optionalID(r.PromoteID), textCell(r.BusierName)}) } return ledgerWorkbook("注册明细", []string{"sourceDb", "所属平台", "ID", "玩家ID", "注册时间", "fgameId", "游戏", "累计充值", "注册IP", "机型", "渠道", "promoteId", "所属商务"}, rows) } @@ -99,7 +105,7 @@ func (s *LedgerService) ExportSpends(ctx context.Context, req *dto.LedgerQuery) rows := make([][]ledgerCell, 0, len(items)) for _, item := range items { r := spendResponse(item) - rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), numericCell(strconv.FormatUint(r.ID, 10)), optionalNumber(r.UserID), textCell(r.OrderNumber), optionalNumber(r.GameID), textCell(r.GameName), textCell(r.ServerID), textCell(r.ServerName), textCell(r.PlayerName), optionalMoney(r.PayAmount), optionalNumber(r.PayTime), optionalNumber(r.PayWay), optionalNumber(r.PayStatus), textCell(r.IP), textCell(r.PromoteAccount), optionalNumber(r.PromoteID), textCell(r.BusierName)}) + rows = append(rows, []ledgerCell{textCell(r.SourceDB), textCell(r.SourceDBLabel), textCell(strconv.FormatUint(r.ID, 10)), optionalID(r.UserID), textCell(r.OrderNumber), optionalID(r.GameID), textCell(r.GameName), textCell(r.ServerID), textCell(r.ServerName), textCell(r.PlayerName), optionalMoney(r.PayAmount), optionalNumber(r.PayTime), optionalNumber(r.PayWay), optionalNumber(r.PayStatus), textCell(r.IP), textCell(r.PromoteAccount), optionalID(r.PromoteID), textCell(r.BusierName)}) } return ledgerWorkbook("充值明细", []string{"sourceDb", "所属平台", "ID", "玩家ID", "订单号", "gameId", "游戏", "serverId", "区服", "角色", "实付金额", "完成时间", "方式", "状态", "IP", "渠道", "promoteId", "所属商务"}, rows) } diff --git a/internal/service/business/ledger_excel_test.go b/internal/service/business/ledger_excel_test.go index 0300f3d..7e82ea4 100644 --- a/internal/service/business/ledger_excel_test.go +++ b/internal/service/business/ledger_excel_test.go @@ -15,11 +15,11 @@ type ledgerExportFake struct{ LedgerUsecase } func (ledgerExportFake) ExportRegistrations(context.Context, business.LedgerFilter) ([]*business.Registration, error) { amount := "9007199254740993.10" stamp := int64(1793577600) - return []*business.Registration{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 7}, RegisterTime: &stamp, FirstGameName: "游戏", TotalPayAmount: &amount, LedgerNames: business.LedgerNames{PromoteAccount: "=1+1"}}}, nil + return []*business.Registration{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 9007199254740993}, RegisterTime: &stamp, FirstGameName: "游戏", TotalPayAmount: &amount, LedgerNames: business.LedgerNames{PromoteAccount: "=1+1"}}}, nil } func (ledgerExportFake) ExportSpends(context.Context, business.LedgerFilter) ([]*business.Spend, error) { amount := "123.45" - return []*business.Spend{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 8}, OrderNumber: "000000000000000001", PayAmount: &amount}}, nil + return []*business.Spend{{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 9007199254740995}, OrderNumber: "000000000000000001", PayAmount: &amount}}, nil } func TestLedgerWorkbookPreservesNumbersAndText(t *testing.T) { s := &LedgerService{uc: ledgerExportFake{}} @@ -41,6 +41,13 @@ func TestLedgerWorkbookPreservesNumbersAndText(t *testing.T) { if amount != "9007199254740993.10" || formula != "" || text != "=1+1" { t.Fatalf("导出丢失金额精度或把账号当成公式: %s %s %s", amount, formula, text) } + for _, cell := range []string{"C2", "D2"} { + value, _ := f.GetCellValue("注册明细", cell) + kind, _ := f.GetCellType("注册明细", cell) + if value != "9007199254740993" || kind != excelize.CellTypeSharedString { + t.Fatalf("ID must be exact text: %s %v", value, kind) + } + } rows, err := f.GetRows("注册明细") if err != nil || len(rows) != 2 || rows[0][0] != "sourceDb" || rows[0][12] != "所属商务" { t.Fatal("原导出字段与顺序丢失") diff --git a/internal/service/dto/analytics.go b/internal/service/dto/analytics.go index 96183cd..5cfb4bc 100644 --- a/internal/service/dto/analytics.go +++ b/internal/service/dto/analytics.go @@ -26,7 +26,7 @@ type AnalyticsMetrics struct { type ChannelAnalyticsRow struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - PromoteID *int64 `json:"promoteId"` + PromoteID *int64 `json:"promoteId,string"` PromoteName string `json:"promoteName"` PromoteType *int `json:"promoteType"` BusierName string `json:"busierName"` @@ -35,7 +35,7 @@ type ChannelAnalyticsRow struct { type GameAnalyticsRow struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - GameID *int64 `json:"gameId"` + GameID *int64 `json:"gameId,string"` GameName string `json:"gameName"` AnalyticsMetrics ARPU string `json:"arpu"` diff --git a/internal/service/dto/business_binding.go b/internal/service/dto/business_binding.go index 324d084..3cf025b 100644 --- a/internal/service/dto/business_binding.go +++ b/internal/service/dto/business_binding.go @@ -11,13 +11,13 @@ type BusinessBindingRequest struct { SourceDB string `json:"sourceDb"` Account string `json:"account"` Password string `json:"password"` - PlatformBusierID uint64 `json:"platformBusierId"` + PlatformBusierID ID `json:"platformBusierId"` } type BusinessBindingResponse struct { UserID uint `json:"userId"` SourceDB string `json:"sourceDb"` PlatformName string `json:"platformName"` - PlatformBusierID uint64 `json:"platformBusierId"` + PlatformBusierID uint64 `json:"platformBusierId,string"` Account string `json:"platformBusierAccount"` UserName string `json:"bindUserName"` PromoteCount *int64 `json:"promoteCount"` diff --git a/internal/service/dto/business_portrait.go b/internal/service/dto/business_portrait.go index 7316d84..923544f 100644 --- a/internal/service/dto/business_portrait.go +++ b/internal/service/dto/business_portrait.go @@ -45,7 +45,7 @@ type PortraitRank struct { Rank int `json:"rank"` SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - PromoteID uint64 `json:"promoteId"` + PromoteID uint64 `json:"promoteId,string"` PromoteName string `json:"promoteName"` Value string `json:"value"` } diff --git a/internal/service/dto/business_promote.go b/internal/service/dto/business_promote.go index 773aa83..dd589cb 100644 --- a/internal/service/dto/business_promote.go +++ b/internal/service/dto/business_promote.go @@ -7,11 +7,11 @@ type BusinessPromoteQuery struct { PlatformBusierID uint64 `form:"platformBusierId"` } type BusinessPromoteResponse struct { - PromoteID uint64 `json:"promoteId"` + PromoteID uint64 `json:"promoteId,string"` PromoteName string `json:"promoteName"` SourceDB string `json:"sourceDb"` PlatformName string `json:"platformName"` - PlatformBusierID uint64 `json:"platformBusierId"` + PlatformBusierID uint64 `json:"platformBusierId,string"` } type BusinessPromotePage struct { Rows []*BusinessPromoteResponse `json:"rows"` diff --git a/internal/service/dto/channel.go b/internal/service/dto/channel.go index 64ee73d..5ebb7fc 100644 --- a/internal/service/dto/channel.go +++ b/internal/service/dto/channel.go @@ -30,17 +30,17 @@ type ChannelResponse struct { LastLoginAt *int64 `json:"lastLoginAt"` Phone string `json:"phone"` SettlementDays *int `json:"settlementDays"` - ID uint64 `json:"id"` + ID uint64 `json:"id,string"` SourceDB string `json:"sourceDb"` PlatformName string `json:"platformName"` Account string `json:"account"` Nickname string `json:"nickname"` Level *int `json:"promoteLevel"` - ParentID uint64 `json:"parentId"` + ParentID uint64 `json:"parentId,string"` ParentName string `json:"parentName"` RealName string `json:"realName"` RegisterType *int `json:"registerType"` - BusierID uint64 `json:"busierId"` + BusierID uint64 `json:"busierId,string"` BusierUserID *uint `json:"busierUserId"` BusierDisplayName string `json:"busierDisplayName"` Status *int `json:"status"` @@ -62,7 +62,7 @@ type ChannelStatusCounts struct { } type ChannelBindOption struct { SourceDB string `json:"sourceDb"` - PlatformBusierID uint64 `json:"platformBusierId"` + PlatformBusierID uint64 `json:"platformBusierId,string"` Account string `json:"platformBusierAccount"` UserID uint `json:"userId"` } @@ -74,7 +74,7 @@ type ChannelCreateRequest struct { RegisterType *int `json:"registerType"` } type ChannelCreateResult struct { - ID uint64 `json:"id"` + ID uint64 `json:"id,string"` SourceDB string `json:"sourceDb"` } type ChannelGameQuery struct { @@ -82,7 +82,7 @@ type ChannelGameQuery struct { PromoteID uint64 `form:"promoteId"` } type ChannelGameResponse struct { - GameID uint64 `json:"gameId"` + GameID uint64 `json:"gameId,string"` GameName string `json:"gameName"` RegisterCount int64 `json:"registerCount"` PayAmount string `json:"payAmount"` diff --git a/internal/service/dto/channel_portrait.go b/internal/service/dto/channel_portrait.go index bae5153..4ce22a0 100644 --- a/internal/service/dto/channel_portrait.go +++ b/internal/service/dto/channel_portrait.go @@ -6,7 +6,7 @@ type ChannelPortraitQuery struct { Period string `form:"period"` } type ChannelPortraitBasic struct { - PromoteID uint64 `json:"promoteId"` + PromoteID uint64 `json:"promoteId,string"` SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` Account string `json:"account"` @@ -26,7 +26,7 @@ type ChannelPortraitBasic struct { LastLoginTime *string `json:"lastLoginTime"` BusierUserID *uint `json:"busierUserId"` BusierDisplayName string `json:"busierDisplayName"` - ParentPromoteID *uint64 `json:"parentPromoteId"` + ParentPromoteID *uint64 `json:"parentPromoteId,string"` ParentPromoteName *string `json:"parentPromoteName"` ParentPromoteAccount *string `json:"parentPromoteAccount"` GameCount *int64 `json:"gameCount"` diff --git a/internal/service/dto/id.go b/internal/service/dto/id.go new file mode 100644 index 0000000..eb024f8 --- /dev/null +++ b/internal/service/dto/id.go @@ -0,0 +1,38 @@ +package dto + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// ID accepts legacy JSON integers and decimal strings. Its wire representation +// is always a string so a browser can round-trip all uint64 values exactly. +type ID uint64 + +func (id ID) MarshalJSON() ([]byte, error) { + return json.Marshal(strconv.FormatUint(uint64(id), 10)) +} + +func (id *ID) UnmarshalJSON(raw []byte) error { + text := string(raw) + if len(raw) > 0 && raw[0] == '"' { + if err := json.Unmarshal(raw, &text); err != nil { + return err + } + } + if text == "" { + return fmt.Errorf("ID must be a decimal integer") + } + for _, digit := range text { + if digit < '0' || digit > '9' { + return fmt.Errorf("ID must be a decimal integer") + } + } + value, err := strconv.ParseUint(text, 10, 64) + if err != nil { + return fmt.Errorf("ID is outside the uint64 range") + } + *id = ID(value) + return nil +} diff --git a/internal/service/dto/id_test.go b/internal/service/dto/id_test.go new file mode 100644 index 0000000..4ba047a --- /dev/null +++ b/internal/service/dto/id_test.go @@ -0,0 +1,68 @@ +package dto + +import ( + "encoding/json" + "math" + "strconv" + "testing" +) + +func TestIDRoundTripsDecimalStringsAndLegacyNumbers(t *testing.T) { + for _, value := range []uint64{1, math.MaxInt32, math.MaxInt32 + 1, 1<<53 - 1, 1 << 53, 1<<53 + 1, math.MaxInt64, math.MaxUint64} { + want := strconv.FormatUint(value, 10) + for _, raw := range []string{want, strconv.Quote(want)} { + var request BusinessBindingRequest + if err := json.Unmarshal([]byte(`{"platformBusierId":`+raw+`}`), &request); err != nil || uint64(request.PlatformBusierID) != value { + t.Fatalf("decode %s: %v, %d", raw, err, request.PlatformBusierID) + } + encoded, err := json.Marshal(request.PlatformBusierID) + if err != nil || string(encoded) != strconv.Quote(want) { + t.Fatalf("encode %s: %s, %v", raw, encoded, err) + } + } + } + for _, raw := range []string{`null`, `true`, `-1`, `1.5`, `1e3`, `18446744073709551616`, `""`, `"-1"`, `"1.0"`, `"1e3"`, `" 1"`, `"+1"`, `"18446744073709551616"`} { + var id ID + if err := json.Unmarshal([]byte(raw), &id); err == nil { + t.Fatalf("accepted invalid ID %s", raw) + } + } +} + +func TestLegacyResponsesPreserveLargeIDsAndNumericMetrics(t *testing.T) { + const value = uint64(1<<53 + 1) + signed := int64(value) + for _, response := range []any{ + ChannelResponse{ID: value, ParentID: value, BusierID: value}, + ChannelCreateResult{ID: value}, ChannelBindOption{PlatformBusierID: value}, + ChannelGameResponse{GameID: value, RegisterCount: 3}, + BusinessBindingResponse{PlatformBusierID: value}, + BusinessPromoteResponse{PromoteID: value, PlatformBusierID: value}, + ChannelPortraitBasic{PromoteID: value, ParentPromoteID: func() *uint64 { v := value; return &v }()}, + RegistrationResponse{ID: value, UserID: value, FirstGameID: &signed, PromoteID: &signed}, + SpendResponse{ID: value, UserID: &signed, GameID: &signed, PromoteID: &signed}, + ChannelAnalyticsRow{PromoteID: &signed}, GameAnalyticsRow{GameID: &signed}, + PerformanceChannel{ID: value}, PortraitRank{PromoteID: value}, + } { + raw, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + var browser map[string]any + if err := json.Unmarshal(raw, &browser); err != nil { + t.Fatal(err) + } + for _, key := range []string{"id", "userId", "parentId", "busierId", "platformBusierId", "promoteId", "parentPromoteId", "gameId", "fgameId"} { + actual, exists := browser[key] + if !exists || actual == nil || actual == float64(0) || actual == "0" { + continue + } + if actual != "9007199254740993" { + t.Fatalf("%T.%s lost ID: %v", response, key, actual) + } + } + if count, ok := browser["registerCount"]; ok && count != float64(3) { + t.Fatalf("metric changed type: %v", count) + } + } +} diff --git a/internal/service/dto/ledger.go b/internal/service/dto/ledger.go index 9325f63..39dca71 100644 --- a/internal/service/dto/ledger.go +++ b/internal/service/dto/ledger.go @@ -22,25 +22,25 @@ type LedgerQuery struct { type RegistrationResponse struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - ID uint64 `json:"id"` - UserID uint64 `json:"userId"` + ID uint64 `json:"id,string"` + UserID uint64 `json:"userId,string"` RegisterTime *int64 `json:"registerTime"` - FirstGameID *int64 `json:"fgameId"` + FirstGameID *int64 `json:"fgameId,string"` FirstGameName string `json:"fgameName"` TotalPayAmount *string `json:"totalPayAmount"` RegisterIP string `json:"registerIp"` DeviceName string `json:"deviceName"` PromoteAccount string `json:"promoteAccount"` - PromoteID *int64 `json:"promoteId"` + PromoteID *int64 `json:"promoteId,string"` BusierName string `json:"busierName"` } type SpendResponse struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - ID uint64 `json:"id"` - UserID *int64 `json:"userId"` + ID uint64 `json:"id,string"` + UserID *int64 `json:"userId,string"` OrderNumber string `json:"payOrderNumber"` - GameID *int64 `json:"gameId"` + GameID *int64 `json:"gameId,string"` GameName string `json:"gameName"` ServerID string `json:"serverId"` ServerName string `json:"serverName"` @@ -51,7 +51,7 @@ type SpendResponse struct { PayStatus *int `json:"payStatus"` IP string `json:"spendIp"` PromoteAccount string `json:"promoteAccount"` - PromoteID *int64 `json:"promoteId"` + PromoteID *int64 `json:"promoteId,string"` BusierName string `json:"busierName"` } type RegistrationPage struct { @@ -73,6 +73,6 @@ type GameOptionQuery struct { type GameOptionResponse struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - GameID uint64 `json:"gameId"` + GameID uint64 `json:"gameId,string"` GameName string `json:"gameName"` } diff --git a/internal/service/dto/performance.go b/internal/service/dto/performance.go index 1f3d500..c3e99d4 100644 --- a/internal/service/dto/performance.go +++ b/internal/service/dto/performance.go @@ -138,7 +138,7 @@ type PerformanceChannel struct { ContributorName string `json:"contributorName"` SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - ID uint64 `json:"id"` + ID uint64 `json:"id,string"` Account string `json:"account"` Name string `json:"name"` OwnerName string `json:"ownerName"` diff --git a/internal/service/dto/server_analysis.go b/internal/service/dto/server_analysis.go index ac67c43..e0d7e24 100644 --- a/internal/service/dto/server_analysis.go +++ b/internal/service/dto/server_analysis.go @@ -25,7 +25,7 @@ type ServerOptionResponse struct { type ServerAnalysisRow struct { SourceDB string `json:"sourceDb"` SourceDBLabel string `json:"sourceDbLabel"` - GameID *int64 `json:"gameId"` + GameID *int64 `json:"gameId,string"` GameName string `json:"gameName"` ServerOptionResponse Register int64 `json:"newRegisterCount"` diff --git a/internal/utils/routepath/routepath.go b/internal/utils/routepath/routepath.go index 9fe0813..ef54ba2 100644 --- a/internal/utils/routepath/routepath.go +++ b/internal/utils/routepath/routepath.go @@ -5,10 +5,13 @@ import "strings" // Normalize removes only a complete configured router prefix. Paths // such as /administrator must not be shortened when the prefix is /admin. func Normalize(path, routerPrefix string) string { - prefix := strings.TrimSuffix(strings.TrimSpace(routerPrefix), "/") - if prefix == "" || prefix == "/" { + prefix := strings.Trim(strings.TrimSpace(routerPrefix), "/") + if prefix == "" { return path } + // Gin also accepts a relative group name such as "api". Normalize it + // the same way before comparing a request path with Casbin policies. + prefix = "/" + prefix if path == prefix { return "/" } diff --git a/internal/utils/routepath/routepath_test.go b/internal/utils/routepath/routepath_test.go index 69eeb3d..9a4c235 100644 --- a/internal/utils/routepath/routepath_test.go +++ b/internal/utils/routepath/routepath_test.go @@ -11,6 +11,8 @@ func TestNormalize(t *testing.T) { }{ {name: "empty prefix", path: "/api/login", prefix: "", want: "/api/login"}, {name: "configured prefix", path: "/admin/api/login", prefix: "/admin", want: "/api/login"}, + {name: "relative prefix accepted by Gin", path: "/api/sw/promote/list", prefix: "api", want: "/sw/promote/list"}, + {name: "relative prefix boundary", path: "/apiary/list", prefix: "api", want: "/apiary/list"}, {name: "trailing slash prefix", path: "/admin/api/login", prefix: "/admin/", want: "/api/login"}, {name: "prefix root", path: "/admin", prefix: "/admin", want: "/"}, {name: "prefix boundary", path: "/administrator/api/login", prefix: "/admin", want: "/administrator/api/login"}, diff --git a/pkg/database/migration/check.go b/pkg/database/migration/check.go new file mode 100644 index 0000000..be7613c --- /dev/null +++ b/pkg/database/migration/check.go @@ -0,0 +1,159 @@ +package migration + +import ( + "fmt" + "sort" + "strings" + + "gorm.io/gorm" +) + +// CheckApplied is read-only; a missing or corrupt ledger must not be repaired +// by a normal service startup with automatic migrations disabled. +func CheckApplied(db *gorm.DB, steps []Step) error { + db = db.Session(&gorm.Session{NewDB: true}) + var rows []record + if err := db.Model(&record{}).Find(&rows).Error; err != nil { + return fmt.Errorf("read migration versions: %w", err) + } + known := map[string]bool{} + for _, step := range steps { + if step.ID == "" || known[step.ID] { + return fmt.Errorf("invalid or duplicate migration ID %q", step.ID) + } + known[step.ID] = true + } + for _, row := range rows { + if _, ok := known[row.ID]; !ok { + return fmt.Errorf("unknown applied migration %q", row.ID) + } + known[row.ID] = false + } + var pending []string + for id, missing := range known { + if missing { + pending = append(pending, id) + } + } + sort.Strings(pending) + if len(pending) > 0 { + return fmt.Errorf("pending migrations: %s", strings.Join(pending, ", ")) + } + return nil +} + +// CheckModels verifies persistent columns and declared indexes without +// changing data or schema. Extra legacy columns and indexes are retained. +func CheckModels(db *gorm.DB, models ...any) error { + db = db.Session(&gorm.Session{NewDB: true}) + var problems []string + for _, model := range models { + stmt := &gorm.Statement{DB: db} + if err := stmt.Parse(model); err != nil { + return err + } + table := stmt.Schema.Table + if !db.Migrator().HasTable(model) { + problems = append(problems, "missing table "+table) + continue + } + columns, err := db.Migrator().ColumnTypes(model) + if err != nil { + return err + } + found := map[string]bool{} + for _, column := range columns { + found[column.Name()] = true + } + for _, name := range stmt.Schema.DBNames { + if !stmt.Schema.FieldsByDBName[name].IgnoreMigration && !found[name] { + problems = append(problems, "missing column "+table+"."+name) + } + } + indexes, err := readIndexes(db, model, table) + if err != nil { + return err + } + if db.Dialector.Name() == "mysql" && len(stmt.Schema.PrimaryFields) > 0 { + var wanted []string + for _, field := range stmt.Schema.PrimaryFields { + wanted = append(wanted, field.DBName) + } + matched := false + for _, actual := range indexes { + if actual.primary && strings.Join(actual.columns, ",") == strings.Join(wanted, ",") { + matched = true + } + } + if !matched { + problems = append(problems, "missing or incompatible primary key "+table) + } + } + for _, index := range stmt.Schema.ParseIndexes() { + wanted := make([]string, 0, len(index.Fields)) + for _, field := range index.Fields { + wanted = append(wanted, field.DBName) + } + covered := false + for _, actual := range indexes { + if actual.prefix || len(actual.columns) < len(wanted) { + continue + } + if index.Class == "UNIQUE" && (!actual.unique || len(actual.columns) != len(wanted)) { + continue + } + if strings.Join(actual.columns[:len(wanted)], ",") == strings.Join(wanted, ",") { + covered = true + break + } + } + if !covered { + problems = append(problems, "missing index "+table+"."+index.Name) + } + } + } + if len(problems) > 0 { + return fmt.Errorf("%s", strings.Join(problems, "; ")) + } + return nil +} + +type schemaIndex struct { + columns []string + unique, prefix, primary bool +} + +func readIndexes(db *gorm.DB, model any, table string) ([]schemaIndex, error) { + if db.Dialector.Name() == "mysql" { + var rows []struct { + IndexName, ColumnName string + NonUnique int + SubPart *int + } + if err := db.Raw("SELECT index_name,column_name,non_unique,sub_part FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? ORDER BY index_name,seq_in_index", table).Scan(&rows).Error; err != nil { + return nil, err + } + var indexes []schemaIndex + previous := "" + for _, row := range rows { + if row.IndexName != previous { + indexes = append(indexes, schemaIndex{unique: row.NonUnique == 0, primary: row.IndexName == "PRIMARY"}) + previous = row.IndexName + } + index := &indexes[len(indexes)-1] + index.columns = append(index.columns, row.ColumnName) + index.prefix = index.prefix || row.SubPart != nil + } + return indexes, nil + } + rows, err := db.Migrator().GetIndexes(model) + if err != nil { + return nil, err + } + indexes := make([]schemaIndex, 0, len(rows)) + for _, row := range rows { + unique, _ := row.Unique() + indexes = append(indexes, schemaIndex{columns: row.Columns(), unique: unique}) + } + return indexes, nil +} diff --git a/pkg/database/migration/check_test.go b/pkg/database/migration/check_test.go new file mode 100644 index 0000000..7b89a60 --- /dev/null +++ b/pkg/database/migration/check_test.go @@ -0,0 +1,109 @@ +package migration + +import ( + "strings" + "testing" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +type checkedSchema struct { + ID uint `gorm:"primaryKey"` + Owner uint `gorm:"index:idx_checked_owner_state,priority:1"` + State int `gorm:"index:idx_checked_owner_state,priority:2"` + Code string `gorm:"uniqueIndex"` +} + +func TestSchemaCheckRejectsMissingColumnsAndIncorrectIndexes(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := CheckModels(db, &checkedSchema{}); err == nil { + t.Fatal("missing table passed") + } + if err := db.Migrator().CreateTable(&checkedSchema{}); err != nil { + t.Fatal(err) + } + if err := CheckModels(db, &checkedSchema{}); err != nil { + t.Fatal(err) + } + for _, statement := range []string{ + "DROP INDEX idx_checked_owner_state", + "CREATE INDEX idx_checked_owner_state ON checked_schemas(state,owner)", + } { + if err := db.Exec(statement).Error; err != nil { + t.Fatal(err) + } + } + if err := CheckModels(db, &checkedSchema{}); err == nil || !strings.Contains(err.Error(), "idx_checked_owner_state") { + t.Fatalf("reversed index passed: %v", err) + } + if err := db.Exec("CREATE INDEX legacy_owner_state ON checked_schemas(owner,state,id)").Error; err != nil { + t.Fatal(err) + } + if err := CheckModels(db, &checkedSchema{}); err != nil { + t.Fatalf("equivalent covering index rejected: %v", err) + } + if err := db.Exec("DROP INDEX idx_checked_schemas_code").Error; err != nil { + t.Fatal(err) + } + if err := db.Exec("CREATE INDEX idx_checked_schemas_code ON checked_schemas(code)").Error; err != nil { + t.Fatal(err) + } + if err := CheckModels(db, &checkedSchema{}); err == nil { + t.Fatal("non-unique replacement passed") + } + if err := db.Migrator().DropColumn(&checkedSchema{}, "Owner"); err != nil { + t.Fatal(err) + } + if err := CheckModels(db, &checkedSchema{}); err == nil || !strings.Contains(err.Error(), "missing column") { + t.Fatalf("missing column passed: %v", err) + } +} + +func TestCheckAppliedDoesNotCreateOrApplyMigrations(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + called := false + steps := []Step{{ID: "required", Migrate: func(*gorm.DB) error { called = true; return nil }}} + if err := CheckApplied(db, steps); err == nil { + t.Fatal("missing ledger passed") + } + if db.Migrator().HasTable(TableName) || called { + t.Fatal("read-only check mutated the database") + } + if err := db.Migrator().CreateTable(&record{}); err != nil { + t.Fatal(err) + } + if err := CheckApplied(db, steps); err == nil || !strings.Contains(err.Error(), "required") { + t.Fatalf("pending migration passed: %v", err) + } + if err := db.Create(&record{ID: "required"}).Error; err != nil { + t.Fatal(err) + } + // openWithDriverConfig returns a settings-bearing handle for MySQL's + // table options. Read-only checks must not retain their Model on it. + configured := db.Set("gorm:table_options", "ENGINE=InnoDB") + if err := CheckApplied(configured, steps); err != nil { + t.Fatal(err) + } + if configured.Statement.Model != nil { + t.Fatal("schema check leaked the migration model into the shared handle") + } + if err := CheckApplied(db, steps); err != nil { + t.Fatal(err) + } + if err := db.Create(&record{ID: "unknown"}).Error; err != nil { + t.Fatal(err) + } + if err := CheckApplied(db, steps); err == nil { + t.Fatal("unknown version passed") + } + if called { + t.Fatal("read-only check ran a migration") + } +} diff --git a/pkg/module/module.go b/pkg/module/module.go index d7d4842..65d7971 100644 --- a/pkg/module/module.go +++ b/pkg/module/module.go @@ -4,7 +4,9 @@ package module import ( + "fmt" "github.com/gin-gonic/gin" + "gorm.io/gorm" "kra/pkg/database/migration" ) @@ -28,16 +30,40 @@ type TimedTask struct { } type Definition struct { - Name string - Migrations []migration.Step - Surface Surface - TimedTasks []TimedTask + Name string + Migrations []migration.Step + Surface Surface + TimedTasks []TimedTask + Prerequisites func(*gorm.DB) error + ValidateSchema func(*gorm.DB) error } type Catalog struct { Definitions []Definition } +func (c Catalog) CheckPrerequisites(db *gorm.DB) error { + for _, item := range c.Definitions { + if item.Prerequisites != nil { + if err := item.Prerequisites(db); err != nil { + return fmt.Errorf("%s prerequisites: %w", item.Name, err) + } + } + } + return nil +} + +func (c Catalog) CheckSchema(db *gorm.DB) error { + for _, item := range c.Definitions { + if item.ValidateSchema != nil { + if err := item.ValidateSchema(db); err != nil { + return fmt.Errorf("%s: %w", item.Name, err) + } + } + } + return nil +} + func (c Catalog) MigrationSteps() []migration.Step { var steps []migration.Step for _, item := range c.Definitions { diff --git a/web/src/view/sw/components/BusinessChannels.vue b/web/src/view/sw/components/BusinessChannels.vue index ca30c78..0161099 100644 --- a/web/src/view/sw/components/BusinessChannels.vue +++ b/web/src/view/sw/components/BusinessChannels.vue @@ -19,7 +19,7 @@ import { createLatestRequest } from '@/utils/latestRequest' import { listBusinessChannels } from '@/api/business' defineOptions({ name: 'SwBusinessChannels' }) -const props = defineProps({ userId: { type: Number, required: true }, sourceDb: { type: String, default: '' }, platformBusierId: { type: Number, default: 0 } }) +const props = defineProps({ userId: { type: Number, required: true }, sourceDb: { type: String, default: '' }, platformBusierId: { type: [String, Number], default: 0 } }) const page = ref(1) const rows = ref([]) const total = ref(0) diff --git a/web/src/view/sw/components/BusinessProfileDrawer.vue b/web/src/view/sw/components/BusinessProfileDrawer.vue index 7d58209..ffd3f06 100644 --- a/web/src/view/sw/components/BusinessProfileDrawer.vue +++ b/web/src/view/sw/components/BusinessProfileDrawer.vue @@ -62,7 +62,7 @@ import { channelCount, channelMoney } from '../channel-display' import { alertStatuses } from '../alert-display' import BusinessTrend from './BusinessTrend.vue' import BusinessChannels from './BusinessChannels.vue' -const props = defineProps({ modelValue: Boolean, userId: { type: Number, default: 0 }, sourceDb: { type: String, default: '' }, platformBusierId: { type: Number, default: 0 }, name: { type: String, default: '' } }) +const props = defineProps({ modelValue: Boolean, userId: { type: Number, default: 0 }, sourceDb: { type: String, default: '' }, platformBusierId: { type: [String, Number], default: 0 }, name: { type: String, default: '' } }) const emit = defineEmits(['update:modelValue']) const visible = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) }) const periods = [{ label: '今日', value: 'today' }, { label: '昨日', value: 'yesterday' }, { label: '近7天', value: 'last7' }, { label: '近30天', value: 'last30' }, { label: '本月', value: 'thismonth' }]