无尘阁日记

无尘阁日记

跟AI一起设计TOKEN方案
2026-03-12

我问:

我们现在的token生成方案是,在我们系统输入一个app_key,然后随机生成一个私钥,即app_secret,然后用户从我系统复制这个app_key和生成的app_secret,还有一个company_id(企业统一信用编码)请求我们的接口login获取token。请看一下我的表是否足够,哪里要修改,你指出修改地,并重新设计那个表。

AI答:

我看了你这份表结构,结论先说:

你现在这套“输入 app_key,系统随机生成 app_secret,再加 company_id(企业统一信用代码)去 login 换 token”的方案,现有表不算完全不够用,但建模还不够干净,后续一定会遇到维护和安全问题。核心问题不在 token_log,而在你把“对接方基础信息”和“认证凭据”揉进了同一张 bridge_partner 表里。

这会带来几个明显问题:

一、你当前设计里的主要问题

1. bridge_partner 职责过重

你现在的 bridge_partner 里同时放了:

一类是“对接方基础配置”,比如 partner_codepartner_name、域名、重试次数这些;
另一类是“认证凭据”,比如 app_keyapp_secret
还有一类是“企业识别信息”,比如 credit_code

这三类东西其实不是一个层面的东西。

更准确地说:

bridge_partner 应该表示“谁在跟我对接”;
app_key/app_secret 应该表示“这个对接方用哪套凭据来认证”;
company_id 则是“本次登录要代表哪个企业”。

你现在把它们都塞到一张表里,后面一旦遇到下面这些场景,就会很难受:

场景一:一个 partner 需要多套 app_key/app_secret

比如测试一套,正式一套;或者同一个 partner 给不同企业配不同凭据。
现在这张表只能放一套,明显不够。

场景二:app_secret 要轮换

比如出于安全原因,需要重新生成 secret。
你现在只有一个 app_secret 字段,没有版本、启用时间、失效时间、上次轮换时间,也没有“旧 secret 是否保留宽限期”的空间。

场景三:想避免明文存储 secret

你当前直接把 app_secret 明文落库了。这个风险偏高。
如果数据库被查到,等于所有调用方都裸奔。

2. 缺少针对 login 的唯一约束

你现在 login 是靠:

app_key + app_secret + company_id

来换 token。

但表里没有体现“这一组关系到底如何唯一定位”。
当前 bridge_partner 只有这个唯一键:

uk_tenant_partner_code (tenant_id, partner_code)

这只保证“同租户下 partner_code 唯一”,并不能保证:

app_key 唯一
或者 company_id + app_key 唯一
或者同一企业下不会配出重复 key

这会让 login 查询和后续排障都变得模糊。

3. credit_code 命名和你的接口概念不一致

你接口上说的是 company_id(企业统一信用编码)
表里写的是 credit_code

从业务上看它们大概率是同一个东西,但名字不一致会导致:

接口文档写 company_id
数据库写 credit_code
代码里可能又写 enterprise_code

最后维护的人会晕。

这一类字段,最好统一成一个概念名。
我的建议是:

数据库里改成更明确的:company_identerprise_credit_code
如果你接口已经定了 company_id,那表里就直接也叫 company_id,最省心。

4. bridge_token_log 现在更像“签发日志”,但不是完整的认证日志

你现在这个表保存了:

jtitokenexpire_timestatussource_ip、请求响应报文。

这能记录“发过什么 token”,但对登录排障来说,还缺几类关键数据:

第一类:认证结果原因

比如:

  • app_key 不存在

  • secret 错误

  • company_id 不匹配

  • partner 已停用

  • secret 已过期

  • 来源 IP 不在白名单

这些最好单独落一个 auth_result_code / auth_result_msg

第二类:凭据版本

如果以后 secret 轮换了,你得知道这次 token 是用哪一版 secret 发出来的。

第三类:token 是否主动作废

你现在 status=0/1 太粗。
更细一点要能区分:

  • 有效

  • 过期失效

  • 主动吊销

  • 替换失效

5. token 明文存库风险偏高

现在 bridge_token_log.token 直接存完整 JWT 明文。

如果只是做追踪,通常没必要完整保存。更稳妥的做法是:

  • jti

  • token_hash

  • 最多存一个 token_preview(前20位+后10位)

  • 除非业务明确要求可回放,否则不要长久保存完整 token

因为 JWT 一旦没过期,被拿到就能直接用。

二、我建议你怎么改

我建议不要只改一个字段,而是把认证凭据从 bridge_partner 中拆出去

也就是说,改成下面这个结构更合理:

1. bridge_partner

只保留对接方基础资料,不放 app_key/app_secret

2. bridge_partner_auth

专门存认证凭据和 company_id 绑定关系

3. bridge_token_log

记录 token 签发与认证结果

这样以后你要做:

  • secret 轮换

  • 测试/正式双凭据

  • 停用某一套 key

  • 限定某企业使用某个 key

  • 按 company_id 查找调用方

  • 统计失败次数、锁定策略

都会顺很多。

三、推荐的认证关系模型

你现在的登录逻辑,本质上应该是:

先按 app_key 找到一条启用中的认证配置,
再校验 company_id 是否匹配,
再校验 app_secret 是否正确,
再判断 partner 是否启用、IP 是否允许、secret 是否过期,
最后签发 token。

所以认证配置表至少要表达出这些信息:

  • 这套凭据属于哪个 partner

  • 它绑定哪个 company_id

  • app_key 是什么

  • app_secret 如何存

  • 当前是否启用

  • 什么时候生效,什么时候失效

  • 是否允许轮换

  • 最近一次使用时间

  • 最近一次失败时间

  • 连续失败次数

  • secret 版本号

四、我建议你修改的点

需要从 bridge_partner 删除的字段

建议删掉这些认证类字段:

  • app_key

  • app_secret

这两个不应继续留在 partner 基础表里。

需要调整命名的字段

建议把:

  • credit_code

改成:

  • company_id

因为你的接口已经是这个概念,统一最好。

需要新增的表

新增一张:

  • bridge_partner_auth

专门管登录认证。

需要增强的日志表

bridge_token_log 建议增加:

  • auth_id

  • company_id

  • auth_result_code

  • auth_result_msg

  • token_status

  • token_hash

  • token_preview

  • issued_at

  • revoked_at

  • secret_version

五、我给你的最终推荐版表设计

下面我直接给你一版我认为更适合你当前方案的 SQL。
我尽量保持和你现有命名风格一致,方便你落地。

1. 重构后的对接方基础表

