20260905172534

This commit is contained in:
oneao committed 2026-09-05 17:25:35 +08:00
1 parent 1b01a31252
commit 7b00eeb88c
711 files changed
+2849 -959

No files matched your search

@@ -19,7 +19,6 @@ BEGIN TRANSACTION;
CREATE TABLE dbo.s_log_login (
b_id bigint NOT NULL,
b_user_id bigint NULL,
b_account nvarchar(50) NULL,
b_login_type varchar(20) NOT NULL, -- 登录类型:password/token/oidc/ldap
b_login_result int NOT NULL CONSTRAINT DF_s_log_login_result DEFAULT (0),
b_fail_reason nvarchar(200) NULL,
@@ -39,7 +38,6 @@ BEGIN TRANSACTION;
);
CREATE INDEX IX_s_log_login_user ON dbo.s_log_login (b_user_id, b_occurdatetime DESC);
CREATE INDEX IX_s_log_login_account ON dbo.s_log_login (b_account, b_occurdatetime DESC);
CREATE INDEX IX_s_log_login_result ON dbo.s_log_login (b_login_result, b_occurdatetime DESC);
END
@@ -3,6 +3,10 @@
中文账号写入会因代码页转换变为 '?'(b_user.b_account 本身是 nvarchar,账号支持中文,见
100_initialize_fms_new.sql)。改为 nvarchar(50) 与用户表对齐。
已废弃(2026-09-05):s_log_login / s_log_technical 的 b_account 列已按《FMS新系统核心表结构设计.md》
从结构中移除(账号即 b_user.b_id,见 b_user_id 列),详见 203_drop_log_account.sql。
本脚本仅针对历史 schema 保留幂等逻辑:列已不存在时自动跳过,无副作用。
可重复执行:仅在列存在且当前类型为 varchar 时才 ALTER。
s_log_login.b_account 上有索引 IX_s_log_login_account,SQL Server 修改列类型前需先删除、改完重建。
*/
@@ -432,7 +432,6 @@ BEGIN
CREATE TABLE dbo.s_log_technical (
b_id bigint NOT NULL, -- 雪花主键(应用层生成,全局唯一)
b_user_id bigint NULL,
b_account nvarchar(50) NULL,
b_sourcetype varchar(20) NULL,
b_loglevel varchar(16) NOT NULL,
b_logcategory varchar(64) NULL,
@@ -459,7 +458,6 @@ BEGIN
CREATE TABLE dbo.s_log_login (
b_id bigint NOT NULL, -- 雪花主键(应用层生成,全局唯一)
b_user_id bigint NULL,
b_account nvarchar(50) NULL,
b_login_type varchar(20) NOT NULL,
b_login_result int NOT NULL CONSTRAINT DF_s_log_login_result DEFAULT (0),
b_fail_reason nvarchar(200) NULL,
@@ -478,7 +476,6 @@ BEGIN
CONSTRAINT PK_s_log_login PRIMARY KEY (b_id)
);
CREATE INDEX IX_s_log_login_user ON dbo.s_log_login (b_user_id, b_occurdatetime DESC);
CREATE INDEX IX_s_log_login_account ON dbo.s_log_login (b_account, b_occurdatetime DESC);
CREATE INDEX IX_s_log_login_result ON dbo.s_log_login (b_login_result, b_occurdatetime DESC);
END;
@@ -0,0 +1,462 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
-- ============================================================================
-- 200_rebuild_business_keys.sql
-- 目的: 按《FMS新系统核心表结构设计.md》(2026-09-04) 将 FMS-NEW 元数据表
-- 从 bigint 雪花ID 方案重构为【字符串业务键】方案。
-- 目标库: FMS-NEW (118.89.70.199:1433), 执行前已备份:
-- /var/opt/mssql/data/FMS-NEW_pre_refactor_20260905.bak
-- 数据迁移范围:
-- 迁移 : b_user(3) / s_module(2) / s_field+view/edit/query(6字段配置)
-- / s_menu(5) / s_i18n_type(3) / s_i18n(8)
-- 不迁移: s_log_* 日志数据 / s_power 权限数据(结构保留,建空表)
-- 删除 : s_role / s_user_role / s_role_power / s_role_scope (文档不含,数据空)
-- 视图 v_b_user / v_s_log_login / v_s_log_technical / v_s_log_audit (文档不含)
-- 说明: 旧表先改名 *_pre200 -> 建新表 -> 迁移数据 -> 删除 *_pre200。
-- 逻辑全部在一个事务内, 失败自动回滚。
-- ============================================================================
IF DB_NAME() <> N'FMS-NEW'
THROW 51000, N'200_rebuild_business_keys can only run against FMS-NEW.', 1;
BEGIN TRANSACTION;
-- ---------------------------------------------------------------------------
-- 1. 删除文档中不存在的对象: 旧视图 + RBAC 表
-- ---------------------------------------------------------------------------
IF OBJECT_ID(N'dbo.v_s_log_audit', N'V') IS NOT NULL DROP VIEW dbo.v_s_log_audit;
IF OBJECT_ID(N'dbo.v_s_log_technical', N'V') IS NOT NULL DROP VIEW dbo.v_s_log_technical;
IF OBJECT_ID(N'dbo.v_s_log_login', N'V') IS NOT NULL DROP VIEW dbo.v_s_log_login;
IF OBJECT_ID(N'dbo.v_b_user', N'V') IS NOT NULL DROP VIEW dbo.v_b_user;
IF OBJECT_ID(N'dbo.s_role_power', N'U') IS NOT NULL DROP TABLE dbo.s_role_power;
IF OBJECT_ID(N'dbo.s_user_role', N'U') IS NOT NULL DROP TABLE dbo.s_user_role;
IF OBJECT_ID(N'dbo.s_role_scope', N'U') IS NOT NULL DROP TABLE dbo.s_role_scope;
IF OBJECT_ID(N'dbo.s_role', N'U') IS NOT NULL DROP TABLE dbo.s_role;
-- ---------------------------------------------------------------------------
-- 2. 旧表改名留档 *_pre200 (数据保留至迁移完成)
-- ---------------------------------------------------------------------------
EXEC sys.sp_rename N'dbo.b_user', N'b_user_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_module', N's_module_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_field', N's_field_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_field_group', N's_field_group_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_field_view', N's_field_view_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_field_edit', N's_field_edit_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_field_query', N's_field_query_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_autocode', N's_autocode_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_relation', N's_relation_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_menu', N's_menu_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_power', N's_power_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_i18n_type', N's_i18n_type_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_i18n', N's_i18n_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_log_technical', N's_log_technical_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_log_login', N's_log_login_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_log_audit', N's_log_audit_pre200', N'OBJECT';
EXEC sys.sp_rename N'dbo.s_log_audit_field', N's_log_audit_field_pre200', N'OBJECT';
-- ---------------------------------------------------------------------------
-- 3. 按设计文档创建新表
-- ---------------------------------------------------------------------------
-- 3.1 用户表
CREATE TABLE dbo.b_user (
b_id varchar(50) not null primary key, -- 用户账号本身
b_name nvarchar(100) null,
b_bz nvarchar(400) null,
b_password nvarchar(255) not null,
b_canuse tinyint not null default 1,
b_logintime tinyint not null default 0
);
-- 3.2 模块
CREATE TABLE dbo.s_module (
b_id varchar(50) not null primary key, -- 模块编码本身, 创建后不可修改
b_parent_id varchar(50) null,
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_module_type varchar(20) not null,
b_viewtable varchar(128) null,
b_savetable varchar(128) null,
b_keyfield varchar(50) null,
b_orderfield varchar(500) null,
b_delete_policy varchar(20) null,
b_canuse tinyint not null default 1,
b_xh int not null default 0,
b_bz nvarchar(2000) null,
b_edit_mode varchar(10) null
);
CREATE INDEX ix_s_module_parent ON dbo.s_module (b_parent_id, b_xh, b_canuse, b_id);
CREATE INDEX ix_s_module_order ON dbo.s_module (b_xh, b_id);
CREATE INDEX ix_s_module_savetable ON dbo.s_module (b_savetable);
-- 3.3 字段与布局
CREATE TABLE dbo.s_field (
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_type varchar(30) not null,
b_dbtype varchar(30) not null,
b_length int null,
b_precision int null,
b_scale int null,
b_nullable tinyint not null default 1,
b_default_value nvarchar(1000) null,
b_db_default nvarchar(1000) null,
b_writemode varchar(20) not null default 'direct',
b_options nvarchar(max) null,
b_canuse tinyint not null default 1,
b_xh int not null default 0, -- 字段定义顺序:同步时按视图列序重排(10 起步)
primary key (b_module_id, b_field)
);
CREATE INDEX ix_s_field_module_order ON dbo.s_field (b_module_id, b_xh, b_field);
CREATE TABLE dbo.s_field_group (
b_id varchar(50) not null, -- 分组编码本身, 模块内唯一, 创建后不可修改
b_module_id varchar(50) not null,
b_parent_id varchar(50) null, -- 父分组编码, NULL 表示根分组
b_title nvarchar(200) not null,
b_i18n varchar(150) null,
b_xh int not null default 0,
b_canuse tinyint not null default 1,
b_colspan int not null default 48,
b_layout_direction varchar(10) null default 'vertical',
primary key (b_module_id, b_id)
);
CREATE INDEX ix_s_field_group_parent ON dbo.s_field_group (b_module_id, b_parent_id, b_xh, b_id);
CREATE INDEX ix_s_field_group_order ON dbo.s_field_group (b_module_id, b_xh, b_id);
CREATE TABLE dbo.s_field_view (
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_group_id varchar(50) null,
b_visible tinyint not null default 1,
b_xh int not null default 0,
b_width int null,
primary key (b_module_id, b_field)
);
CREATE INDEX ix_s_field_view_order ON dbo.s_field_view (b_module_id, b_xh, b_field);
CREATE TABLE dbo.s_field_edit (
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_group_id varchar(50) null,
b_visible tinyint not null default 1,
b_required tinyint not null default 0,
b_readonly tinyint not null default 0,
b_disabled tinyint not null default 0,
b_xh int not null default 0,
b_width int null,
b_placeholder nvarchar(400) null,
b_colspan int not null default 12,
b_height int null,
primary key (b_module_id, b_field)
);
CREATE INDEX ix_s_field_edit_order ON dbo.s_field_edit (b_module_id, b_xh, b_field);
CREATE TABLE dbo.s_field_query (
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_component varchar(30) null,
b_condition_no int not null default 1,
b_operator varchar(20) not null,
b_default_value nvarchar(1000) null,
b_canuse tinyint not null default 1,
b_xh int not null default 0,
primary key (b_module_id, b_field, b_condition_no)
);
CREATE INDEX ix_s_field_query_order ON dbo.s_field_query (b_module_id, b_xh, b_field, b_condition_no);
-- 3.4 自动编码
CREATE TABLE dbo.s_autocode (
b_module_id varchar(50) not null,
b_field varchar(50) not null,
b_prefix nvarchar(200) not null default N'',
b_dateformat varchar(30) not null default 'yyyyMM',
b_separator nvarchar(20) not null default N'-',
b_seqwidth int not null default 5,
b_resettype varchar(10) not null default 'month',
b_startvalue bigint not null default 1,
b_currentperiod varchar(8) null,
b_currentvalue bigint not null default 0,
b_canuse tinyint not null default 1,
primary key (b_module_id, b_field)
);
-- 3.5 模块关系
CREATE TABLE dbo.s_relation (
b_module_id varchar(50) not null,
b_target_module_id varchar(50) not null,
b_relation_type varchar(20) not null,
b_field varchar(50) not null,
b_target_field varchar(50) not null,
b_foreign_side varchar(10) null,
b_junction_module_id varchar(50) null,
b_junction_field varchar(50) null,
b_junction_target_field varchar(50) null,
b_ownership varchar(20) not null default 'reference',
b_owner_side varchar(10) null,
b_delete_policy varchar(20) null,
b_order_module_id varchar(50) null,
b_order_field varchar(50) null,
b_canuse tinyint not null default 1,
b_xh int not null default 0,
primary key (b_module_id, b_field, b_target_module_id, b_target_field)
);
CREATE INDEX ix_s_relation_module ON dbo.s_relation (b_module_id, b_canuse, b_xh, b_field, b_target_module_id);
CREATE INDEX ix_s_relation_target ON dbo.s_relation (b_target_module_id);
-- 3.6 菜单
CREATE TABLE dbo.s_menu (
b_id varchar(50) not null primary key, -- 菜单编码本身
b_parent_id varchar(50) null,
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_menu_type varchar(20) not null,
b_route varchar(200) null,
b_module_id varchar(50) null,
b_icon varchar(50) null,
b_xh int not null default 0,
b_canuse tinyint not null default 1
);
CREATE INDEX ix_s_menu_parent ON dbo.s_menu (b_parent_id, b_xh, b_canuse, b_id);
CREATE INDEX ix_s_menu_order ON dbo.s_menu (b_xh, b_id);
CREATE INDEX ix_s_menu_module ON dbo.s_menu (b_module_id);
CREATE INDEX ix_s_menu_route ON dbo.s_menu (b_route, b_canuse, b_id);
-- 3.7 权限(结构保留, 数据不迁移)
CREATE TABLE dbo.s_power (
b_id varchar(50) not null primary key, -- 权限编码本身
b_name nvarchar(200) not null,
b_i18n varchar(150) null,
b_power_type varchar(20) not null,
b_resource varchar(128) null,
b_module_id varchar(50) null,
b_field varchar(50) null,
b_operation varchar(30) not null,
b_canuse tinyint not null default 1,
b_xh int not null default 0
);
CREATE INDEX ix_s_power_module ON dbo.s_power (b_module_id, b_field, b_canuse);
CREATE INDEX ix_s_power_resource ON dbo.s_power (b_power_type, b_resource, b_canuse);
-- 3.8 多语言
CREATE TABLE dbo.s_i18n_type (
b_id varchar(20) not null primary key, -- 语言编码本身
b_name nvarchar(200) not null,
b_canuse tinyint not null default 1,
b_default tinyint not null default 0,
b_xh int not null default 0
);
CREATE TABLE dbo.s_i18n (
b_id varchar(200) not null primary key, -- b_key + ':' + b_locale
b_key varchar(150) not null,
b_locale varchar(20) not null,
b_value nvarchar(1000) not null,
b_i18ntype varchar(20) null,
b_canuse tinyint not null default 1,
b_module_id varchar(50) null,
unique (b_key, b_locale)
);
CREATE INDEX ix_s_i18n_module_key ON dbo.s_i18n (b_module_id, b_key, b_locale);
CREATE INDEX ix_s_i18n_locale ON dbo.s_i18n (b_locale, b_canuse);
-- 3.9 日志(仅结构, 数据不迁移)
CREATE TABLE dbo.s_log_technical (
b_id uniqueidentifier not null,
b_user_id varchar(50) null,
b_sourcetype varchar(20) null,
b_loglevel varchar(16) not null,
b_logcategory varchar(64) null,
b_operation varchar(100) null,
b_module_id varchar(50) null,
b_request_id varchar(64) null,
b_trace_id varchar(64) null,
b_server_node varchar(128) null,
b_duration_ms bigint null,
b_result_code varchar(50) null,
b_error_code varchar(50) null,
b_error_message nvarchar(4000) null,
b_exception_text nvarchar(max) null,
b_context_text nvarchar(max) null,
b_occurdatetime datetime2 default sysutcdatetime(),
primary key nonclustered (b_id)
);
CREATE TABLE dbo.s_log_login (
b_id uniqueidentifier not null,
b_user_id varchar(50) null,
b_login_type varchar(20) not null,
b_login_result tinyint not null default 0,
b_fail_reason nvarchar(400) null,
b_ip varchar(50) null,
b_ip_location nvarchar(400) null,
b_browser varchar(100) null,
b_browser_version varchar(50) null,
b_os varchar(100) null,
b_os_version varchar(50) null,
b_device_type varchar(20) null,
b_user_agent nvarchar(max) null,
b_session_id varchar(50) null,
b_request_id varchar(64) null,
b_trace_id varchar(64) null,
b_occurdatetime datetime2 default sysutcdatetime(),
primary key nonclustered (b_id)
);
CREATE TABLE dbo.s_log_audit (
b_id uniqueidentifier not null,
b_event_code varchar(50) not null,
b_module_id varchar(50) null,
b_data_id varchar(50) null,
b_business_no varchar(50) null,
b_operation varchar(100) null,
b_operator_id varchar(50) null,
b_operator_name nvarchar(200) null,
b_source_type varchar(20) not null default 'user',
b_visibility varchar(20) not null default 'internal',
b_request_id varchar(64) null,
b_trace_id varchar(64) null,
b_payload_text nvarchar(max) null,
b_occurdatetime datetime2 default sysutcdatetime(),
primary key nonclustered (b_id)
);
CREATE TABLE dbo.s_log_audit_field (
b_id uniqueidentifier not null,
b_event_id uniqueidentifier not null,
b_module_id varchar(50) null,
b_data_id varchar(50) null,
b_field varchar(50) not null,
b_field_label nvarchar(200) null,
b_before_value nvarchar(max) null,
b_after_value nvarchar(max) null,
b_before_display nvarchar(1000) null,
b_after_display nvarchar(1000) null,
b_visibility varchar(20) not null default 'internal',
primary key nonclustered (b_id)
);
CREATE CLUSTERED INDEX cx_s_log_technical_occur ON dbo.s_log_technical (b_occurdatetime, b_id);
CREATE CLUSTERED INDEX cx_s_log_login_occur ON dbo.s_log_login (b_occurdatetime, b_id);
CREATE CLUSTERED INDEX cx_s_log_audit_occur ON dbo.s_log_audit (b_occurdatetime, b_id);
CREATE CLUSTERED INDEX cx_s_log_audit_field_event ON dbo.s_log_audit_field (b_event_id, b_id);
CREATE INDEX ix_s_log_technical_trace ON dbo.s_log_technical (b_trace_id, b_occurdatetime);
CREATE INDEX ix_s_log_login_user ON dbo.s_log_login (b_user_id, b_occurdatetime);
CREATE INDEX ix_s_log_audit_module ON dbo.s_log_audit (b_module_id, b_data_id, b_occurdatetime);
-- ---------------------------------------------------------------------------
-- 4. 数据迁移 (旧 *_pre200 -> 新表)
-- ---------------------------------------------------------------------------
-- 4.1 b_user: b_account -> b_id, 丢弃业务系统冗余列
INSERT INTO dbo.b_user (b_id, b_name, b_bz, b_password, b_canuse, b_logintime)
SELECT b_account, b_name, b_bz, b_password, b_canuse, b_logintime
FROM dbo.b_user_pre200;
-- 4.2 s_module: b_code -> b_id, 父模块与录入/更新人引用改业务键
INSERT INTO dbo.s_module (
b_id, b_parent_id, b_name, b_i18n, b_module_type, b_viewtable, b_savetable,
b_keyfield, b_orderfield, b_delete_policy, b_canuse, b_xh, b_bz,
b_edit_mode
)
SELECT
m.b_code, p.b_code, m.b_name, m.b_i18n, m.b_module_type, m.b_viewtable, m.b_savetable,
m.b_keyfield, m.b_orderfield, m.b_delete_policy, m.b_canuse, m.b_xh, m.b_bz,
m.b_edit_mode
FROM dbo.s_module_pre200 m
LEFT JOIN dbo.s_module_pre200 p ON p.b_id = m.b_parent_id;
-- 4.3 s_field: b_module_id 引用改业务键
INSERT INTO dbo.s_field (
b_module_id, b_field, b_name, b_i18n, b_type, b_dbtype, b_length,
b_precision, b_scale, b_nullable, b_default_value, b_db_default, b_writemode,
b_options, b_canuse
)
SELECT
mo.b_code, f.b_field, f.b_name, f.b_i18n, f.b_type, f.b_dbtype, f.b_length,
f.b_precision, f.b_scale, f.b_nullable, f.b_default_value, f.b_db_default, f.b_writemode,
f.b_options, f.b_canuse
FROM dbo.s_field_pre200 f
JOIN dbo.s_module_pre200 mo ON mo.b_id = f.b_module_id;
-- 4.4 s_field_view / s_field_edit / s_field_query: b_field_id -> b_field
INSERT INTO dbo.s_field_view (b_module_id, b_field, b_group_id, b_visible, b_xh, b_width)
SELECT mo.b_code, f.b_field, NULL, v.b_visible, v.b_xh, v.b_width
FROM dbo.s_field_view_pre200 v
JOIN dbo.s_module_pre200 mo ON mo.b_id = v.b_module_id
JOIN dbo.s_field_pre200 f ON f.b_id = v.b_field_id AND f.b_module_id = v.b_module_id;
INSERT INTO dbo.s_field_edit (b_module_id, b_field, b_group_id, b_visible, b_required, b_readonly, b_disabled, b_xh, b_width, b_placeholder, b_colspan, b_height)
SELECT mo.b_code, f.b_field, NULL, e.b_visible, e.b_required, e.b_readonly, e.b_disabled, e.b_xh, e.b_width, e.b_placeholder, e.b_colspan, e.b_height
FROM dbo.s_field_edit_pre200 e
JOIN dbo.s_module_pre200 mo ON mo.b_id = e.b_module_id
JOIN dbo.s_field_pre200 f ON f.b_id = e.b_field_id AND f.b_module_id = e.b_module_id;
INSERT INTO dbo.s_field_query (b_module_id, b_field, b_component, b_condition_no, b_operator, b_default_value, b_canuse, b_xh)
SELECT mo.b_code, f.b_field, q.b_component, q.b_condition_no, q.b_operator, q.b_default_value, q.b_canuse, q.b_xh
FROM dbo.s_field_query_pre200 q
JOIN dbo.s_module_pre200 mo ON mo.b_id = q.b_field_module_id
JOIN dbo.s_field_pre200 f ON f.b_id = q.b_field_id AND f.b_module_id = q.b_field_module_id;
-- s_field_group / s_autocode / s_relation: 无有效数据, 保持空表
-- (原 s_field_group 仅存的 1 行数据其 b_module_id 已不存在, 属孤儿数据, 不迁移)
-- 4.5 s_menu: b_code -> b_id, 父菜单/绑定模块引用改业务键
INSERT INTO dbo.s_menu (b_id, b_parent_id, b_name, b_i18n, b_menu_type, b_route, b_module_id, b_icon, b_xh, b_canuse)
SELECT
m.b_code, p.b_code, m.b_name, m.b_i18n, m.b_menu_type, m.b_route, mo.b_code,
NULLIF(LEFT(CONVERT(nvarchar(max), m.b_icon), 50), N''), m.b_xh, m.b_canuse
FROM dbo.s_menu_pre200 m
LEFT JOIN dbo.s_menu_pre200 p ON p.b_id = m.b_parent_id
LEFT JOIN dbo.s_module_pre200 mo ON mo.b_id = m.b_module_id;
-- 4.6 s_i18n_type: b_code -> b_id
INSERT INTO dbo.s_i18n_type (b_id, b_name, b_canuse, b_default, b_xh)
SELECT b_code, b_name, b_canuse, b_default, b_xh
FROM dbo.s_i18n_type_pre200;
-- 4.7 s_i18n: b_id = b_key + ':' + b_locale, 模块引用改业务键(0 -> NULL)
INSERT INTO dbo.s_i18n (b_id, b_key, b_locale, b_value, b_i18ntype, b_canuse, b_module_id)
SELECT
i.b_key + N':' + i.b_locale, i.b_key, i.b_locale, i.b_value, i.b_i18ntype, i.b_canuse,
CASE WHEN i.b_module_id = 0 THEN NULL ELSE mo.b_code END
FROM dbo.s_i18n_pre200 i
LEFT JOIN dbo.s_module_pre200 mo ON mo.b_id = i.b_module_id;
-- s_power / s_log_* : 按需求不迁移数据
-- ---------------------------------------------------------------------------
-- 5. 删除 *_pre200 旧表
-- ---------------------------------------------------------------------------
DROP TABLE dbo.b_user_pre200;
DROP TABLE dbo.s_module_pre200;
DROP TABLE dbo.s_field_pre200;
DROP TABLE dbo.s_field_group_pre200;
DROP TABLE dbo.s_field_view_pre200;
DROP TABLE dbo.s_field_edit_pre200;
DROP TABLE dbo.s_field_query_pre200;
DROP TABLE dbo.s_autocode_pre200;
DROP TABLE dbo.s_relation_pre200;
DROP TABLE dbo.s_menu_pre200;
DROP TABLE dbo.s_power_pre200;
DROP TABLE dbo.s_i18n_type_pre200;
DROP TABLE dbo.s_i18n_pre200;
DROP TABLE dbo.s_log_technical_pre200;
DROP TABLE dbo.s_log_login_pre200;
DROP TABLE dbo.s_log_audit_pre200;
DROP TABLE dbo.s_log_audit_field_pre200;
COMMIT TRANSACTION;
-- 验证输出
SELECT DB_NAME() AS database_name,
(SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0) AS user_table_count,
(SELECT COUNT(*) FROM dbo.b_user) AS user_count,
(SELECT COUNT(*) FROM dbo.s_module) AS module_count,
(SELECT COUNT(*) FROM dbo.s_field) AS field_count,
(SELECT COUNT(*) FROM dbo.s_menu) AS menu_count,
(SELECT COUNT(*) FROM dbo.s_i18n) AS i18n_count;
@@ -0,0 +1,45 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
-- ============================================================================
-- 201_add_bf_files.sql
-- 目的: 在 FMS-NEW 补建文件表 bf_files(旧 FMS 系统已有,代码 FileService 仍依赖)。
-- 参考旧库 FMS.dbo.bf_files 列结构;将"引用类"列改为字符串业务键:
-- mx_moduleid -> varchar(50) 引用 s_module.b_id(模块编码)
-- mx_inputuser_id / mx_updateuser_id / mx_shuser_id
-- -> varchar(50) 引用 b_user.b_id(账号)
-- father 仍为 bigint,引用本表 subid(文件树父节点)。
-- 主键 subid 为应用层雪花 bigint,由 FileService/IdGenerator 生成。
-- ============================================================================
IF DB_NAME() <> N'FMS-NEW'
THROW 51000, N'201_add_bf_files can only run against FMS-NEW.', 1;
IF OBJECT_ID(N'dbo.bf_files', N'U') IS NULL
BEGIN
CREATE TABLE dbo.bf_files (
subid bigint not null, -- 雪花主键(应用层生成)
father bigint null, -- 父节点(引用同表 subid)
mx_xh int null default 0,
mx_filename nvarchar(500) null,
mx_filesize varchar(50) null,
mx_fileext varchar(50) null,
mx_mapfilename nvarchar(500) null,
mx_moduleid varchar(50) null, -- 引用 s_module.b_id(模块编码)
mx_cutfilename nvarchar(500) null,
mx_cutfilesize varchar(50) null,
mx_cate_id varchar(50) null,
mx_bz nvarchar(500) null,
mx_orderid varchar(50) null,
mx_sh int not null default 0,
mx_shdatetime datetime2 null,
mx_shuser_id varchar(50) null, -- 引用 b_user.b_id(账号)
mx_inputuser_id varchar(50) null, -- 引用 b_user.b_id(账号)
mx_inputdatetime datetime2 null default sysutcdatetime(),
mx_updateuser_id varchar(50) null,
mx_updatedatetime datetime2 null,
CONSTRAINT PK_bf_files PRIMARY KEY (subid)
);
CREATE INDEX ix_bf_files_module ON dbo.bf_files (mx_moduleid);
CREATE INDEX ix_bf_files_father ON dbo.bf_files (father);
END;
@@ -0,0 +1,26 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
-- ============================================================================
-- 202_add_bf_files_cateid.sql
-- 目的: 在 FMS-NEW 补建文件分类表 bf_files_cateid(旧 FMS 系统已有,
-- 前端 fms-file 组件 FileListPanel / useFileCategory 仍按该表加载分类)。
-- 参考旧库 G3HY2025.dbo.bf_files_cateid 列结构,并按字符串业务键/状态列
-- 统一规范:b_id 即分类编码(varchar,被 bf_files.mx_cate_id 引用)。
-- ============================================================================
IF DB_NAME() <> N'FMS-NEW'
THROW 51000, N'202_add_bf_files_cateid can only run against FMS-NEW.', 1;
IF OBJECT_ID(N'dbo.bf_files_cateid', N'U') IS NULL
BEGIN
CREATE TABLE dbo.bf_files_cateid (
b_id varchar(50) not null, -- 分类编码(字符串业务键),被 bf_files.mx_cate_id 引用
b_name nvarchar(200) not null, -- 分类名称
b_files_type varchar(50) null, -- 用途标识(如 file_sh 附件),可选
b_xh int not null default 0,
b_canuse tinyint not null default 1, -- 启用
b_bz nvarchar(500) null, -- 备注
CONSTRAINT PK_bf_files_cateid PRIMARY KEY (b_id)
);
END;
@@ -0,0 +1,30 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
-- ============================================================================
-- 203_drop_log_account.sql
-- 目的: 从日志表移除冗余的 b_account 列,与《FMS新系统核心表结构设计.md》对齐:
-- s_log_technical: 删除 b_account,保留 b_user_id
-- s_log_login: 删除 b_account,保留 b_user_id
-- 新结构中账号即 b_user.b_id(字符串业务键),日志通过 b_user_id 记录用户;
-- b_account 列系历史建表脚本(100/200)多余引入,数据库中无其索引依赖。
-- ============================================================================
IF DB_NAME() <> N'FMS-NEW'
THROW 51000, N'203_drop_log_account can only run against FMS-NEW.', 1;
-- 若历史库中残留依赖 b_account 的索引,先显式删除
IF OBJECT_ID(N'dbo.IX_s_log_login_account', N'INDEX') IS NOT NULL
DROP INDEX IX_s_log_login_account ON dbo.s_log_login;
IF OBJECT_ID(N'dbo.s_log_login', N'U') IS NOT NULL
AND COL_LENGTH(N'dbo.s_log_login', N'b_account') IS NOT NULL
BEGIN
ALTER TABLE dbo.s_log_login DROP COLUMN b_account;
END;
IF OBJECT_ID(N'dbo.s_log_technical', N'U') IS NOT NULL
AND COL_LENGTH(N'dbo.s_log_technical', N'b_account') IS NOT NULL
BEGIN
ALTER TABLE dbo.s_log_technical DROP COLUMN b_account;
END;
@@ -0,0 +1,69 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
-- ============================================================================
-- 204_add_s_field_xh.sql
-- 目的: 给 s_field 增加排序列 b_xh(对齐 s_field_group/view/edit/query 的 b_xh 惯例)。
-- 此前 s_field 无顺序列,前端只能按 b_field 字母排序,模块「同步字段」后
-- 保存再加载,顺序会被字母序打乱,无法按数据库视图/表的列顺序持久化。
-- 目标库: FMS-NEW
-- 说明: 1) 已存在 s_field 的库执行本脚本(新增列并回填初值)。
-- 2) b_xh 初值按各模块 b_viewtable 的列顺序(sys.columns.column_id)赋序,
-- 步长 10;视图里找不到的字段保持 0(排在末位,按 b_field 兜底)。
-- 3) 全新库在 200_rebuild_business_keys.sql 建表定义中已含 b_xh,
-- 此处 COL_LENGTH 判断将跳过 ADD,仅执行回填与建索引(幂等)。
-- 4) 批次按 GO 拆分:同一批次内 ALTER 加列后引用新列会在解析期
-- 报 Invalid column name,必须拆批执行(sqlcmd / JDBC 均按 GO 分批)。
-- ============================================================================
IF DB_NAME() <> N'FMS-NEW'
THROW 51000, N'204_add_s_field_xh can only run against FMS-NEW.', 1;
BEGIN TRANSACTION;
IF COL_LENGTH(N'dbo.s_field', N'b_xh') IS NULL
BEGIN
ALTER TABLE dbo.s_field
ADD b_xh int NOT NULL CONSTRAINT DF_s_field_xh DEFAULT (0);
END;
GO
-- 按模块查询视图的列顺序给既有字段行赋序(10 起步,与其它配置表一致)
UPDATE f
SET f.b_xh = ranked.xh
FROM dbo.s_field f
JOIN (
SELECT f.b_module_id,
f.b_field,
ROW_NUMBER() OVER (
PARTITION BY f.b_module_id
ORDER BY c.column_id
) * 10 AS xh
FROM dbo.s_field f
JOIN dbo.s_module m ON m.b_id = f.b_module_id
JOIN sys.columns c ON c.object_id = OBJECT_ID(m.b_viewtable)
AND c.name = f.b_field
) ranked
ON ranked.b_module_id = f.b_module_id
AND ranked.b_field = f.b_field;
GO
IF OBJECT_ID(N'dbo.ix_s_field_module_order', N'I') IS NULL
BEGIN
CREATE INDEX ix_s_field_module_order
ON dbo.s_field (b_module_id, b_xh, b_field);
END;
GO
COMMIT TRANSACTION;
GO
-- 验证输出
SELECT m.b_id AS module_id,
m.b_viewtable,
f.b_xh,
f.b_field
FROM dbo.s_module m
JOIN dbo.s_field f ON f.b_module_id = m.b_id
ORDER BY m.b_id, f.b_xh, f.b_field;
GO
@@ -46,7 +46,7 @@ public class AuthService {
if (users.isEmpty()) {
loginLogService.recordLogin(
normalizedOrgId, normalizedUserId, null,
normalizedOrgId, normalizedUserId,
false, "账号不存在", null, request);
return Optional.empty();
}
@@ -55,24 +55,27 @@ public class AuthService {
String storedPassword = getString(account, "b_password");
if (!passwordMatches(storedPassword, password)) {
loginLogService.recordLogin(
normalizedOrgId, normalizedUserId, null,
normalizedOrgId, normalizedUserId,
false, "密码错误", null, request);
return Optional.empty();
}
String accountId = getString(account, "b_account");
long userPrimaryKey = getLong(account, "b_id");
// 字符串业务键方案:b_user.b_id 即用户账号本身
String accountId = getString(account, "b_id");
if (accountId == null || accountId.isBlank()) {
throw new IllegalStateException("登录用户缺少有效的 b_id");
}
String name = getString(account, "b_name");
String sessionId = UUID.randomUUID().toString();
String token = jwtUtils.generateToken(accountId, normalizedOrgId, sessionId);
activeSessionRegistry.activate(normalizedOrgId, accountId, sessionId);
loginLogService.recordLogin(
normalizedOrgId, accountId, userPrimaryKey,
normalizedOrgId, accountId,
true, null, sessionId, request);
Map<String, Object> user = new LinkedHashMap<>();
user.put("id", userPrimaryKey);
user.put("id", accountId);
user.put("account", accountId);
user.put("name", name);
@@ -88,21 +91,6 @@ public class AuthService {
return value == null ? null : value.toString();
}
private long getLong(Map<String, Object> row, String column) {
Object value = getValue(row, column);
if (value instanceof Number number) {
return number.longValue();
}
if (value != null) {
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException ignored) {
// Report the same configuration error below.
}
}
throw new IllegalStateException("登录用户缺少有效的" + column);
}
private Object getValue(Map<String, Object> row, String column) {
for (Map.Entry<String, Object> entry : row.entrySet()) {
if (entry.getKey().equalsIgnoreCase(column)) {
@@ -65,18 +65,14 @@ public class DataSaveService {
throw new BusinessException("count 必须是 1 到 1000 之间的整数");
}
final long moduleIdValue;
try {
moduleIdValue = Long.parseLong(moduleId);
} catch (NumberFormatException exception) {
throw new BusinessException("moduleId 必须是有效的数字");
}
// 字符串业务键方案:moduleId 即模块编码(如 's_i18n_type'),非 bigint
String normalizedModuleId = moduleId.trim();
try (Connection connection = dataSource.getConnection()) {
boolean originalAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
AutoCodeRule rule = loadAutoCodeRule(connection, moduleIdValue);
AutoCodeRule rule = loadAutoCodeRule(connection, normalizedModuleId);
DbUtils.TableMetadata targetTable = dbUtils.loadTableMetadata(connection, rule.saveTable());
DbUtils.ColumnMetadata targetColumn = dbUtils.getColumn(targetTable, rule.fieldName());
if (targetColumn == null) {
@@ -119,14 +115,16 @@ public class DataSaveService {
}
long lastValue = nextValue - 1;
// s_autocode 主键为 (b_module_id, b_field)
try (PreparedStatement statement = connection.prepareStatement(
"UPDATE dbo.s_module_auto_code "
+ "SET b_current_period = ?, b_current_value = ? "
+ "WHERE b_id = ?"
"UPDATE dbo.s_autocode "
+ "SET b_currentperiod = ?, b_currentvalue = ? "
+ "WHERE b_module_id = ? AND b_field = ?"
)) {
statement.setString(1, period);
statement.setLong(2, lastValue);
statement.setLong(3, rule.id());
statement.setString(3, rule.moduleId());
statement.setString(4, rule.fieldName());
statement.executeUpdate();
}
connection.commit();
@@ -142,18 +140,19 @@ public class DataSaveService {
}
}
private AutoCodeRule loadAutoCodeRule(Connection connection, long moduleId) throws SQLException {
String sql = "SELECT TOP 1 r.b_id, r.b_prefix, r.b_date_format, "
+ "r.b_separator, r.b_sequence_width, r.b_reset_type, "
+ "r.b_start_value, r.b_current_period, r.b_current_value, "
+ "m.b_savetable, f.b_field "
+ "FROM dbo.s_module_auto_code r WITH (UPDLOCK, HOLDLOCK, ROWLOCK) "
private AutoCodeRule loadAutoCodeRule(Connection connection, String moduleId) throws SQLException {
// 字符串业务键方案:s_autocode 主键为 (b_module_id, b_field),
// 字段名直接存于 s_autocode.b_field,不再经 s_module_field 中间表。
String sql = "SELECT TOP 1 r.b_prefix, r.b_dateformat, r.b_separator, "
+ "r.b_seqwidth, r.b_resettype, r.b_startvalue, "
+ "r.b_currentperiod, r.b_currentvalue, r.b_field, m.b_savetable "
+ "FROM dbo.s_autocode r WITH (UPDLOCK, HOLDLOCK, ROWLOCK) "
+ "INNER JOIN dbo.s_module m ON m.b_id = r.b_module_id "
+ "INNER JOIN dbo.s_module_field f ON f.b_id = r.b_field_id "
+ "WHERE r.b_module_id = ? AND r.b_canuse = 1 "
+ "AND m.b_module_type = 'data' AND f.b_canuse = 1";
+ "AND m.b_module_type = 'data' "
+ "ORDER BY r.b_field";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, moduleId);
statement.setString(1, moduleId);
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
throw new BusinessException("当前模块未配置启用的自动编码规则");
@@ -163,16 +162,19 @@ public class DataSaveService {
if (saveTable == null || saveTable.isBlank()) {
throw new BusinessException("模块未配置保存表,无法生成自动编号");
}
if (fieldName == null || fieldName.isBlank()) {
throw new BusinessException("自动编码规则缺少字段配置");
}
return new AutoCodeRule(
resultSet.getLong("b_id"),
moduleId,
resultSet.getString("b_prefix"),
resultSet.getString("b_date_format"),
resultSet.getString("b_dateformat"),
resultSet.getString("b_separator"),
resultSet.getInt("b_sequence_width"),
resultSet.getString("b_reset_type"),
resultSet.getLong("b_start_value"),
resultSet.getString("b_current_period"),
resultSet.getLong("b_current_value"),
resultSet.getInt("b_seqwidth"),
resultSet.getString("b_resettype"),
resultSet.getLong("b_startvalue"),
resultSet.getString("b_currentperiod"),
resultSet.getLong("b_currentvalue"),
saveTable,
fieldName
);
@@ -353,7 +355,7 @@ public class DataSaveService {
}
private record AutoCodeRule(
long id,
String moduleId,
String prefix,
String dateFormat,
String separator,
@@ -394,29 +396,42 @@ public class DataSaveService {
tableName,
metadataCache
);
DbUtils.ColumnMetadata keyColumn = dbUtils.getColumn(table, keyField);
if (keyColumn == null) {
throw new BusinessException("主键字段不存在: " + tableName + "." + keyField);
// 字符串业务键方案支持复合主键:key_field 可用逗号分隔多个列,
// 例如 s_field 传 "b_module_id,b_field";单列主键传 "b_id"。
List<DbUtils.ColumnMetadata> keyColumns = new ArrayList<>();
for (String name : keyField.split(",")) {
String trimmed = name.trim();
if (trimmed.isEmpty()) {
continue;
}
DbUtils.ColumnMetadata column = dbUtils.getColumn(table, trimmed);
if (column == null) {
throw new BusinessException("主键字段不存在: " + tableName + "." + trimmed);
}
keyColumns.add(column);
}
if (keyColumns.isEmpty()) {
throw new BusinessException("主键字段不能为空: " + tableName);
}
executeRows(
connection,
table,
keyColumn,
keyColumns,
"delete",
rows(request, "deletes")
);
executeRows(
connection,
table,
keyColumn,
keyColumns,
"update",
rows(request, "updates")
);
executeRows(
connection,
table,
keyColumn,
keyColumns,
"insert",
rows(request, "inserts")
);
@@ -445,7 +460,7 @@ public class DataSaveService {
private void executeRows(
Connection connection,
DbUtils.TableMetadata table,
DbUtils.ColumnMetadata keyColumn,
List<DbUtils.ColumnMetadata> keyColumns,
String action,
List<Map<String, Object>> rows
) {
@@ -453,8 +468,8 @@ public class DataSaveService {
try {
switch (action) {
case "delete" -> dbUtils.delete(connection, table, rows.get(index));
case "update" -> dbUtils.update(connection, table, keyColumn, rows.get(index));
case "insert" -> dbUtils.insert(connection, table, keyColumn, rows.get(index));
case "update" -> dbUtils.update(connection, table, keyColumns, rows.get(index));
case "insert" -> dbUtils.insert(connection, table, null, rows.get(index));
default -> throw new IllegalArgumentException("不支持的保存操作: " + action);
}
} catch (SQLException exception) {
@@ -132,14 +132,16 @@ public class FileService {
""";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setLong(1, subid);
// father 自引用 bf_files.subid,仍为 bigint
ps.setObject(2, toLongOrNull(father));
ps.setString(3, filename);
ps.setString(4, filesize);
ps.setString(5, fileext);
ps.setString(6, mapfilename);
ps.setObject(7, toLongOrNull(moduleId));
ps.setObject(8, toLongOrNull(cateId));
ps.setObject(9, toLongOrNull(userId));
// 字符串业务键:模块编码 / 分类编码 / 用户账号均按字符串存储
ps.setString(7, moduleId);
ps.setString(8, cateId);
ps.setString(9, userId);
ps.executeUpdate();
}
} finally {
@@ -3,7 +3,7 @@ package cn.g3soft.fmsapi.service;
import cn.g3soft.fmsapi.database.OrgContext;
import cn.g3soft.fmsapi.utils.ClientInfo;
import cn.g3soft.fmsapi.utils.ClientInfoExtractor;
import cn.g3soft.fmsapi.utils.snowflake.idgen.IdGenerator;
import cn.g3soft.fmsapi.utils.UuidV7Generator;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,7 +37,6 @@ public class LoginLogService {
public void recordLogin(
String orgId,
String account,
Long userId,
boolean success,
String failReason,
String sessionId,
@@ -50,9 +49,10 @@ public class LoginLogService {
ClientInfo info = clientInfoExtractor.extract(request);
Map<String, Object> row = new LinkedHashMap<>();
row.put("b_id", IdGenerator.nextId());
row.put("b_user_id", userId);
row.put("b_account", account);
// 字符串业务键方案:主键为业务层生成的 UUIDv7
row.put("b_id", UuidV7Generator.nextUuid().toString());
// b_user_id 引用 b_user.b_id(账号字符串,即稳定用户标识);仅登录成功时存在
row.put("b_user_id", success ? account : null);
row.put("b_login_type", LOGIN_TYPE_PASSWORD);
row.put("b_occurdatetime", Timestamp.from(java.time.Instant.now()));
row.put("b_ip", info.ip());
@@ -2,7 +2,6 @@ package cn.g3soft.fmsapi.utils;
import cn.g3soft.fmsapi.database.OrgDataSourceManager;
import cn.g3soft.fmsapi.exception.BusinessException;
import cn.g3soft.fmsapi.utils.snowflake.idgen.IdGenerator;
import jakarta.annotation.Resource;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
@@ -166,8 +165,9 @@ public class DbUtils {
String account
) throws SQLException {
try (Connection connection = dataSourceManager.getConnection(orgId)) {
String sql = "SELECT [b_id], [b_account], [b_name], [b_password] "
+ "FROM [b_user] WHERE [b_account] = ? AND [b_canuse] = 1";
// 字符串业务键方案:b_user.b_id 即用户账号本身,不再有独立 b_account
String sql = "SELECT [b_id], [b_name], [b_password] "
+ "FROM [b_user] WHERE [b_id] = ? AND [b_canuse] = 1";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, account);
try (ResultSet resultSet = statement.executeQuery()) {
@@ -244,23 +244,12 @@ public class DbUtils {
return result;
}
private boolean shouldAssignKey(Object keyValue) {
if (keyValue == null) {
return true;
}
if (keyValue instanceof Number number) {
return number.longValue() < 0;
}
if (keyValue instanceof String text && !text.isBlank()) {
try {
return Long.parseLong(text) < 0;
} catch (NumberFormatException ignored) {
return false;
}
}
return false;
}
/**
* 通用新增:不再自动生成主键(字符串业务键方案下,主键由请求方提供,
* 例如 s_module.b_id=模块编码、s_field 的 (b_module_id,b_field))。
* 需要数据库生成主键的场景(如未来 bigint 业务表、bf_files 文件表)
* 由调用方先取号(IdGenerator / 文件服务 subid)再随行写入。
*/
public int insert(
Connection connection,
TableMetadata table,
@@ -268,9 +257,6 @@ public class DbUtils {
Map<String, Object> row
) throws SQLException {
Map<String, Object> normalizedRow = normalizeRow(row);
if (keyColumn != null && shouldAssignKey(normalizedRow.get(keyColumn.normalizedName()))) {
normalizedRow.put(keyColumn.normalizedName(), IdGenerator.nextId());
}
LinkedHashMap<ColumnMetadata, Object> values = writableValues(
table,
normalizedRow,
@@ -293,13 +279,34 @@ public class DbUtils {
ColumnMetadata keyColumn,
Map<String, Object> row
) throws SQLException {
return update(connection, table, List.of(keyColumn), row);
}
/**
* 通用修改:支持单列或多列业务键定位(字符串业务键方案下,
* 如 s_field 以 (b_module_id, b_field) 为键)。主键值从 row 中读取。
*/
public int update(
Connection connection,
TableMetadata table,
List<ColumnMetadata> keyColumns,
Map<String, Object> row
) throws SQLException {
if (keyColumns == null || keyColumns.isEmpty()) {
throw new BusinessException("更新操作必须指定主键字段");
}
Map<String, Object> normalizedRow = normalizeRow(row);
Object keyValue = requiredKeyValue(normalizedRow, keyColumn, table.requestedName());
for (ColumnMetadata keyColumn : keyColumns) {
requiredKeyValue(normalizedRow, keyColumn, table.requestedName());
}
LinkedHashMap<ColumnMetadata, Object> values = writableValues(
table,
normalizedRow,
keyColumn.normalizedName()
null
);
for (ColumnMetadata keyColumn : keyColumns) {
values.remove(keyColumn);
}
if (values.isEmpty()) {
throw new BusinessException("没有可修改的字段: " + table.requestedName());
}
@@ -307,12 +314,17 @@ public class DbUtils {
String setSql = values.keySet().stream()
.map(column -> quoteColumn(column) + " = ?")
.collect(Collectors.joining(", "));
String whereSql = keyColumns.stream()
.map(column -> quoteColumn(column) + " = ?")
.collect(Collectors.joining(" AND "));
String sql = "UPDATE " + table.quotedName()
+ " SET " + setSql
+ " WHERE " + quoteColumn(keyColumn) + " = ?";
+ " WHERE " + whereSql;
List<Object> parameters = new ArrayList<>(values.values());
parameters.add(keyValue);
for (ColumnMetadata keyColumn : keyColumns) {
parameters.add(normalizedRow.get(keyColumn.normalizedName()));
}
return executeWrite(connection, sql, parameters, "修改");
}
@@ -0,0 +1,37 @@
package cn.g3soft.fmsapi.utils;
import java.security.SecureRandom;
import java.util.UUID;
/**
* 业务层 UUIDv7 生成器(RFC 9562 时间有序版本)。
* <p>UUID 主键不再依赖雪花 bigint:日志表 s_log_* 使用 uniqueidentifier 主键,
* 主键非聚集、按 b_occurdatetime 建立聚集索引,因此只需按时间大致有序即可。
* <p>生成结果满足 UUIDv7 布局:
* <pre>
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | unix_ts_ms |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | unix_ts_ms | ver | rand_a |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |var| rand_b |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* </pre>
*/
public final class UuidV7Generator {
private static final SecureRandom RANDOM = new SecureRandom();
private UuidV7Generator() {
}
public static UUID nextUuid() {
long unixTsMs = System.currentTimeMillis() & 0xFFFFFFFFFFFFL;
long mostSigBits = (unixTsMs << 16) | (RANDOM.nextLong() & 0x0FFFL);
mostSigBits = (mostSigBits & ~0xF000L) | 0x7000L;
long leastSigBits = (RANDOM.nextLong() & 0x3FFFFFFFFFFFFFFFL) | 0x8000000000000000L;
return new UUID(mostSigBits, leastSigBits);
}
}
@@ -47,8 +47,7 @@ class AuthServiceTests {
"FMS",
"g3soft"
)).thenReturn(List.of(Map.of(
"b_id", 828084517703744L,
"b_account", "g3soft",
"b_id", "g3soft",
"b_name", "G3Soft",
"b_password", ""
)));
@@ -64,7 +63,7 @@ class AuthServiceTests {
assertThat(result).isPresent();
assertThat(result.orElseThrow()).containsEntry("token", "token");
Map<?, ?> user = (Map<?, ?>) result.orElseThrow().get("user");
assertThat(user.get("id")).isEqualTo(828084517703744L);
assertThat(user.get("id")).isEqualTo("g3soft");
assertThat(user.get("account")).isEqualTo("g3soft");
assertThat(user.get("name")).isEqualTo("G3Soft");
}
@@ -54,3 +54,4 @@ cn\g3soft\fmsapi\utils\ClientInfoExtractor.class
cn\g3soft\fmsapi\exception\AuthenticationException.class
cn\g3soft\fmsapi\database\OrgDatabaseProperties.class
cn\g3soft\fmsapi\service\DataSaveService$AutoCodeFormat.class
cn\g3soft\fmsapi\utils\UuidV7Generator.class
@@ -42,3 +42,4 @@ D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\utils\snowflake\cor
D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\utils\snowflake\idgen\DefaultIdGenerator.java
D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\utils\snowflake\idgen\IdGenerator.java
D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\utils\StringUtils.java
D:\workspace\code\fms\fms-api\src\main\java\cn\g3soft\fmsapi\utils\UuidV7Generator.java