DROP TABLE IF EXISTS `bridge_partner`;
CREATE TABLE `bridge_partner`
(
    `id`                     BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`              BIGINT NOT NULL COMMENT '企业租户ID',
    `enterprise_id`          BIGINT NOT NULL COMMENT '匹配主系统企业ID',
    `partner_code`           VARCHAR(100) NOT NULL COMMENT '对接方编码,如nuoming',
    `partner_name`           VARCHAR(255) NOT NULL COMMENT '对接方名称',
    `enterprise_name`        VARCHAR(255) DEFAULT NULL COMMENT '企业名称',

    `api_host_test`          VARCHAR(500) DEFAULT NULL COMMENT '测试环境域名/IP',
    `api_host_production`    VARCHAR(500) DEFAULT NULL COMMENT '正式环境域名/IP',

    `ip_allow_list`          VARCHAR(2000) DEFAULT NULL COMMENT 'IP白名单,多个逗号分隔',
    `max_retry_count`        INT DEFAULT 3 COMMENT '最大重试次数',
    `retry_interval_seconds` INT DEFAULT 60 COMMENT '重试间隔秒',

    `status`                 TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0停用,1启用',
    `remark`                 VARCHAR(500) DEFAULT NULL COMMENT '备注',

    `created_by`             BIGINT DEFAULT NULL COMMENT '创建人',
    `updated_by`             BIGINT DEFAULT NULL COMMENT '更新人',
    `is_deleted`             TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0否1是',
    `deleted_time`           DATETIME DEFAULT NULL COMMENT '删除时间',
    `create_time`            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_tenant_partner_code` (`tenant_id`, `partner_code`) USING BTREE,
    KEY `idx_enterprise_id` (`enterprise_id`) USING BTREE,
    KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对接方基础配置表';

2. 新增认证配置表

这一张是核心。

DROP TABLE IF EXISTS `bridge_partner_auth`;
CREATE TABLE `bridge_partner_auth`
(
    `id`                       BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`                BIGINT NOT NULL COMMENT '企业租户ID',
    `enterprise_id`            BIGINT NOT NULL COMMENT '主系统企业ID',
    `partner_id`               BIGINT NOT NULL COMMENT '关联bridge_partner.id',

    `company_id`               VARCHAR(100) NOT NULL COMMENT '企业统一信用代码,login接口传入的company_id',
    `app_key`                  VARCHAR(200) NOT NULL COMMENT '系统分配的app_key',

    `app_secret_hash`          VARCHAR(255) NOT NULL COMMENT 'app_secret摘要,建议保存哈希,不保存明文',
    `app_secret_enc`           VARCHAR(1000) DEFAULT NULL COMMENT '加密后的app_secret密文,可选;仅在确有展示/重发需求时保留',
    `secret_version`           INT NOT NULL DEFAULT 1 COMMENT 'secret版本号,便于轮换',
    `secret_hint`              VARCHAR(100) DEFAULT NULL COMMENT 'secret提示信息,例如前3后2位,用于人工核对',

    `auth_status`              TINYINT NOT NULL DEFAULT 1 COMMENT '认证状态:0停用,1启用',
    `effective_time`           DATETIME DEFAULT NULL COMMENT '生效时间',
    `expire_time`              DATETIME DEFAULT NULL COMMENT '失效时间',

    `last_used_time`           DATETIME DEFAULT NULL COMMENT '最近一次成功使用时间',
    `last_failed_time`         DATETIME DEFAULT NULL COMMENT '最近一次失败时间',
    `failed_count`             INT NOT NULL DEFAULT 0 COMMENT '连续失败次数',

    `allow_token_issue`        TINYINT NOT NULL DEFAULT 1 COMMENT '是否允许签发token:0否1是',
    `remark`                   VARCHAR(500) DEFAULT NULL COMMENT '备注',

    `created_by`               BIGINT DEFAULT NULL COMMENT '创建人',
    `updated_by`               BIGINT DEFAULT NULL COMMENT '更新人',
    `is_deleted`               TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0否1是',
    `deleted_time`             DATETIME DEFAULT NULL COMMENT '删除时间',
    `create_time`              DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`              DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    PRIMARY KEY (`id`) USING BTREE,

    UNIQUE KEY `uk_app_key` (`app_key`) USING BTREE,
    UNIQUE KEY `uk_partner_company_secret_ver` (`partner_id`, `company_id`, `secret_version`) USING BTREE,

    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_company_id` (`company_id`) USING BTREE,
    KEY `idx_enterprise_id` (`enterprise_id`) USING BTREE,
    KEY `idx_auth_status` (`auth_status`) USING BTREE,
    KEY `idx_expire_time` (`expire_time`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对接认证配置表';

六、token 日志表也建议一起重构

DROP TABLE IF EXISTS `bridge_token_log`;
CREATE TABLE `bridge_token_log`
(
    `id`                BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`         BIGINT NOT NULL COMMENT '企业租户ID',
    `enterprise_id`     BIGINT NOT NULL COMMENT '主系统企业ID',
    `partner_id`        BIGINT NOT NULL COMMENT '关联bridge_partner.id',
    `auth_id`           BIGINT DEFAULT NULL COMMENT '关联bridge_partner_auth.id',

    `company_id`        VARCHAR(100) DEFAULT NULL COMMENT '本次login传入的company_id',
    `request_id`        VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',

    `grant_type`        VARCHAR(50) NOT NULL DEFAULT 'client_credentials' COMMENT '授权类型',
    `jti`               VARCHAR(100) DEFAULT NULL COMMENT 'JWT唯一ID',
    `token_hash`        VARCHAR(255) DEFAULT NULL COMMENT 'token摘要',
    `token_preview`     VARCHAR(100) DEFAULT NULL COMMENT 'token预览,建议前20位+后10位',
    `token_status`      VARCHAR(30) NOT NULL DEFAULT 'ISSUED' COMMENT 'ISSUED/EXPIRED/REVOKED/REPLACED/FAILED',

    `issued_at`         DATETIME DEFAULT NULL COMMENT '签发时间',
    `expire_time`       DATETIME DEFAULT NULL COMMENT '过期时间',
    `revoked_at`        DATETIME DEFAULT NULL COMMENT '吊销时间',

    `auth_result_code`  VARCHAR(64) DEFAULT NULL COMMENT '认证结果码,如SUCCESS/APP_KEY_NOT_FOUND/SECRET_INVALID/COMPANY_ID_MISMATCH/IP_DENIED/PARTNER_DISABLED/SECRET_EXPIRED',
    `auth_result_msg`   VARCHAR(1000) DEFAULT NULL COMMENT '认证结果说明',

    `source_ip`         VARCHAR(50) DEFAULT NULL COMMENT '请求来源IP',
    `request_body`      JSON DEFAULT NULL COMMENT '获取token请求体',
    `response_body`     JSON DEFAULT NULL COMMENT '获取token响应体',

    `create_time`       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_jti` (`jti`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_auth_id` (`auth_id`) USING BTREE,
    KEY `idx_enterprise_id` (`enterprise_id`) USING BTREE,
    KEY `idx_company_id` (`company_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_expire_time` (`expire_time`) USING BTREE,
    KEY `idx_token_status` (`token_status`) USING BTREE,
    KEY `idx_auth_result_code` (`auth_result_code`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='Token签发与认证日志表';

七、为什么我推荐这样设计

1. 更贴合你真实登录动作

你现在登录动作不是“partner 登录”,而是:

“某个 company_id 使用某套 app_key/app_secret 登录”。

所以认证配置应该是一个独立对象,而不是 partner 表上的几个字段。

2. 支持 secret 轮换

后面你要重新生成 secret,不需要覆盖原字段,只需要:

  • 老记录失效

  • 新增一条 secret_version + 1

这样历史 token 也能追到。

3. 支持更严格的安全控制

你将来可以方便加:

  • 连续失败 5 次锁定

  • 限制 IP

  • 限制有效期

  • 临时停用某个 app_key

  • 审计某企业近期所有登录失败

4. 排障会轻松很多

以后别人说:

“我 app_key 没问题,为啥登录失败?”

你一查 bridge_token_log 就能直接看到:

  • 用的是哪个 auth_id

  • company_id 是否匹配

  • 是 secret 错误还是 partner 停用了

  • 还是 IP 不在白名单

八、如果你暂时不想拆表,最低限度也要这样改

如果你现在不想大动,只想在 bridge_partner 上最小修改,那至少也得做这些:

必改项

1. credit_code 改成 company_id

统一接口和数据库概念。

2. app_key 加唯一索引

不然登录查询不稳定。

3. app_secret 不要明文存

改成:

  • app_secret_hash

  • app_secret_enc(可选)

4. 增加 secret 生命周期字段

比如:

  • secret_version

  • effective_time

  • expire_time

5. 增加登录失败控制字段

比如:

  • last_used_time

  • last_failed_time

  • failed_count

但我还是建议你拆表,因为这个改法只是“勉强能用”。

九、你这套 login 接口服务端校验顺序,我也顺便帮你定一下

建议后端校验顺序固定为:

第一步:按 app_key 查认证配置

查不到直接返回 APP_KEY_NOT_FOUND

第二步:校验认证配置状态

如停用、已删除、已过期,直接拒绝

第三步:校验 company_id

不匹配返回 COMPANY_ID_MISMATCH

第四步:校验 app_secret

用 hash 比对,不要直接比明文

第五步:校验 IP 白名单

不通过返回 IP_DENIED

第六步:签发 token

生成:

  • jti

  • iat

  • exp

  • partner_id

  • enterprise_id

  • company_id

  • app_key

这样 token 里带上 company_id,后面业务接口鉴权时也更稳。

十、最后给你一个明确结论

你当前表结构里,最该改的是 bridge_partner
不是说它不能用,而是它现在把“对接方信息”和“认证凭据”混在一起了,短期能跑,长期一定会乱。

我的最终建议是:

推荐方案

保留 bridge_partner 做基础配置表,新增 bridge_partner_auth 做认证表,同时增强 bridge_token_log

不推荐方案

继续把 app_key/app_secret/company_id 塞在 bridge_partner 一张表里。

因为你现在已经有 token 签发日志表了,说明这块认证体系已经不是一次性的临时功能,而是正式能力。正式能力就应该拆清楚。

如果你愿意,我下一步可以直接基于你现有整份 SQL,给你输出一版“可直接替换落库”的完整认证模块 SQL,只包含这三张表的最终版。

我问:

对于我们来说一个partner就相当于一家企业。请你明确这一点。

另外还需要在填写企业配置的时候点击一下连通对方OA,这个也需要记录,是否联通,为何无法联通等。

另外我们的这个租户id实际就是企业的ID,是我们系统中唯一编码,因此enterprise_id改成enterprise_code,用于存储企业统一信用编码。company_i就可以去掉了。

用户在前端点击生成app_secret这个过程不需要记录吗?你如果有记录,请你详细注释。还是说bridge_partner_auth就是这张表,只不过不是记录明文app_secret,但是前端需要显示这个明文啊,怎么得到?

secret_version这个怎么使用的?

grant_type啥意思,请详细注释。

jti如何生成,规则是啥,请详细注释。

请你直接输出sql,注释放在sql里。

AI答:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

USE `oa_bridge_v4`;

-- =========================================================
-- 对接企业主表
-- 这里明确:对我们来说,一个 partner 就等于一家企业
-- 所以本表既是“对接方配置表”,也是“企业级认证配置表”
--
-- 设计原则:
-- 1)tenant_id:存我们系统中的企业唯一编码。你已明确它就是企业ID,所以继续保留tenant_id即可
-- 2)enterprise_code:改为存企业统一社会信用代码,替代原 enterprise_id
-- 3)company_id:去掉,不再单独存,因为本质上已被 enterprise_code 替代
-- 4)app_key:对外提供的接入标识,建议全局唯一
-- 5)app_secret:
--    不建议仅存明文。这里同时设计三类字段:
--    - app_secret_hash:用于登录校验,服务端拿用户传入的明文 secret 做同算法摘要比对
--    - app_secret_ciphertext:用于“确实有业务需要再次展示明文”时,保存加密密文;不是明文
--    - app_secret_mask:用于前端列表展示,例如 abcd****wxyz
--    实际明文 secret 不建议长期明文落库
-- 6)前端点击“生成 app_secret”是否需要记录?
--    需要。原因:
--    - 谁生成的
--    - 什么时候生成的
--    - 是首次生成还是重置
--    - 是否成功
--    - 失败原因
--    因此下面单独设计 bridge_partner_secret_log 记录“生成/重置动作日志”
--    该日志表默认不存明文 secret,只记录动作、版本、结果等审计信息
-- 7)前端如何显示明文?
--    推荐方式:
--    - 后端生成 secret 时,在“生成成功的那个瞬间”把明文直接返回前端一次
--    - 前端提醒用户立刻复制保存
--    - 数据库只存 hash 和 ciphertext,不存明文
--    - 若后续确实还要“再次查看明文”,则只能由服务端用 app_secret_ciphertext 解密后返回
--      但这会提高安全风险,因此更推荐“仅首次显示一次”
-- 8)secret_version 如何使用?
--    - 首次生成时为 1
--    - 之后每重置一次 app_secret,版本号 +1
--    - 登录时只校验当前生效版本
--    - token 日志里记录签发时使用的是第几版 secret,便于排障和审计
-- 9)OA连通性:
--    - 本表保留当前最新连通状态
--    - 另配 bridge_partner_connectivity_log 记录每次手工测试/自动测试明细日志
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner`;
CREATE TABLE `bridge_partner`
(
    `id`                          BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',

    `tenant_id`                   BIGINT NOT NULL COMMENT '我们系统中的企业唯一编码。你已明确:tenant_id 实际就是企业ID',
    `enterprise_code`             VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码。替代原 enterprise_id 字段',

    `partner_code`                VARCHAR(100) NOT NULL COMMENT '对接企业编码。虽然 partner 等于企业,但仍保留,便于接口、日志、配置识别,如 nuoming',
    `partner_name`                VARCHAR(255) NOT NULL COMMENT '对接企业名称',
    `enterprise_name`             VARCHAR(255) DEFAULT NULL COMMENT '企业名称冗余字段,便于展示和检索',

    `api_host_test`               VARCHAR(500) DEFAULT NULL COMMENT '对方OA测试环境接口域名或IP',
    `api_host_production`         VARCHAR(500) DEFAULT NULL COMMENT '对方OA正式环境接口域名或IP',

    `app_key`                     VARCHAR(200) NOT NULL COMMENT '对外分配的接入标识。建议由系统生成并全局唯一。login时作为账号标识之一',
    `app_secret_hash`             VARCHAR(255) NOT NULL COMMENT 'app_secret 摘要值,用于登录校验;不存明文。用户传入明文后,服务端做同算法摘要比对',
    `app_secret_ciphertext`       VARCHAR(2000) DEFAULT NULL COMMENT 'app_secret 加密密文。仅当业务要求“可再次展示明文”时使用;不是明文。若采用“只展示一次”方案,可为空',
    `app_secret_mask`             VARCHAR(100) DEFAULT NULL COMMENT 'app_secret 脱敏展示值,如 abcd****wxyz,用于前端列表回显,避免暴露完整secret',
    `secret_version`              INT NOT NULL DEFAULT 1 COMMENT '当前生效的 secret 版本号。首次生成为1,每重置一次+1。token签发日志需记录本版本号,便于定位“某token是用哪一版secret签发的”',
    `secret_last_generated_time`  DATETIME DEFAULT NULL COMMENT '最近一次生成或重置 app_secret 的时间',
    `secret_last_generated_by`    BIGINT DEFAULT NULL COMMENT '最近一次生成或重置 app_secret 的操作人',
    `secret_effective_time`       DATETIME DEFAULT NULL COMMENT '当前 app_secret 生效时间。通常生成成功后立即生效',
    `secret_expire_time`          DATETIME DEFAULT NULL COMMENT '当前 app_secret 失效时间。若为空表示长期有效;若有值则超过时间后 login 不允许再签发 token',

    `login_failed_count`          INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数。可用于风控,例如连续失败过多后临时锁定',
    `last_login_success_time`     DATETIME DEFAULT NULL COMMENT '最近一次 login 成功时间',
    `last_login_failed_time`      DATETIME DEFAULT NULL COMMENT '最近一次 login 失败时间',

    `ip_allow_list`               VARCHAR(2000) DEFAULT NULL COMMENT '调用方IP白名单,多个用英文逗号分隔;为空表示不限制',
    `max_retry_count`             INT DEFAULT 3 COMMENT '业务接口重试最大次数。是接口调用重试策略,不是 login 重试次数',
    `retry_interval_seconds`      INT DEFAULT 60 COMMENT '业务接口重试间隔秒数',

    `oa_connect_status`           TINYINT NOT NULL DEFAULT 0 COMMENT '当前对方OA连通状态:0-未测试,1-连通成功,2-连通失败',
    `oa_connect_env`              VARCHAR(20) DEFAULT NULL COMMENT '最近一次测试的环境:TEST/PRODUCTION',
    `oa_connect_time`             DATETIME DEFAULT NULL COMMENT '最近一次点击“连通对方OA”测试的时间',
    `oa_connect_result_code`      VARCHAR(64) DEFAULT NULL COMMENT '最近一次连通测试结果码,如 SUCCESS/TIMEOUT/DNS_ERROR/HTTP_401/HTTP_500/SOCKET_ERROR',
    `oa_connect_result_msg`       VARCHAR(1000) DEFAULT NULL COMMENT '最近一次连通测试结果说明,例如“DNS解析失败”“连接超时”“鉴权失败”等',
    `oa_connect_response_time_ms` INT DEFAULT NULL COMMENT '最近一次连通测试耗时,单位毫秒',
    `oa_connect_by`               BIGINT DEFAULT NULL COMMENT '最近一次点击“连通对方OA”的操作人',

    `status`                      TINYINT NOT NULL DEFAULT 1 COMMENT '记录状态:0-停用,1-启用。停用后不允许 login,不允许业务接口交互',
    `remark`                      VARCHAR(500) DEFAULT NULL COMMENT '备注',

    `created_by`                  BIGINT DEFAULT NULL COMMENT '创建人',
    `updated_by`                  BIGINT DEFAULT NULL COMMENT '更新人',
    `is_deleted`                  TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0否,1是。逻辑删除',
    `deleted_time`                DATETIME DEFAULT NULL COMMENT '删除时间',
    `create_time`                 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`                 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_tenant_id` (`tenant_id`) USING BTREE,
    UNIQUE KEY `uk_partner_code` (`partner_code`) USING BTREE,
    UNIQUE KEY `uk_app_key` (`app_key`) USING BTREE,
    UNIQUE KEY `uk_enterprise_code` (`enterprise_code`) USING BTREE,

    KEY `idx_partner_name` (`partner_name`) USING BTREE,
    KEY `idx_status` (`status`) USING BTREE,
    KEY `idx_oa_connect_status` (`oa_connect_status`) USING BTREE,
    KEY `idx_secret_expire_time` (`secret_expire_time`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对接企业主表(一个partner就等于一家企业,同时承载企业级认证配置)';


-- =========================================================
-- app_secret 生成/重置日志表
--
-- 为什么需要这张表:
-- 1)前端点击“生成 app_secret”是一个关键安全动作,建议必须审计
-- 2)要知道是谁点的、何时点的、是首次生成还是重置、是否成功、失败原因是什么
-- 3)一般不在此表保存明文 secret,避免明文到处扩散
--
-- 关于“前端需要显示这个明文,怎么得到”:
-- 推荐流程:
-- 1)服务端生成随机明文 secret
-- 2)服务端当场返回给前端一次,供用户复制
-- 3)数据库只落 hash / ciphertext / mask / version
-- 4)本日志表只记录动作,不落明文
--
-- 如果你业务上坚持要“稍后还能再看明文”,也不建议存在本表里,
-- 而是从 bridge_partner.app_secret_ciphertext 解密后返回,且必须做好权限控制和操作审计
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner_secret_log`;
CREATE TABLE `bridge_partner_secret_log`
(
    `id`                     BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`              BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`        VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码',
    `partner_id`             BIGINT NOT NULL COMMENT '关联 bridge_partner.id',

    `operation_type`         VARCHAR(30) NOT NULL COMMENT '操作类型:GENERATE-首次生成;RESET-重置secret;ROTATE-轮换secret。一般首次点击生成为GENERATE,之后重新生成为RESET或ROTATE',
    `old_secret_version`     INT DEFAULT NULL COMMENT '旧 secret 版本号。首次生成时为空;重置时记录原版本',
    `new_secret_version`     INT DEFAULT NULL COMMENT '新 secret 版本号。首次生成通常为1,之后每次+1',
    `generate_status`        TINYINT NOT NULL DEFAULT 0 COMMENT '生成结果:0-失败,1-成功',
    `generate_result_code`   VARCHAR(64) DEFAULT NULL COMMENT '结果码:SUCCESS/RANDOM_FAIL/ENCRYPT_FAIL/SAVE_FAIL/PERMISSION_DENIED 等',
    `generate_result_msg`    VARCHAR(1000) DEFAULT NULL COMMENT '结果说明,便于排障',

    `secret_mask`            VARCHAR(100) DEFAULT NULL COMMENT '本次生成出来的 secret 脱敏值,仅用于审计确认,不保存明文',
    `is_frontend_displayed`  TINYINT NOT NULL DEFAULT 0 COMMENT '本次明文 secret 是否已返回并展示给前端:0-否,1-是',
    `display_expire_time`    DATETIME DEFAULT NULL COMMENT '若前端采用一次性展示机制,可记录前端展示凭证失效时间;仅审计用途,可为空',

    `operator_id`            BIGINT DEFAULT NULL COMMENT '操作人ID',
    `operator_name`          VARCHAR(100) DEFAULT NULL COMMENT '操作人姓名',
    `source_ip`              VARCHAR(50) DEFAULT NULL COMMENT '操作来源IP',
    `request_id`             VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `request_body`           JSON DEFAULT NULL COMMENT '请求报文。注意不要写入明文 secret',
    `response_body`          JSON DEFAULT NULL COMMENT '响应报文。注意若含明文 secret,建议落库前脱敏或不存',

    `remark`                 VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_operation_type` (`operation_type`) USING BTREE,
    KEY `idx_new_secret_version` (`new_secret_version`) USING BTREE,
    KEY `idx_operator_id` (`operator_id`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='app_secret生成/重置审计日志表';


-- =========================================================
-- 对方OA连通性测试日志表
--
-- 用途:
-- 1)记录每次点击“连通对方OA”的完整历史,而不是只保留 bridge_partner 上的当前快照
-- 2)能回答:
--    - 是否测通过
--    - 在什么环境测的
--    - 为什么不通
--    - 请求耗时多少
--    - 是谁测的
--    - 当时用的哪个地址
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner_connectivity_log`;
CREATE TABLE `bridge_partner_connectivity_log`
(
    `id`                       BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`                BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`          VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码',
    `partner_id`               BIGINT NOT NULL COMMENT '关联 bridge_partner.id',

    `connect_env`              VARCHAR(20) NOT NULL COMMENT '测试环境:TEST/PRODUCTION',
    `target_host`              VARCHAR(500) DEFAULT NULL COMMENT '本次测试命中的目标地址。通常取当时配置的 api_host_test 或 api_host_production',
    `connect_status`           TINYINT NOT NULL DEFAULT 0 COMMENT '连通结果:0-失败,1-成功',
    `result_code`              VARCHAR(64) DEFAULT NULL COMMENT '结果码:SUCCESS/TIMEOUT/DNS_ERROR/HTTP_401/HTTP_403/HTTP_404/HTTP_500/SSL_ERROR/SOCKET_ERROR 等',
    `result_msg`               VARCHAR(1000) DEFAULT NULL COMMENT '结果说明,记录为何无法连通',
    `http_status_code`         INT DEFAULT NULL COMMENT '若走HTTP接口,可记录HTTP状态码',
    `response_time_ms`         INT DEFAULT NULL COMMENT '本次连通测试耗时,单位毫秒',

    `operator_id`              BIGINT DEFAULT NULL COMMENT '点击测试按钮的操作人ID',
    `operator_name`            VARCHAR(100) DEFAULT NULL COMMENT '点击测试按钮的操作人姓名',
    `source_ip`                VARCHAR(50) DEFAULT NULL COMMENT '操作来源IP',
    `request_id`               VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `request_body`             JSON DEFAULT NULL COMMENT '请求报文',
    `response_body`            JSON DEFAULT NULL COMMENT '响应报文',

    `remark`                   VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`              DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_connect_env` (`connect_env`) USING BTREE,
    KEY `idx_connect_status` (`connect_status`) USING BTREE,
    KEY `idx_operator_id` (`operator_id`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对方OA连通性测试日志表';


-- =========================================================
-- JWT签发日志表
--
-- 说明:
-- 1)你当前 login 不再需要 company_id,因为已明确一个 partner 就是一家企业
--    登录时可以只用 app_key + app_secret
-- 2)grant_type 是什么?
--    grant_type 是“授权类型”的意思,源自 OAuth2 的概念
--    对你这个场景,最贴近的是 client_credentials
--    含义是:不是某个个人用户登录,而是“系统对系统”凭据换 token
--    所以默认写 client_credentials 最合适
-- 3)jti 是什么?
--    jti = JWT ID,JWT唯一编号。每签发一个token,都应有一个全局唯一jti
--    用途:
--    - 防重复
--    - 可追踪某个token
--    - 可做吊销、黑名单、审计
-- 4)jti 如何生成?
--    推荐规则:
--    - 必须全局唯一
--    - 不建议用简单自增ID
--    - 建议使用 UUID、UUID去掉横杠、雪花ID、或“时间戳+随机串”
--    示例:
--      a)32位UUID无横杠:2f8c2f9e7b2347d6a1fcb3bb7d8c1234
--      b)业务前缀+时间+随机串:jwt_202603121530301234_ab12cd34ef56
--    推荐做法:
--      jti = lower(replace(uuid(), '-', ''))
--    简单、稳定、足够唯一
-- 5)token是否存明文?
--    安全上更推荐存 token_hash + token_preview
--    若你确实需要排查完整token,也可临时保留 token 明文
--    这里给你保留 token 字段,但同时加入 token_hash / token_preview,便于后续平滑切换
-- =========================================================
DROP TABLE IF EXISTS `bridge_token_log`;
CREATE TABLE `bridge_token_log`
(
    `id`                    BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`             BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`       VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码',
    `partner_id`            BIGINT NOT NULL COMMENT '关联 bridge_partner.id',

    `request_id`            VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `grant_type`            VARCHAR(50) NOT NULL DEFAULT 'client_credentials' COMMENT '授权类型。当前推荐固定为 client_credentials,表示系统对系统凭据换 token,不是自然人账号登录',
    `app_key_snapshot`      VARCHAR(200) DEFAULT NULL COMMENT '签发时使用的 app_key 快照。冗余保存,避免后续主表变化影响审计',
    `secret_version`        INT DEFAULT NULL COMMENT '签发该 token 时使用的 secret 版本号。便于排查“这个token是用哪一版secret签发的”',

    `jti`                   VARCHAR(100) NOT NULL COMMENT 'JWT唯一编号。建议使用 lower(replace(uuid(), ''-'', '''')) 生成,要求全局唯一,用于审计、追踪、吊销',
    `token`                 VARCHAR(2048) DEFAULT NULL COMMENT '签发的JWT明文token。若后续考虑安全收紧,可改为不再保存明文,只保留 token_hash 与 token_preview',
    `token_hash`            VARCHAR(255) DEFAULT NULL COMMENT 'token摘要值。用于校验和追踪,不暴露明文',
    `token_preview`         VARCHAR(100) DEFAULT NULL COMMENT 'token脱敏预览值,例如前20位 + ... + 后10位,便于人工排查',
    `issued_time`           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'token签发时间',
    `expire_time`           DATETIME NOT NULL COMMENT 'token过期时间',
    `token_status`          TINYINT NOT NULL DEFAULT 1 COMMENT 'token状态:0-失效,1-有效,2-已吊销。若只需要简单模型,可只用0和1',

    `auth_status`           TINYINT NOT NULL DEFAULT 1 COMMENT '本次login鉴权结果:0-失败,1-成功。即使鉴权失败,也建议记录日志,便于统计攻击和排障',
    `auth_result_code`      VARCHAR(64) DEFAULT NULL COMMENT '鉴权结果码:SUCCESS/APP_KEY_NOT_FOUND/SECRET_INVALID/PARTNER_DISABLED/SECRET_EXPIRED/IP_DENIED 等',
    `auth_result_msg`       VARCHAR(1000) DEFAULT NULL COMMENT '鉴权结果说明',

    `source_ip`             VARCHAR(50) DEFAULT NULL COMMENT '请求来源IP',
    `request_body`          JSON DEFAULT NULL COMMENT '获取token时的请求体。注意不要原样落库明文 app_secret,建议入库前脱敏',
    `response_body`         JSON DEFAULT NULL COMMENT '获取token时的响应体。注意若含完整token,需结合安全要求决定是否完整保存',

    `remark`                VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_jti` (`jti`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_expire_time` (`expire_time`) USING BTREE,
    KEY `idx_token_status` (`token_status`) USING BTREE,
    KEY `idx_auth_status` (`auth_status`) USING BTREE,
    KEY `idx_secret_version` (`secret_version`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='JWT签发与login鉴权日志表';


-- =========================================================
-- 可选:若你希望强制保证“主表最新连通结果”和“日志表最后一条记录”语义一致,
-- 可以在业务层约定:
-- 1)每次插入 bridge_partner_connectivity_log 成功后
-- 2)同步更新 bridge_partner 里的 oa_connect_* 快照字段
--
-- 可选:若你希望强制保证“生成secret动作”和“主表secret版本”一致,
-- 可以在业务层约定:
-- 1)先生成明文secret
-- 2)算 hash、密文、mask
-- 3)更新 bridge_partner 的 app_secret_* 与 secret_version
-- 4)插入 bridge_partner_secret_log
-- 5)将明文secret只在本次接口响应中返回前端一次
-- =========================================================

SET FOREIGN_KEY_CHECKS = 1;

如果你要,我下一步可以继续直接给你补一版“login 接口的请求/校验/生成 token 的完整后端流程注释版”,把 app_secret_hashapp_secret_ciphertextjtitoken_hash 这些字段在代码里怎么用一次讲透。

我问:

所谓的轮换secret是根据企业有效时间跟secret有效时间比对后,如果secret有效期过了,自动刷新吗?请你注明到注释里。

JWT签发日志表虽然明确了一个parter就是一家企业,我们还是拿这个来沟通,这是领导要求的,所以不变,但是是选填的。

只需要输出sql,注释还是写在里面。这次基本就差不多了。

AI答:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

USE `oa_bridge_v4`;

-- =========================================================
-- 对接企业主表
-- 这里明确:对我们来说,一个 partner 就等于一家企业
-- 但为便于与领导、业务、接口文档持续沟通,表名、字段名中仍保留 partner 这一叫法
--
-- 设计原则:
-- 1)tenant_id:存我们系统中的企业唯一编码。你已明确:tenant_id 实际就是企业ID
-- 2)enterprise_code:存企业统一社会信用代码,替代原 enterprise_id
-- 3)company_id:去掉,不再单独存,因为本质上已被 enterprise_code 替代
-- 4)app_key:对外提供的接入标识,建议全局唯一
-- 5)app_secret:
--    - app_secret_hash:用于 login 校验,不存明文
--    - app_secret_ciphertext:用于“确有业务需要再次展示明文”时保存密文;不是明文
--    - app_secret_mask:前端列表展示的脱敏值,如 abcd****wxyz
-- 6)secret_version:当前生效的 secret 版本号
--    - 首次生成时为 1
--    - 之后每重置一次 app_secret,版本号 +1
--    - token 日志里记录签发时使用的是第几版 secret,便于排障和审计
-- 7)关于 secret 轮换(这里按你的要求特别注明):
--    - 所谓“轮换 secret”,不是指单纯对比企业有效时间和 secret 有效时间后,数据库自己直接自动刷新
--    - 正确含义是:当系统发现当前 secret 已过期,或者管理员主动要求更换时,生成一套新的 secret,并将 secret_version + 1
--    - 是否“自动刷新”,属于业务策略,不是表结构本身的内建能力
--    - 你可以在业务层实现两种策略:
--      A. 自动轮换:当 login 或定时任务发现 secret_expire_time 已过,就自动生成新 secret,并更新版本
--      B. 人工轮换:secret 过期后不自动生成,要求前端/管理员点击“生成/重置 secret”
--    - 如果采用自动轮换,必须注意:新明文 secret 如何安全通知对方,否则对方拿不到新 secret,后续仍无法 login
--    - 因此从实际落地看,更推荐“过期后禁止签发 + 人工重置”,除非双方有成熟的密钥同步机制
--    - 企业有效时间与 secret 有效时间可以分别存在:
--      * 企业有效时间:代表这家企业整体是否仍具备对接资格
--      * secret 有效时间:代表当前这把 secret 是否还能用于 login
--      * login 时通常需要两者都有效,才允许签发 token
-- 8)OA连通性:
--    - 本表保留当前最新连通状态快照
--    - 另配 bridge_partner_connectivity_log 记录每次手工测试/自动测试明细日志
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner`;
CREATE TABLE `bridge_partner`
(
    `id`                          BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',

    `tenant_id`                   BIGINT NOT NULL COMMENT '我们系统中的企业唯一编码。你已明确:tenant_id 实际就是企业ID',
    `enterprise_code`             VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码。替代原 enterprise_id 字段',

    `partner_code`                VARCHAR(100) NOT NULL COMMENT '对接企业编码。虽然 partner 等于企业,但仍保留,便于接口、日志、配置识别,如 nuoming',
    `partner_name`                VARCHAR(255) NOT NULL COMMENT '对接企业名称',
    `enterprise_name`             VARCHAR(255) DEFAULT NULL COMMENT '企业名称冗余字段,便于展示和检索',

    `partner_effective_time`      DATETIME DEFAULT NULL COMMENT '企业对接生效时间。为空表示不限制起始时间',
    `partner_expire_time`         DATETIME DEFAULT NULL COMMENT '企业对接失效时间。超过该时间后,即使 app_key、app_secret 正确,也不允许 login 和业务交互',

    `api_host_test`               VARCHAR(500) DEFAULT NULL COMMENT '对方OA测试环境接口域名或IP',
    `api_host_production`         VARCHAR(500) DEFAULT NULL COMMENT '对方OA正式环境接口域名或IP',

    `app_key`                     VARCHAR(200) NOT NULL COMMENT '对外分配的接入标识。建议由系统生成并全局唯一。login时作为账号标识之一',
    `app_secret_hash`             VARCHAR(255) NOT NULL COMMENT 'app_secret 摘要值,用于 login 校验;不存明文。用户传入明文后,服务端做同算法摘要比对',
    `app_secret_ciphertext`       VARCHAR(2000) DEFAULT NULL COMMENT 'app_secret 加密密文。仅当业务要求“可再次展示明文”时使用;不是明文。若采用“只展示一次”方案,可为空',
    `app_secret_mask`             VARCHAR(100) DEFAULT NULL COMMENT 'app_secret 脱敏展示值,如 abcd****wxyz,用于前端列表回显,避免暴露完整 secret',
    `secret_version`              INT NOT NULL DEFAULT 1 COMMENT '当前生效的 secret 版本号。首次生成为1,每重置或轮换一次+1。token签发日志需记录本版本号,便于定位“某token是用哪一版secret签发的”',
    `secret_last_generated_time`  DATETIME DEFAULT NULL COMMENT '最近一次生成、重置或轮换 app_secret 的时间',
    `secret_last_generated_by`    BIGINT DEFAULT NULL COMMENT '最近一次生成、重置或轮换 app_secret 的操作人',
    `secret_effective_time`       DATETIME DEFAULT NULL COMMENT '当前 app_secret 生效时间。通常生成成功后立即生效',
    `secret_expire_time`          DATETIME DEFAULT NULL COMMENT '当前 app_secret 失效时间。超过该时间后,当前 secret 不再允许用于 login。是否自动轮换新 secret,由业务策略决定,不是表自动完成',
    `secret_rotate_mode`          VARCHAR(20) NOT NULL DEFAULT 'MANUAL' COMMENT 'secret轮换模式:MANUAL-人工轮换;AUTO-自动轮换。仅表示业务策略标识,不代表数据库会自动刷新。若为AUTO,通常由定时任务或login流程检测过期后触发生成新secret',
    `secret_rotate_notice`        VARCHAR(500) DEFAULT NULL COMMENT 'secret轮换策略备注。例如“过期后禁止签发,需人工重置”“过期后由定时任务自动轮换并邮件通知对方”',

    `login_failed_count`          INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数。可用于风控,例如连续失败过多后临时锁定',
    `last_login_success_time`     DATETIME DEFAULT NULL COMMENT '最近一次 login 成功时间',
    `last_login_failed_time`      DATETIME DEFAULT NULL COMMENT '最近一次 login 失败时间',

    `ip_allow_list`               VARCHAR(2000) DEFAULT NULL COMMENT '调用方IP白名单,多个用英文逗号分隔;为空表示不限制',
    `max_retry_count`             INT DEFAULT 3 COMMENT '业务接口重试最大次数。是接口调用重试策略,不是 login 重试次数',
    `retry_interval_seconds`      INT DEFAULT 60 COMMENT '业务接口重试间隔秒数',

    `oa_connect_status`           TINYINT NOT NULL DEFAULT 0 COMMENT '当前对方OA连通状态:0-未测试,1-连通成功,2-连通失败',
    `oa_connect_env`              VARCHAR(20) DEFAULT NULL COMMENT '最近一次测试的环境:TEST/PRODUCTION',
    `oa_connect_time`             DATETIME DEFAULT NULL COMMENT '最近一次点击“连通对方OA”测试的时间',
    `oa_connect_result_code`      VARCHAR(64) DEFAULT NULL COMMENT '最近一次连通测试结果码,如 SUCCESS/TIMEOUT/DNS_ERROR/HTTP_401/HTTP_500/SOCKET_ERROR',
    `oa_connect_result_msg`       VARCHAR(1000) DEFAULT NULL COMMENT '最近一次连通测试结果说明,例如“DNS解析失败”“连接超时”“鉴权失败”等',
    `oa_connect_response_time_ms` INT DEFAULT NULL COMMENT '最近一次连通测试耗时,单位毫秒',
    `oa_connect_by`               BIGINT DEFAULT NULL COMMENT '最近一次点击“连通对方OA”的操作人',

    `status`                      TINYINT NOT NULL DEFAULT 1 COMMENT '记录状态:0-停用,1-启用。停用后不允许 login,不允许业务接口交互',
    `remark`                      VARCHAR(500) DEFAULT NULL COMMENT '备注',

    `created_by`                  BIGINT DEFAULT NULL COMMENT '创建人',
    `updated_by`                  BIGINT DEFAULT NULL COMMENT '更新人',
    `is_deleted`                  TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0否,1是。逻辑删除',
    `deleted_time`                DATETIME DEFAULT NULL COMMENT '删除时间',
    `create_time`                 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`                 DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_tenant_id` (`tenant_id`) USING BTREE,
    UNIQUE KEY `uk_partner_code` (`partner_code`) USING BTREE,
    UNIQUE KEY `uk_app_key` (`app_key`) USING BTREE,
    UNIQUE KEY `uk_enterprise_code` (`enterprise_code`) USING BTREE,

    KEY `idx_partner_name` (`partner_name`) USING BTREE,
    KEY `idx_status` (`status`) USING BTREE,
    KEY `idx_oa_connect_status` (`oa_connect_status`) USING BTREE,
    KEY `idx_partner_expire_time` (`partner_expire_time`) USING BTREE,
    KEY `idx_secret_expire_time` (`secret_expire_time`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对接企业主表(一个partner就等于一家企业,同时承载企业级认证配置)';


-- =========================================================
-- app_secret 生成/重置/轮换日志表
--
-- 为什么需要这张表:
-- 1)前端点击“生成 app_secret”是一个关键安全动作,建议必须审计
-- 2)要知道是谁点的、何时点的、是首次生成还是重置/轮换、是否成功、失败原因是什么
-- 3)一般不在此表保存明文 secret,避免明文到处扩散
--
-- 关于“前端需要显示这个明文,怎么得到”:
-- 推荐流程:
-- 1)服务端生成随机明文 secret
-- 2)服务端当场返回给前端一次,供用户复制
-- 3)数据库只落 hash / ciphertext / mask / version
-- 4)本日志表只记录动作,不落明文
--
-- 关于“轮换 secret”:
-- 1)ROTATE 本质上也是生成新 secret,只是语义上强调“替换旧版本”
-- 2)若系统发现 secret 已过期,可根据 secret_rotate_mode 决定:
--    - MANUAL:仅记录不可用,要求管理员手动生成/重置
--    - AUTO:由业务程序自动生成新 secret,并写入本表 operation_type = ROTATE
-- 3)是否自动轮换,不是表本身完成,而是业务流程或定时任务完成
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner_secret_log`;
CREATE TABLE `bridge_partner_secret_log`
(
    `id`                     BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`              BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`        VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码',
    `partner_id`             BIGINT NOT NULL COMMENT '关联 bridge_partner.id',

    `operation_type`         VARCHAR(30) NOT NULL COMMENT '操作类型:GENERATE-首次生成;RESET-人工重置secret;ROTATE-轮换secret。轮换可由人工触发,也可由业务程序按策略自动触发',
    `trigger_type`           VARCHAR(20) NOT NULL DEFAULT 'MANUAL' COMMENT '触发方式:MANUAL-人工点击;AUTO-系统自动触发。若secret过期后由程序或定时任务自动生成新secret,则记为AUTO',
    `old_secret_version`     INT DEFAULT NULL COMMENT '旧 secret 版本号。首次生成时为空;重置或轮换时记录原版本',
    `new_secret_version`     INT DEFAULT NULL COMMENT '新 secret 版本号。首次生成通常为1,之后每次+1',
    `generate_status`        TINYINT NOT NULL DEFAULT 0 COMMENT '生成结果:0-失败,1-成功',
    `generate_result_code`   VARCHAR(64) DEFAULT NULL COMMENT '结果码:SUCCESS/RANDOM_FAIL/ENCRYPT_FAIL/SAVE_FAIL/PERMISSION_DENIED/SECRET_EXPIRED_AUTO_ROTATE 等',
    `generate_result_msg`    VARCHAR(1000) DEFAULT NULL COMMENT '结果说明,便于排障',

    `secret_mask`            VARCHAR(100) DEFAULT NULL COMMENT '本次生成出来的 secret 脱敏值,仅用于审计确认,不保存明文',
    `is_frontend_displayed`  TINYINT NOT NULL DEFAULT 0 COMMENT '本次明文 secret 是否已返回并展示给前端:0-否,1-是。若为系统自动轮换,一般为0,因为通常不会直接展示给前端',
    `display_expire_time`    DATETIME DEFAULT NULL COMMENT '若前端采用一次性展示机制,可记录前端展示凭证失效时间;仅审计用途,可为空',

    `operator_id`            BIGINT DEFAULT NULL COMMENT '操作人ID。若为AUTO自动触发,可为空或填系统用户ID',
    `operator_name`          VARCHAR(100) DEFAULT NULL COMMENT '操作人姓名。若为AUTO自动触发,可写SYSTEM',
    `source_ip`              VARCHAR(50) DEFAULT NULL COMMENT '操作来源IP。若为AUTO自动触发,可为空',
    `request_id`             VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `request_body`           JSON DEFAULT NULL COMMENT '请求报文。注意不要写入明文 secret',
    `response_body`          JSON DEFAULT NULL COMMENT '响应报文。注意若含明文 secret,建议落库前脱敏或不存',

    `remark`                 VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_operation_type` (`operation_type`) USING BTREE,
    KEY `idx_trigger_type` (`trigger_type`) USING BTREE,
    KEY `idx_new_secret_version` (`new_secret_version`) USING BTREE,
    KEY `idx_operator_id` (`operator_id`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='app_secret生成/重置/轮换审计日志表';


-- =========================================================
-- 对方OA连通性测试日志表
--
-- 用途:
-- 1)记录每次点击“连通对方OA”的完整历史,而不是只保留 bridge_partner 上的当前快照
-- 2)能回答:
--    - 是否测通过
--    - 在什么环境测的
--    - 为什么不通
--    - 请求耗时多少
--    - 是谁测的
--    - 当时用的哪个地址
-- =========================================================
DROP TABLE IF EXISTS `bridge_partner_connectivity_log`;
CREATE TABLE `bridge_partner_connectivity_log`
(
    `id`                       BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`                BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`          VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码',
    `partner_id`               BIGINT NOT NULL COMMENT '关联 bridge_partner.id',

    `connect_env`              VARCHAR(20) NOT NULL COMMENT '测试环境:TEST/PRODUCTION',
    `target_host`              VARCHAR(500) DEFAULT NULL COMMENT '本次测试命中的目标地址。通常取当时配置的 api_host_test 或 api_host_production',
    `connect_status`           TINYINT NOT NULL DEFAULT 0 COMMENT '连通结果:0-失败,1-成功',
    `result_code`              VARCHAR(64) DEFAULT NULL COMMENT '结果码:SUCCESS/TIMEOUT/DNS_ERROR/HTTP_401/HTTP_403/HTTP_404/HTTP_500/SSL_ERROR/SOCKET_ERROR 等',
    `result_msg`               VARCHAR(1000) DEFAULT NULL COMMENT '结果说明,记录为何无法连通',
    `http_status_code`         INT DEFAULT NULL COMMENT '若走HTTP接口,可记录HTTP状态码',
    `response_time_ms`         INT DEFAULT NULL COMMENT '本次连通测试耗时,单位毫秒',

    `operator_id`              BIGINT DEFAULT NULL COMMENT '点击测试按钮的操作人ID',
    `operator_name`            VARCHAR(100) DEFAULT NULL COMMENT '点击测试按钮的操作人姓名',
    `source_ip`                VARCHAR(50) DEFAULT NULL COMMENT '操作来源IP',
    `request_id`               VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `request_body`             JSON DEFAULT NULL COMMENT '请求报文',
    `response_body`            JSON DEFAULT NULL COMMENT '响应报文',

    `remark`                   VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`              DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_connect_env` (`connect_env`) USING BTREE,
    KEY `idx_connect_status` (`connect_status`) USING BTREE,
    KEY `idx_operator_id` (`operator_id`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='对方OA连通性测试日志表';


-- =========================================================
-- JWT签发日志表
--
-- 说明:
-- 1)虽然已经明确一个 partner 就是一家企业,但按你的要求,这里仍保留 partner 叫法用于沟通
-- 2)enterprise_code 设为可选填,所以允许为 NULL
-- 3)grant_type 是“授权类型”的意思,源自 OAuth2 概念
--    当前场景推荐固定为 client_credentials
--    含义:不是某个自然人账号登录,而是系统对系统凭据换 token
-- 4)jti = JWT ID,JWT唯一编号
--    用途:
--    - 标识每一个 token
--    - 防重复
--    - 可追踪
--    - 可吊销
-- 5)jti 生成规则建议:
--    - 必须全局唯一
--    - 不建议用简单自增ID
--    - 推荐使用:lower(replace(uuid(), '-', ''))
--    - 例如:2f8c2f9e7b2347d6a1fcb3bb7d8c1234
-- 6)token 是否存明文:
--    - 安全上更推荐只存 token_hash + token_preview
--    - 这里保留 token 字段,便于你当前排查;后续若安全要求提高,可改为不存明文
-- =========================================================
DROP TABLE IF EXISTS `bridge_token_log`;
CREATE TABLE `bridge_token_log`
(
    `id`                    BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `tenant_id`             BIGINT NOT NULL COMMENT '企业唯一编码',
    `enterprise_code`       VARCHAR(100) DEFAULT NULL COMMENT '企业统一社会信用代码。按当前要求,这里为选填,可为空',
    `partner_id`            BIGINT NOT NULL COMMENT '关联 bridge_partner.id。虽然partner本质上就是企业,但为便于沟通和领导要求,这里字段名保持不变',

    `request_id`            VARCHAR(100) DEFAULT NULL COMMENT '请求流水号',
    `grant_type`            VARCHAR(50) NOT NULL DEFAULT 'client_credentials' COMMENT '授权类型。当前推荐固定为 client_credentials,表示系统对系统凭据换 token,不是自然人账号登录',
    `app_key_snapshot`      VARCHAR(200) DEFAULT NULL COMMENT '签发时使用的 app_key 快照。冗余保存,避免后续主表变化影响审计',
    `secret_version`        INT DEFAULT NULL COMMENT '签发该 token 时使用的 secret 版本号。便于排查“这个 token 是用哪一版 secret 签发的”',

    `jti`                   VARCHAR(100) NOT NULL COMMENT 'JWT唯一编号。建议使用 lower(replace(uuid(), ''-'', '''')) 生成,要求全局唯一,用于审计、追踪、吊销',
    `token`                 VARCHAR(2048) DEFAULT NULL COMMENT '签发的JWT明文token。若后续考虑安全收紧,可改为不再保存明文,只保留 token_hash 与 token_preview',
    `token_hash`            VARCHAR(255) DEFAULT NULL COMMENT 'token摘要值。用于校验和追踪,不暴露明文',
    `token_preview`         VARCHAR(100) DEFAULT NULL COMMENT 'token脱敏预览值,例如前20位 + ... + 后10位,便于人工排查',
    `issued_time`           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'token签发时间',
    `expire_time`           DATETIME NOT NULL COMMENT 'token过期时间',
    `token_status`          TINYINT NOT NULL DEFAULT 1 COMMENT 'token状态:0-失效,1-有效,2-已吊销。若只需要简单模型,可只用0和1',

    `auth_status`           TINYINT NOT NULL DEFAULT 1 COMMENT '本次login鉴权结果:0-失败,1-成功。即使鉴权失败,也建议记录日志,便于统计攻击和排障',
    `auth_result_code`      VARCHAR(64) DEFAULT NULL COMMENT '鉴权结果码:SUCCESS/APP_KEY_NOT_FOUND/SECRET_INVALID/PARTNER_DISABLED/PARTNER_EXPIRED/SECRET_EXPIRED/IP_DENIED 等',
    `auth_result_msg`       VARCHAR(1000) DEFAULT NULL COMMENT '鉴权结果说明',

    `source_ip`             VARCHAR(50) DEFAULT NULL COMMENT '请求来源IP',
    `request_body`          JSON DEFAULT NULL COMMENT '获取token时的请求体。注意不要原样落库明文 app_secret,建议入库前脱敏',
    `response_body`         JSON DEFAULT NULL COMMENT '获取token时的响应体。注意若含完整token,需结合安全要求决定是否完整保存',

    `remark`                VARCHAR(500) DEFAULT NULL COMMENT '备注',
    `create_time`           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `uk_jti` (`jti`) USING BTREE,
    KEY `idx_partner_id` (`partner_id`) USING BTREE,
    KEY `idx_tenant_id` (`tenant_id`) USING BTREE,
    KEY `idx_request_id` (`request_id`) USING BTREE,
    KEY `idx_expire_time` (`expire_time`) USING BTREE,
    KEY `idx_token_status` (`token_status`) USING BTREE,
    KEY `idx_auth_status` (`auth_status`) USING BTREE,
    KEY `idx_secret_version` (`secret_version`) USING BTREE
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci COMMENT='JWT签发与login鉴权日志表';


SET FOREIGN_KEY_CHECKS = 1;