Compare commits
18
Commits
3ea09a1cdd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5426b573c1 | ||
|
|
44b13c96f6 | ||
|
|
062e9a652b | ||
|
|
57fca4e468 | ||
|
|
cd6c8a0450 | ||
|
|
81727978f1 | ||
|
|
1a0aecbd7e | ||
|
|
f7381c3f85 | ||
|
|
dc2d6697af | ||
|
|
cf6dc208ef | ||
|
|
23df8ffd75 | ||
|
|
8e2afda170 | ||
|
|
f50d4ab4a3 | ||
|
|
2768d58aa3 | ||
|
|
a5840ed7c6 | ||
|
|
e798751e4b | ||
|
|
7493265a05 | ||
|
|
d76143ad6e |
@@ -0,0 +1,2 @@
|
|||||||
|
# 勿提交含 API Token 的配置文件
|
||||||
|
jira_upload.env
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 影响分析报告 — g3fo-commit-jira 无 MCP 支持
|
||||||
|
|
||||||
|
## 1. 改动概览
|
||||||
|
|
||||||
|
- **背景**:技能原依赖 Cursor Atlassian MCP 完成 Jira 搜索/创建/评论;其它 IDE 无 MCP 时无法完成流程。
|
||||||
|
- **目标**:在仅配置 `jira_upload.env`(与上传附件相同 Token)时,通过脚本完成查询、创建 Issue、添加评论。
|
||||||
|
- **涉及模块**:`skills/g3fo-commit-jira/scripts/`、`SKILL.md`、`references/workflow.md`、`references/env_config.md`。
|
||||||
|
- **改动类型**:功能新增(脚本)+ 文档更新。
|
||||||
|
|
||||||
|
## 2. 方法级改动分析
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 新增 `jira_env.py` | 统一加载 `jira_upload.env`,支持在技能根目录查找配置文件。 |
|
||||||
|
| 新增 `jira_cli.py` | `myself` / `search` / `issue` / `create` / `comment` REST 调用。 |
|
||||||
|
| 修改 `upload_attachment.py` | 改为引用 `jira_env`,行为与原先一致(多一层技能目录 config 查找)。 |
|
||||||
|
| SKILL / workflow / env_config | 描述无 MCP 路径与命令示例。 |
|
||||||
|
|
||||||
|
**与原有逻辑差异**:上传附件 URL、认证方式未变;配置查找增加 `skills/g3fo-commit-jira/jira_upload.env`。
|
||||||
|
|
||||||
|
## 3. 调用方与影响范围
|
||||||
|
|
||||||
|
- **调用方**:Agent 按 SKILL 执行;用户手动运行脚本。
|
||||||
|
- **破坏性变更**:否。未配置 env 时 `upload_attachment` 仍报错;原 MCP 流程仍可用。
|
||||||
|
- **边界**:Jira Server/Data Center 与 Cloud API 差异未专门适配(与现有上传脚本一致,面向 Cloud)。
|
||||||
|
|
||||||
|
## 4. 风险与回滚
|
||||||
|
|
||||||
|
- **风险级别**:低。新脚本失败时退回 MCP 或仅本地 amend。
|
||||||
|
- **回滚**:删除 `jira_cli.py`/`jira_env.py` 并恢复 `upload_attachment.py` 内联配置逻辑;回滚文档。
|
||||||
|
- **回滚方式是否简单**:是。
|
||||||
|
|
||||||
|
## 5. 验证与测试
|
||||||
|
|
||||||
|
- `python -m py_compile` 通过;`jira_cli.py --help` 正常。
|
||||||
|
- 真实 Jira 调用需用户环境凭证,未在 CI 中执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 影响分析报告 — jira_cli Markdown → ADF 渲染(评论/描述)
|
||||||
|
|
||||||
|
## 1. 改动概览
|
||||||
|
|
||||||
|
- **背景**:REST 脚本原先把正文整段当作 ADF 段落,`###` 等在 Jira 界面显示为原文,与 Atlassian 插件/MCP 的 Markdown 体验不一致。
|
||||||
|
- **目标**:在 `jira_cli.py` 内将常见 Markdown 转为 ADF(heading、list、strong、code、link、codeBlock、rule、blockquote),使 `comment` / `create` 的展示接近网页富文本。
|
||||||
|
- **涉及模块**:`scripts/jira_cli.py`、`SKILL.md`。
|
||||||
|
- **改动类型**:功能增强(无 API 变更)。
|
||||||
|
|
||||||
|
## 2. 方法级改动分析
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `markdown_to_adf`(新) | 按行解析 Markdown,输出 ADF `doc`。 |
|
||||||
|
| `plain_to_adf` | 改为委托 `markdown_to_adf`,保持调用方不变。 |
|
||||||
|
| `parse_inline_adf`(新) | 行内 `**`、`` ` ``、`[text](url)`。 |
|
||||||
|
|
||||||
|
**与原有逻辑差异**:非 Markdown 行仍按段落(含换行 hardBreak)输出;纯文本行为与旧版「多段段落」接近,但连续单行不再强制 `\\n\\n` 才分段。
|
||||||
|
|
||||||
|
## 3. 调用方与影响范围
|
||||||
|
|
||||||
|
- **调用方**:`cmd_comment`、`cmd_create` 经 `plain_to_adf` → `markdown_to_adf`。
|
||||||
|
- **破坏性变更**:否。极端表格/复杂 MD 语法未实现,可能仍以普通文本行展示。
|
||||||
|
- **边界**:与 Jira Cloud ADF 一致;Server/DC 若 API 不同需单独验证。
|
||||||
|
|
||||||
|
## 4. 风险与回滚
|
||||||
|
|
||||||
|
- **风险级别**:低。若某 ADF 节点被实例拒绝,可回退 `plain_to_adf` 为旧实现。
|
||||||
|
- **回滚方式是否简单**:是(恢复旧 `plain_to_adf` 单函数)。
|
||||||
|
|
||||||
|
## 5. 验证与测试
|
||||||
|
|
||||||
|
- 本地 `markdown_to_adf` 样例 JSON 结构校验;`py_compile` 通过。
|
||||||
|
- 完整评论 POST 需对接真实 Jira。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 影响分析报告 — AI 执行流程防走偏(SKILL / workflow / checklist)
|
||||||
|
|
||||||
|
## 1. 改动概览
|
||||||
|
|
||||||
|
- **背景**:不同 AI 执行本技能时偶发跳步、先写 Jira 再看 diff、占位 Key 上传、误用 MCP 冒充附件等。
|
||||||
|
- **目标**:在文档层强制 **Step 1→5 线性顺序**、门禁表、红线与 Step 3 交互规则;新增逐步检查清单供 Agent 对照。
|
||||||
|
- **涉及模块**:`SKILL.md`、`references/workflow.md`、新增 `references/agent_execution_checklist.md`。
|
||||||
|
- **改动类型**:文档 / 流程约束增强(无脚本行为变更)。
|
||||||
|
|
||||||
|
## 2. 方法级改动分析
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `SKILL.md` | 前置「AI 执行契约」、各 Step「前置」说明、frontmatter description 强调线性流程。 |
|
||||||
|
| `workflow.md` | 目录增加「AI 执行顺序提醒」,链回 SKILL 与 checklist。 |
|
||||||
|
| `agent_execution_checklist.md`(新) | Step 0~5 打勾表 + 走偏速查。 |
|
||||||
|
|
||||||
|
**与原有逻辑差异**:仅约束 Agent 阅读与执行顺序;不改变 Python/API 行为。
|
||||||
|
|
||||||
|
## 3. 调用方与影响范围
|
||||||
|
|
||||||
|
- **调用方**:所有读取本技能的 Agent。
|
||||||
|
- **破坏性变更**:否。用户手动跑脚本不受影响。
|
||||||
|
|
||||||
|
## 4. 风险与回滚
|
||||||
|
|
||||||
|
- **风险级别**:低。文档过长可能略增 token;可精简 checklist。
|
||||||
|
- **回滚方式是否简单**:是(还原 SKILL/workflow、删除 checklist)。
|
||||||
|
|
||||||
|
## 5. 验证与测试
|
||||||
|
|
||||||
|
- 文档结构与人读一致性自检;无自动化测试。
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
---
|
||||||
|
name: g3fo-commit-jira
|
||||||
|
description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按 Step 1→5 线性执行**(见 SKILL 内「AI 执行契约」),每步完成后再进入下一步;强制 git 修订号校验与 HEAD 校验(amend 场景);Jira 附件仅用 upload_attachment.py。按任务类型生成文档并上传,评论影响分析,最后 amend 追加 Jira、禁止 push。详见 references/agent_execution_checklist.md。
|
||||||
|
---
|
||||||
|
|
||||||
|
# G3FO Commit Jira 技能
|
||||||
|
|
||||||
|
本技能通过 git 修订号自动分析改动,绑定/创建 Jira 任务,按**公司 Jira 工单附件文档政策**生成并上传所需文档(开发计划、影响分析、任务摘要),并更新 commit message。
|
||||||
|
|
||||||
|
> **公司文档政策**(自 2026-03-09 起):何时附加哪些文件见 `references/jira_commit_docs_policy.md`。
|
||||||
|
> 报告模板、任务类型判定与 JQL 参考 `references/workflow.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 执行契约(防走偏,**必读且优先于即兴发挥**)
|
||||||
|
|
||||||
|
### 1. 线性流程,禁止跳步
|
||||||
|
|
||||||
|
必须按 **Step 1 → Step 2 → Step 3 → Step 4 → Step 5** 顺序执行。**每完成一步**,在回复中用一句话标明 **`Step N 已完成`**,再进入下一步。禁止在未完成前置步骤时执行后续操作。
|
||||||
|
|
||||||
|
| 在未完成… | 禁止执行… |
|
||||||
|
|-----------|-----------|
|
||||||
|
| **Step 1**:`git rev-parse <hash>` 成功;若需 amend,已确认该 hash **就是当前 HEAD** | 调用 Jira、生成报告正文、落盘 `<KEY>_*.md`、`git commit --amend` |
|
||||||
|
| **Step 2**:已对照政策表写出**任务类型** + 三文档各是否需要(✅/❌) | 批量生成无关文档,或该写报告却跳过 |
|
||||||
|
| **Step 3**:已持有**真实** Jira Issue Key(用户给出 / 用户从列表选定 / 新建命令返回的 key) | 以真实路径上传附件、写绑定该 Issue 的评论(禁止用 `JIRA-XXX` 等占位 Key 落盘上传) |
|
||||||
|
| **Step 4**:本任务在 Jira 侧应做的上传/评论/建单已按政策做完 | `git commit --amend` |
|
||||||
|
|
||||||
|
### 2. 红线(违反即视为流程错误)
|
||||||
|
|
||||||
|
1. **禁止 `git push`**。本技能只做到 amend 为止。
|
||||||
|
2. **禁止跳过** `git rev-parse <hash>`;amend 场景下禁止在 **hash ≠ HEAD** 时仍执行 amend。
|
||||||
|
3. **禁止 amend 已推送的提交**(除非用户明确要求并确认风险)。使用 `git branch -r --contains <hash>` 检测。
|
||||||
|
4. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。
|
||||||
|
5. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。
|
||||||
|
6. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。
|
||||||
|
7. **禁止**在用户已提供 Jira 编号时仍执行搜索、展示列表或创建新 Issue。
|
||||||
|
|
||||||
|
### 3. Step 3 交互规则(易走偏)
|
||||||
|
|
||||||
|
- 用户**已给** Jira 编号 → **直接进入 Step 4**,Key = 用户给的编号,**禁止执行搜索、展示列表或创建新 Issue**。
|
||||||
|
- 用户**未给** → 搜索展示列表后,**必须等待用户输入 1~N 或 0**(或明确同意新建),**禁止**擅自替用户选一个 Issue 绑定。
|
||||||
|
- 搜索为空 → 可进入新建流程;新建成功后 Key 以 API 返回为准。
|
||||||
|
|
||||||
|
### 4. 自检
|
||||||
|
|
||||||
|
逐步执行时可对照 **`references/agent_execution_checklist.md`** 逐项确认。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置要求
|
||||||
|
|
||||||
|
**1. 强制提供 Git 修订号 (hash)**
|
||||||
|
|
||||||
|
用户必须提供一个有效的 git 修订号(如 `HEAD` 或具体的 `hash`)。
|
||||||
|
- **校验逻辑**:使用 `git rev-parse <hash>` 检查修订号是否存在。
|
||||||
|
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:"找不到修订号 `<hash>`,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。"
|
||||||
|
- **路径要求**:必须在相关的项目根目录下执行 git 命令。如果当前目录不是 git 仓库或不是目标项目,请提示用户切换到正确的项目路径。
|
||||||
|
- **已 push 提交检测**:使用 `git branch -r --contains <hash>` 检查提交是否已推送到远程。如果已推送,**默认跳过 Step 5(amend)**,仅完成 Jira 绑定和文档上传,并告知用户:
|
||||||
|
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
|
||||||
|
- **例外**:如果用户明确要求修改已 push 的提交(如通过 `--force` 或其他方式),需用户确认风险后再执行。
|
||||||
|
|
||||||
|
**2. Jira 编号 (可选)**
|
||||||
|
|
||||||
|
用户可以主动提供 Jira 编号(如 `G3SF-123`)。
|
||||||
|
- 如果提供了 Jira 编号,**直接使用该编号,跳过「查找/选择/新建 Jira 任务」的步骤**,进入 Step 4 上传文档和评论。
|
||||||
|
- 如果未提供,则按流程自动查找或提示用户新建。
|
||||||
|
|
||||||
|
**3. Jira 访问方式(二选一)**
|
||||||
|
|
||||||
|
| 方式 | 适用场景 |
|
||||||
|
|------|----------|
|
||||||
|
| **REST 脚本(推荐通用)** | 任意 IDE;仅需 `jira_upload.env`(与上传附件**同一套** Token)。查 Issue、新建 Task、写评论、上传附件**全部**可走脚本。 |
|
||||||
|
| **Atlassian MCP** | 仅 Cursor 等已安装并授权 Atlassian 插件的环境;可与脚本混用(附件仍必须用脚本)。 |
|
||||||
|
|
||||||
|
**无 MCP 时**:必须配置 `jira_upload.env`,并用 `scripts/jira_cli.py` 完成 Step 3/4 中的查询、创建与评论(见下文「无 MCP 执行要点」)。
|
||||||
|
|
||||||
|
**有 MCP 时**:可用 MCP 搜索/创建/评论;或仍用 `jira_cli.py`(行为一致,便于脚本化)。
|
||||||
|
|
||||||
|
**4. `jira_upload.env`(上传附件 + 无 MCP 时全部 Jira API)**
|
||||||
|
|
||||||
|
- 在技能目录 `skills/g3fo-commit-jira/` 放置 `jira_upload.env`(或 `--config` 指定),包含:`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`。示例见 `jira_upload.env.example`,说明见 `references/env_config.md`。
|
||||||
|
- **上传附件**:`scripts/upload_attachment.py`(MCP 不支持上传文件)。
|
||||||
|
- **无 MCP 时查 Jira / 建单 / 评论**:`scripts/jira_cli.py`,与上传使用**相同** Token,详见 `references/env_config.md` 与 `references/workflow.md`「无 MCP / jira_cli」。
|
||||||
|
- 若未配置:无法调用 Jira API;须提示用户创建 `jira_upload.env` 或在本机 Cursor 使用 MCP。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行步骤(严格顺序)
|
||||||
|
|
||||||
|
> **提醒**:仅当上一节「执行契约」中本步的前置条件已满足时,才执行本节对应步骤。
|
||||||
|
|
||||||
|
### Step 1 - 获取提交内容
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git show <hash> # 获取 diff + 元数据
|
||||||
|
git log -1 --format="%B" <hash> # 获取原始 commit message
|
||||||
|
git branch -r --contains <hash> # 检查是否已推送到远程(有输出则已 push)
|
||||||
|
```
|
||||||
|
|
||||||
|
分析要点:
|
||||||
|
- 涉及的服务模块/包名
|
||||||
|
- 新增/修改/删除的方法与接口
|
||||||
|
- 逻辑改动的核心目的
|
||||||
|
- **是否已推送**:如果 `git branch -r --contains <hash>` 有输出,说明已推送,Step 5 将跳过 amend
|
||||||
|
|
||||||
|
### Step 2 - 判定任务类型并确定需生成的文档
|
||||||
|
|
||||||
|
**前置**:Step 1 已完成。
|
||||||
|
根据改动内容与 `references/jira_commit_docs_policy.md` 中的**详细条件表**,先判定任务类型,再决定需要生成并上传的文档(须在回复中写明类型与三文档要/不要):
|
||||||
|
|
||||||
|
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|
||||||
|
|----------|:--------:|:--------:|:--------:|
|
||||||
|
| 漏洞修复——简单(如拼写、界面对齐) | ❌ | ❌ | ❌ |
|
||||||
|
| 漏洞修复——中等(范围有限的逻辑错误) | ❌ | ✅ | ❌ |
|
||||||
|
| 漏洞修复——复杂/高风险(并发、核心模块) | ✅ | ✅ | ✅ |
|
||||||
|
| 小幅度增强(如小效用方法) | ❌ | ✅ | ❌ |
|
||||||
|
| 新功能/模块 | ✅ | ✅ | ✅ |
|
||||||
|
| 主要重构 | ✅ | ✅ | ✅ |
|
||||||
|
| 配置/基础设施变更 | ❌ | ✅ | ❌ |
|
||||||
|
|
||||||
|
- **无需任何文档**时:仅执行 Step 3(确定 Jira)、Step 5(amend),不生成报告、不写评论、不上传附件;仍将 Jira 编号追加到 commit message。
|
||||||
|
- **需要文档**时:继续 Step 2 下半部分,生成对应内容。
|
||||||
|
|
||||||
|
### Step 2(续)- 生成所需报告内容
|
||||||
|
|
||||||
|
对上述判定为"需要"的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件):
|
||||||
|
|
||||||
|
- **开发计划**:步骤、设计方法、技术决策。
|
||||||
|
- **影响分析**:改动概览、方法级分析、影响范围、风险与回滚、验证与测试(模板见 workflow.md)。
|
||||||
|
- **任务摘要**:所完成工作的简要回顾,以及任何偏离计划之处。
|
||||||
|
|
||||||
|
生成后先不按"公司附件名"落盘,等取得 Jira 编号后再以 `<JIRA-ID>_*.md` 命名保存并上传。
|
||||||
|
|
||||||
|
**文档保存路径规则**:
|
||||||
|
- 所有生成的 MD 文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
|
||||||
|
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
|
||||||
|
- 若该文件夹不存在,**必须先创建**再保存文件
|
||||||
|
- 示例路径:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
|
||||||
|
|
||||||
|
### Step 3 - 确定 Jira 任务
|
||||||
|
|
||||||
|
**前置**:Step 2 已完成(至少已判定文档需求;若需文档,可先草稿内容,**真实 Key 确定后再按 `<KEY>_*.md` 保存**)。
|
||||||
|
|
||||||
|
**1. 如果用户已提供 Jira 编号:**
|
||||||
|
- **直接使用该编号**(如 `G3SF-123`),**跳过查找、选择和新建流程**,直接进入 Step 4。
|
||||||
|
- 不执行 JQL 搜索,不展示列表,不创建新 Issue。
|
||||||
|
|
||||||
|
**2. 如果用户未提供 Jira 编号:**
|
||||||
|
- 使用 JQL 查找当前用户在 G3SF 项目下的进行中任务(limit 5):
|
||||||
|
```
|
||||||
|
project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC
|
||||||
|
```
|
||||||
|
- **有 MCP**:用 MCP 的 JQL 搜索,展示列表。
|
||||||
|
- **无 MCP**:执行(PowerShell 用 `;` 分隔):
|
||||||
|
```powershell
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" search --limit 5
|
||||||
|
```
|
||||||
|
可加 `--format json` 供解析。展示列表供用户选择,或输入 `0` 新建。
|
||||||
|
- 如果搜索结果为空,自动进入新建流程。
|
||||||
|
|
||||||
|
### Step 4 - 写入 Jira(附件 + 评论)
|
||||||
|
|
||||||
|
**前置**:已持有本任务最终 **Issue Key**(Step 3)。
|
||||||
|
|
||||||
|
**附件命名规范(公司要求)**:上传到 Jira 的 MD 文件名必须为
|
||||||
|
`<JIRA-ID>_Dev_Plan.md`、`<JIRA-ID>_Impact_Analysis.md`、`<JIRA-ID>_Task_Summary.md`。
|
||||||
|
根据 Step 2 判定结果,仅上传“需要”的文档;每个文件先按该命名写入本地再上传(脚本以本地文件名为 Jira 附件名)。
|
||||||
|
|
||||||
|
**如果选择/新建的任务 ID > 0:**
|
||||||
|
|
||||||
|
1. **保存并上传附件**(已配置 `jira_upload.env` 且本任务需要文档时):
|
||||||
|
将 Step 2 生成的各文档内容,按公司规范命名写入项目 `doc/{日期}` 目录(日期格式 `YYYY-MM-DD`,若不存在则先创建),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 创建日期目录(若不存在)
|
||||||
|
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
|
||||||
|
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
|
||||||
|
|
||||||
|
# 示例:需要三份文档时,先写入 G3SF-123_Dev_Plan.md、G3SF-123_Impact_Analysis.md、G3SF-123_Task_Summary.md,再:
|
||||||
|
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "$dateFolder/G3SF-123_Impact_Analysis.md" --file "$dateFolder/G3SF-123_Dev_Plan.md" --file "$dateFolder/G3SF-123_Task_Summary.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传;有 MCP 时可仅写评论,无 MCP 则必须配置 env 才能完成评论。
|
||||||
|
2. **写评论**(有影响分析正文时):
|
||||||
|
- **MCP**:`jira_add_comment`,正文为影响分析报告(插件侧多为 Markdown 渲染)。
|
||||||
|
- **无 MCP**:`jira_cli.py comment/create` 会将常见 **Markdown**(`#`~`######` 标题、`-`/`1.` 列表、`**粗体**`、`` `代码` ``、代码块、`[链](url)` 等)转为 **Jira ADF**,在网页上与富文本一致;仍建议长文用 `--body-file`。将报告写入临时文件后:
|
||||||
|
```powershell
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" comment --issue "G3SF-123" --body-file "path\to\impact_body.md"
|
||||||
|
```
|
||||||
|
或使用 `--body "..."`(长文建议 `--body-file`)。
|
||||||
|
3. 记录该任务的 issue key。
|
||||||
|
|
||||||
|
**如果用户选择 0 或搜索无结果:**
|
||||||
|
|
||||||
|
- **MCP**:调用 `jira_create_issue`(见 `references/workflow.md`)。
|
||||||
|
- **无 MCP**:使用 `jira_cli.py create`(经办人默认为 Token 对应用户):
|
||||||
|
```powershell
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" create --summary "[G3SF] 提炼后的改动摘要" --description-file "path\to\impact.md"
|
||||||
|
```
|
||||||
|
脚本会打印新 issue key(如 `G3SF-123`);可加 `--format json` 解析 `key` 字段。
|
||||||
|
|
||||||
|
**新建 Issue 字段约定**(两种途径均需遵守):
|
||||||
|
|
||||||
|
- project: `G3SF`,issuetype: `Task`
|
||||||
|
- **Summary**:`[G3SF]` 前缀 + 一句话摘要
|
||||||
|
- **Description**:完整影响分析(Markdown 可先写入文件再用 `--description-file`)
|
||||||
|
- **Assignee**:当前用户(MCP 传 accountId;`jira_cli` 默认 `assignee` = API Token 对应账号)
|
||||||
|
|
||||||
|
- 若需要文档,创建成功后按公司命名上传对应 MD 到该 Issue
|
||||||
|
|
||||||
|
### Step 5 - 更新 Git 提交信息
|
||||||
|
|
||||||
|
**前置**:Step 4 已按政策完成(无需文档的绑定类任务可跳过上传/评论,但须已有 Key)。
|
||||||
|
|
||||||
|
**检查提交是否已推送**:
|
||||||
|
```powershell
|
||||||
|
git branch -r --contains <hash>
|
||||||
|
```
|
||||||
|
- 如果有输出,说明提交已推送到远程分支,**跳过 amend**,告知用户:
|
||||||
|
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
|
||||||
|
- 如果无输出,说明提交未推送,继续执行 amend。
|
||||||
|
|
||||||
|
**执行 amend(仅当未推送时)**:
|
||||||
|
|
||||||
|
使用获取到的 Jira 编号,通过 `git commit --amend` 更新 commit message。**严禁 push**。
|
||||||
|
|
||||||
|
**注意**:在 Windows PowerShell 环境下,需确保 UTF8 编码以防止中文乱码。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 设置 UTF8 编码
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
$origMsg = (git log -1 --format="%B").TrimEnd()
|
||||||
|
$jiraKey = "G3SF-123" # 实际获取到的 key
|
||||||
|
|
||||||
|
# 使用 --cleanup=verbatim 确保原始消息格式不被 git 自动修剪导致编码转换问题
|
||||||
|
git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim
|
||||||
|
```
|
||||||
|
|
||||||
|
完成后告知用户:
|
||||||
|
|
||||||
|
**未推送时**:
|
||||||
|
```
|
||||||
|
绑定成功:
|
||||||
|
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
|
||||||
|
Commit: <git log --oneline -1 的结果>
|
||||||
|
已追加 Jira 编号到 commit message
|
||||||
|
```
|
||||||
|
|
||||||
|
**已推送时**:
|
||||||
|
```
|
||||||
|
绑定完成(提交已推送,未修改 commit message):
|
||||||
|
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
|
||||||
|
Commit: <git log --oneline -1 的结果>
|
||||||
|
提示:提交已推送到远程,如需修改 commit message 请手动处理
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关参考
|
||||||
|
|
||||||
|
- **AI 逐步检查清单(防走偏)**:`references/agent_execution_checklist.md`
|
||||||
|
- **公司 Jira 附件文档政策**(何时附加哪些文件):`references/jira_commit_docs_policy.md`
|
||||||
|
- **报告模板、三文档说明与错误处理**:`references/workflow.md`
|
||||||
|
- **上传附件配置**:`references/env_config.md`;脚本 `scripts/upload_attachment.py`,依赖见 `scripts/requirements.txt`
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 任务总结 — g3fo-commit-jira 增强
|
||||||
|
|
||||||
|
## 任务信息
|
||||||
|
|
||||||
|
- **任务**:无 Atlassian MCP 时仍可通过同一 Jira Token 查 Jira、建单、评论。
|
||||||
|
- **范围**:`skills/g3fo-commit-jira`。
|
||||||
|
|
||||||
|
## 改动说明
|
||||||
|
|
||||||
|
- 新增 `scripts/jira_cli.py`(search / issue / create / comment / myself)。
|
||||||
|
- 新增 `scripts/jira_env.py`,`upload_attachment.py` 复用配置加载;支持在技能目录放置 `jira_upload.env`。
|
||||||
|
- 更新 `SKILL.md`、`references/workflow.md`、`references/env_config.md`。
|
||||||
|
|
||||||
|
## 影响与风险
|
||||||
|
|
||||||
|
- 无破坏性变更;风险低。详见 `IMPACT_ANALYSIS.md`。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
- 本地语法与 CLI `--help` 已验证;连通性依赖用户 `jira_upload.env`。
|
||||||
|
|
||||||
|
## 后续
|
||||||
|
|
||||||
|
- 若需同步到全局技能目录,可使用项目内 `skill-sync` 流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务总结 — AI 流程防走偏(文档增强)
|
||||||
|
|
||||||
|
- **目标**:减少 Agent 跳步、误上传、未等用户选 Issue 等偏离。
|
||||||
|
- **改动**:`SKILL.md` 增加「AI 执行契约」与各 Step 前置条件;新增 `references/agent_execution_checklist.md`;`workflow.md` 增加执行顺序提醒。
|
||||||
|
- **测试**:文档审阅;逻辑无代码变更。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Jira 上传附件配置示例
|
||||||
|
# 复制为 jira_upload.env 并填入真实值,勿提交 jira_upload.env 到 Git
|
||||||
|
|
||||||
|
JIRA_BASE_URL=https://你的站点.atlassian.net
|
||||||
|
JIRA_EMAIL=你的邮箱@company.com
|
||||||
|
JIRA_API_TOKEN=你的 API Token
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# AI 执行检查清单(g3fo-commit-jira)
|
||||||
|
|
||||||
|
执行本技能时建议 **边做边勾**( mentally 或写在回复里),避免跳步、顺序颠倒或误用工具。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开始前(Step 0)
|
||||||
|
|
||||||
|
- [ ] 用户已提供 **git 修订号**(如 `HEAD` 或完整/短 hash)
|
||||||
|
- [ ] 在 **目标业务仓库根目录** 执行 git(`git rev-parse --is-inside-work-tree` 为真)
|
||||||
|
- [ ] 已执行 `git rev-parse <hash>`,修订号存在
|
||||||
|
- [ ] 若要对提交做 amend:已确认 `git rev-parse HEAD` **等于** `git rev-parse <hash>`(否则停止,提示用户)
|
||||||
|
- [ ] 已检查 `git branch -r --contains <hash>`,确认提交是否已推送(已推送则跳过 amend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — 获取提交内容
|
||||||
|
|
||||||
|
- [ ] 已运行 `git show <hash>`(或等效)并理解改动范围
|
||||||
|
- [ ] 已记录原始 commit message(供 Step 5)
|
||||||
|
|
||||||
|
**未完成 Step 0~1 前:禁止** 写 Jira、生成报告、amend。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — 任务类型与文档
|
||||||
|
|
||||||
|
- [ ] 已阅读并对照 `references/jira_commit_docs_policy.md`(或 SKILL 中的条件表)
|
||||||
|
- [ ] 已明确写出:**任务类型** + **开发计划 / 影响分析 / 任务摘要** 各是否需要(✅/❌)
|
||||||
|
- [ ] 若需要文档:已按 `workflow.md` 模板准备内容(可先草稿,**真实 Jira Key 出来后再按名落盘**)
|
||||||
|
|
||||||
|
**禁止**:未判定类型就上传三份或一份都不写却写长评论(应与政策一致)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Jira Key
|
||||||
|
|
||||||
|
- [ ] 已有 **真实 Issue Key**(如 `G3SF-123`):来自用户直给、用户从列表选择、或 `create` 成功返回
|
||||||
|
- [ ] 若用户已提供 Jira 编号:**跳过搜索、展示列表和新建流程**,直接使用该编号
|
||||||
|
- [ ] 若走列表:已 **等用户选 1~N 或 0**,未擅自替用户绑定
|
||||||
|
|
||||||
|
**未取得真实 Key 前:禁止** 使用 `<JIRA-ID>_*.md` 落盘上传(禁止占位符 Key)。
|
||||||
|
**用户已提供 Jira 时:禁止** 执行搜索、展示列表或创建新 Issue。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — 写入 Jira
|
||||||
|
|
||||||
|
- [ ] 需要附件时:文件名为 `<KEY>_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md`(仅实际上传需要的)
|
||||||
|
- [ ] 文档保存路径:`doc/{日期}`(日期格式 `YYYY-MM-DD`),若目录不存在则先创建
|
||||||
|
- [ ] 附件 **仅** 通过 `scripts/upload_attachment.py` + `jira_upload.env`(不用 MCP 冒充上传)
|
||||||
|
- [ ] 需要影响分析进评论时:MCP `jira_add_comment` 或 `jira_cli.py comment`(与政策一致)
|
||||||
|
- [ ] 新建 Issue 时:Summary 带 `[G3SF]`,类型 Task,描述/评论与内容一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Git amend
|
||||||
|
|
||||||
|
- [ ] 已检查 `git branch -r --contains <hash>`:若已推送,**跳过 amend**,告知用户
|
||||||
|
- [ ] 若未推送:仅 `git commit --amend`,**未** `git push`
|
||||||
|
- [ ] Windows 已按 SKILL 处理 UTF-8 / `--cleanup=verbatim`(如适用)
|
||||||
|
- [ ] 若 message 已含 `**** Jira`,未重复追加
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 走偏速查
|
||||||
|
|
||||||
|
| 现象 | 纠正 |
|
||||||
|
|------|------|
|
||||||
|
| 先写 Jira 再去看 diff | 回到 Step 1 |
|
||||||
|
| 没有 Key 就上传 | 先完成 Step 3 |
|
||||||
|
| 用 MCP 上传 md | 改用 `upload_attachment.py` |
|
||||||
|
| 简单拼写改却写三份文档 | 重读政策表,按类型裁剪 |
|
||||||
|
| 执行了 push | 违反技能范围,后续勿再 push |
|
||||||
|
| amend 已推送的提交 | 先检查 `git branch -r --contains`,已推送则跳过 amend |
|
||||||
|
| 用户已给 Jira 却还搜索/新建 | 直接使用用户提供的编号,跳过 Step 3 的搜索流程 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**原则**:顺序 = 1 → 2 → 3 → 4 → 5;每步通过后再进入下一步;不确定时重读 `SKILL.md` 本节与 `workflow.md` 错误处理表。
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Jira 上传附件 — 配置文件说明
|
||||||
|
|
||||||
|
本技能通过脚本 `scripts/upload_attachment.py` 将影响分析报告 MD 文件上传到 Jira Issue 附件(Atlassian MCP 插件不支持上传文件)。脚本从**配置文件**或环境变量读取 Jira 认证与站点信息,环境变量可覆盖配置文件中的同名项。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件方式(推荐)
|
||||||
|
|
||||||
|
### 1. 创建配置文件
|
||||||
|
|
||||||
|
在技能目录 `skills/g3fo-commit-jira/` 下放置 `jira_upload.env`,或复制示例后改名并填写真实值:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 在技能目录下
|
||||||
|
copy jira_upload.env.example jira_upload.env
|
||||||
|
# 编辑 jira_upload.env,填入 JIRA_BASE_URL、JIRA_EMAIL、JIRA_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 文件格式
|
||||||
|
|
||||||
|
`jira_upload.env` 为 KEY=VALUE 格式,支持 `#` 注释:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Jira 站点 URL,不要带末尾 /
|
||||||
|
JIRA_BASE_URL=https://你的站点.atlassian.net
|
||||||
|
# 登录邮箱
|
||||||
|
JIRA_EMAIL=你的邮箱@company.com
|
||||||
|
# API Token(在 Atlassian 账户设置中创建)
|
||||||
|
JIRA_API_TOKEN=你的 API Token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 指定配置文件路径
|
||||||
|
|
||||||
|
从仓库根目录执行时,建议用 `--config` 指定技能目录下的配置文件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue G3SF-123 --file "G3SF-123_Impact_Analysis.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 说明 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `JIRA_BASE_URL` | Jira 站点 URL,不要带末尾 `/` | `https://your-domain.atlassian.net` |
|
||||||
|
| `JIRA_EMAIL` | 登录 Jira 的邮箱 | `you@company.com` |
|
||||||
|
| `JIRA_API_TOKEN` | Jira 账户的 API Token | 在 [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens) 创建 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 无 MCP:`jira_cli.py`(查 Jira / 建单 / 评论)
|
||||||
|
|
||||||
|
在 **未安装 Atlassian MCP** 的环境(其它 IDE、终端)中,使用与上传附件**相同**的 `jira_upload.env`,通过 `scripts/jira_cli.py` 调用 Jira REST API v3:
|
||||||
|
|
||||||
|
| 子命令 | 作用 |
|
||||||
|
|--------|------|
|
||||||
|
| `myself` | 当前用户 accountId / 邮箱 |
|
||||||
|
| `search` | JQL 搜索(默认:G3SF 进行中且指派给当前用户) |
|
||||||
|
| `issue <KEY>` | 查看单条 Issue |
|
||||||
|
| `create` | 创建 Issue(默认 G3SF + Task,经办人为 Token 用户) |
|
||||||
|
| `comment` | 添加评论 |
|
||||||
|
|
||||||
|
依赖:`pip install -r scripts/requirements.txt`(仅需 `requests`)。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 搜索(与技能 Step 3 默认 JQL 一致)
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" search --limit 5
|
||||||
|
|
||||||
|
# 新建(描述来自文件)
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" create --summary "[G3SF] 简述" --description-file "D:\tmp\impact.md"
|
||||||
|
|
||||||
|
# 评论
|
||||||
|
python skills/g3fo-commit-jira/scripts/jira_cli.py --config "skills/g3fo-commit-jira/jira_upload.env" comment --issue G3SF-123 --body-file "D:\tmp\comment.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
配置文件查找顺序:当前工作目录、`scripts/` 下、`skills/g3fo-commit-jira/` 下的 `jira_upload.env`。详细参数见 `references/workflow.md`「无 MCP:jira_cli 速查」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量覆盖
|
||||||
|
|
||||||
|
若同时存在配置文件和环境变量,**环境变量优先**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Token 获取
|
||||||
|
|
||||||
|
1. 打开 https://id.atlassian.com/manage-profile/security/api-tokens
|
||||||
|
2. 使用 Jira 登录邮箱登录
|
||||||
|
3. 点击 “Create API token”,命名后复制
|
||||||
|
4. Token 只显示一次,请妥善保存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全注意
|
||||||
|
|
||||||
|
- **不要将 `jira_upload.env` 提交到 Git**。技能目录下已通过 `.gitignore` 忽略。
|
||||||
|
- 不要将 `JIRA_API_TOKEN` 写入代码或提交到仓库。
|
||||||
|
- 定期在 Atlassian 账户中轮换 API Token。
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Jira 工单附件文档政策(公司强制性指南)
|
||||||
|
|
||||||
|
自 **2026年3月9日** 起,以下为将 **开发计划**、**影响分析**、**任务摘要** 附加到 Jira 工单的强制性指南,用于跟踪设计决策、评估风险、为未来参考提供背景及项目间协作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一般规则(经验法则)
|
||||||
|
|
||||||
|
| 场景 | 需要附加的文档 |
|
||||||
|
|------|----------------|
|
||||||
|
| **漏洞修复** | 仅包括 **影响分析**(若修复有非简单副作用) |
|
||||||
|
| **新功能 / 重大核心变更** | **全部三个文件**:开发计划、影响分析、任务摘要 |
|
||||||
|
| **琐碎 / 小改动** | 无需文件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细条件(按任务类型)
|
||||||
|
|
||||||
|
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|
||||||
|
|----------|:--------:|:--------:|:--------:|
|
||||||
|
| 漏洞修复——简单(如拼写错误、界面对齐) | ❌ | ❌ | ❌ |
|
||||||
|
| 漏洞修复——中等(如范围有限的逻辑错误) | ❌ | ✅ | ❌ |
|
||||||
|
| 漏洞修复——复杂/高风险(如并发、核心模块) | ✅ | ✅ | ✅ |
|
||||||
|
| 小幅度增强(如添加小效用方法) | ❌ | ✅ | ❌ |
|
||||||
|
| 新功能/模块 | ✅ | ✅ | ✅ |
|
||||||
|
| 主要重构 | ✅ | ✅ | ✅ |
|
||||||
|
| 配置/基础设施变更 | ❌ | ✅ | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文档含义说明
|
||||||
|
|
||||||
|
- **开发计划**:概述步骤、设计方法和技术决策。
|
||||||
|
- **影响分析**:列出受影响区域、风险、性能考虑因素及向后兼容性。
|
||||||
|
- **任务摘要**:简要回顾所完成的工作,包括任何偏离计划的地方。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 提交方式与命名规范
|
||||||
|
|
||||||
|
- 将文件(Markdown)**直接放在工单的附件部分**。
|
||||||
|
- 使用一致的命名规范:
|
||||||
|
- `<JIRA-ID>_Dev_Plan.md`
|
||||||
|
- `<JIRA-ID>_Impact_Analysis.md`
|
||||||
|
- `<JIRA-ID>_Task_Summary.md`
|
||||||
|
|
||||||
|
示例:工单 `G3SF-123` 的三个附件命名为
|
||||||
|
`G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.md`。
|
||||||
|
|
||||||
|
**本地保存路径**:
|
||||||
|
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
|
||||||
|
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
|
||||||
|
- 若该文件夹不存在,**必须先创建**再保存文件
|
||||||
|
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# G3FO Commit Jira — 详细工作流参考
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
0. [AI 执行顺序提醒](#ai-执行顺序提醒)
|
||||||
|
1. [公司 Jira 附件文档政策](#公司-jira-附件文档政策)
|
||||||
|
2. [Step 1: 获取并解析提交内容](#step-1-获取并解析提交内容)
|
||||||
|
3. [Step 2: 任务类型判定与三文档模板](#step-2-任务类型判定与三文档模板)
|
||||||
|
4. [Step 3: Jira 任务查找与确认](#step-3-jira-任务查找与确认)
|
||||||
|
5. [Step 4: Jira 字段映射与写入](#step-4-jira-字段映射与写入)
|
||||||
|
6. [Step 5: Git Amend 命令详解](#step-5-git-amend-命令详解)
|
||||||
|
7. [错误处理表](#错误处理表)
|
||||||
|
8. [Atlassian MCP 工具速查](#atlassian-mcp-工具速查)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 执行顺序提醒
|
||||||
|
|
||||||
|
与 **`SKILL.md` 中「AI 执行契约」** 一致:必须 **Step 1 → 2 → 3 → 4 → 5**,不可跳步。附件仅通过 **`upload_attachment.py`**;**禁止 push**。
|
||||||
|
逐步打勾请用 **`references/agent_execution_checklist.md`**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 公司 Jira 附件文档政策
|
||||||
|
|
||||||
|
自 **2026年3月9日** 起,何时附加「开发计划」「影响分析」「任务摘要」以公司强制性指南为准。
|
||||||
|
完整政策与详细条件表见 **`references/jira_commit_docs_policy.md`**。
|
||||||
|
|
||||||
|
- **附件命名规范**:上传到 Jira 的 MD 必须命名为
|
||||||
|
`<JIRA-ID>_Dev_Plan.md`、`<JIRA-ID>_Impact_Analysis.md`、`<JIRA-ID>_Task_Summary.md`
|
||||||
|
例如:`G3SF-123_Impact_Analysis.md`。
|
||||||
|
- 根据任务类型仅生成并上传“需要”的文档;无需文档的琐碎改动仍可绑定 Jira 并 amend,但不生成报告、不写评论、不上传附件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: 获取并解析提交内容
|
||||||
|
|
||||||
|
### 命令
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 检查 hash 是否为 HEAD
|
||||||
|
$head = git rev-parse HEAD
|
||||||
|
$provided = git rev-parse <hash>
|
||||||
|
if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" }
|
||||||
|
|
||||||
|
# 检查提交是否已推送到远程
|
||||||
|
git branch -r --contains <hash>
|
||||||
|
# 如果有输出,说明已推送,Step 5 将跳过 amend
|
||||||
|
|
||||||
|
# 获取完整 diff 和元数据
|
||||||
|
git show <hash>
|
||||||
|
|
||||||
|
# 仅获取原始 commit message(用于后续 amend 保留)
|
||||||
|
git log -1 --format="%B" <hash>
|
||||||
|
|
||||||
|
# 列出修改的文件(不含 diff 内容,快速预览)
|
||||||
|
git diff-tree --no-commit-id -r --name-status <hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解析要点
|
||||||
|
|
||||||
|
从 `git show` 输出中提取:
|
||||||
|
|
||||||
|
| 信息 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--- a/path` / `+++ b/path` | 修改的文件路径 |
|
||||||
|
| `@@ ... @@` 上下文 | 修改的类名、方法名 |
|
||||||
|
| `+` 行 | 新增代码(方法签名、接口、配置) |
|
||||||
|
| `-` 行 | 删除/替换代码 |
|
||||||
|
| 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) |
|
||||||
|
| `git branch -r --contains` 输出 | 是否已推送(有输出 = 已推送,跳过 amend) |
|
||||||
|
|
||||||
|
G3FO 服务包路径规律:
|
||||||
|
- `com.afe.g3fo.<service-short-name>.*` → 对应服务
|
||||||
|
- `controller` / `api` → REST 接口层
|
||||||
|
- `facade` / `facade.impl` → Dubbo 接口层
|
||||||
|
- `service` / `service.impl` → 业务逻辑层
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: 任务类型判定与三文档模板
|
||||||
|
|
||||||
|
### 2.1 按任务类型确定需生成的文档
|
||||||
|
|
||||||
|
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|
||||||
|
|----------|:--------:|:--------:|:--------:|
|
||||||
|
| 漏洞修复——简单(如拼写、界面对齐) | ❌ | ❌ | ❌ |
|
||||||
|
| 漏洞修复——中等(范围有限的逻辑错误) | ❌ | ✅ | ❌ |
|
||||||
|
| 漏洞修复——复杂/高风险(并发、核心模块) | ✅ | ✅ | ✅ |
|
||||||
|
| 小幅度增强(如小效用方法) | ❌ | ✅ | ❌ |
|
||||||
|
| 新功能/模块 | ✅ | ✅ | ✅ |
|
||||||
|
| 主要重构 | ✅ | ✅ | ✅ |
|
||||||
|
| 配置/基础设施变更 | ❌ | ✅ | ❌ |
|
||||||
|
|
||||||
|
生成内容后,在**取得 Jira 编号后**按公司规范保存为 `<JIRA-ID>_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md` 再上传。无需文档时跳过生成与上传,仅绑定 Jira 并 amend。
|
||||||
|
|
||||||
|
### 2.2 影响分析报告模板
|
||||||
|
|
||||||
|
生成影响分析时严格按以下结构输出(Markdown 格式):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 影响分析报告
|
||||||
|
|
||||||
|
### 1. 改动概览
|
||||||
|
- **背景与目标**:[说明为什么改、解决什么问题]
|
||||||
|
- **涉及模块**:[列举受影响的服务/模块,如 g3fo-trade-service、g3fo-margin-service]
|
||||||
|
- **改动类型**:功能新增 / 缺陷修复 / 重构 / 配置变更(选一或多个)
|
||||||
|
|
||||||
|
### 2. 方法级改动分析
|
||||||
|
|
||||||
|
| 文件/类 | 方法/接口 | 改动说明 | 行为是否变更 |
|
||||||
|
|---------|----------|---------|------------|
|
||||||
|
| XxxService.java | `calculateMargin()` | 新增空值校验 | 否(边界补强) |
|
||||||
|
|
||||||
|
### 3. 调用方与影响范围
|
||||||
|
|
||||||
|
| 调用方 | 文件路径 | 影响说明 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| XxxController | controller/XxxController.java | 入参校验增强,现有调用方兼容 |
|
||||||
|
|
||||||
|
- **破坏性变更**:是 / 否
|
||||||
|
- 如为"是",具体变更点:[列出方法签名、返回值、异常类型变化]
|
||||||
|
|
||||||
|
### 4. 风险与回滚
|
||||||
|
- **风险级别**:低 / 中 / 高
|
||||||
|
- **风险说明**:[说明原因]
|
||||||
|
- **回滚方式**:回退版本 / 关闭特性开关 / 回滚配置
|
||||||
|
- **回滚方式是否简单**:是 / 否
|
||||||
|
|
||||||
|
### 5. 验证与测试
|
||||||
|
- **已执行测试**:单测 / 集成测试 / 冒烟测试(选已执行的)
|
||||||
|
- **关键用例**:[简述关键场景及预期结果]
|
||||||
|
- **需线上观察**:[列出需关注的日志关键字、监控指标或告警项]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 如果 diff 信息不足以确认调用方,在报告中注明:「建议在代码库中搜索 `<方法名>` 确认所有调用方」。
|
||||||
|
|
||||||
|
### 2.3 开发计划模板(仅当任务类型要求时生成)
|
||||||
|
|
||||||
|
开发计划用于概述步骤、设计方法和技术决策。结构示例:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 开发计划
|
||||||
|
|
||||||
|
### 1. 目标与范围
|
||||||
|
- **目标**:[本任务要达成的结果]
|
||||||
|
- **范围**:[涉及模块、接口、配置等]
|
||||||
|
|
||||||
|
### 2. 步骤与设计方法
|
||||||
|
1. [步骤一:如需求分析、接口设计]
|
||||||
|
2. [步骤二:如实现方案、关键类/方法]
|
||||||
|
3. [步骤三:如测试与验证]
|
||||||
|
|
||||||
|
### 3. 技术决策
|
||||||
|
- [关键技术选型或实现方式及理由]
|
||||||
|
- [与现有架构的衔接方式]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 任务摘要模板(仅当任务类型要求时生成)
|
||||||
|
|
||||||
|
任务摘要用于简要回顾所完成的工作,包括任何偏离计划的地方。结构示例:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 任务摘要
|
||||||
|
|
||||||
|
### 1. 完成内容
|
||||||
|
- [已完成的主要改动与交付物]
|
||||||
|
|
||||||
|
### 2. 与计划差异(如有)
|
||||||
|
- [若实际实现与开发计划有偏差,在此说明原因与结果]
|
||||||
|
|
||||||
|
### 3. 后续建议(可选)
|
||||||
|
- [遗留事项、后续优化或文档更新建议]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Jira 任务查找与确认
|
||||||
|
|
||||||
|
### 用户已提供 Jira 编号
|
||||||
|
|
||||||
|
如果用户已提供 Jira 编号(如 `G3SF-123`):
|
||||||
|
- **直接使用该编号**,跳过 JQL 搜索、列表展示和新建流程
|
||||||
|
- **禁止**执行搜索、展示列表或创建新 Issue
|
||||||
|
- 直接进入 Step 4 上传文档和评论
|
||||||
|
|
||||||
|
### 用户未提供 Jira 编号
|
||||||
|
|
||||||
|
执行 JQL 查询:
|
||||||
|
|
||||||
|
### JQL 查询
|
||||||
|
|
||||||
|
```
|
||||||
|
project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC
|
||||||
|
```
|
||||||
|
|
||||||
|
- 限制返回 5 条,避免信息过载
|
||||||
|
- 若返回 0 条,直接告知用户并自动进入新建流程
|
||||||
|
|
||||||
|
### 展示格式
|
||||||
|
|
||||||
|
```
|
||||||
|
找到以下 G3SF 进行中任务,请输入编号绑定,或输入 0 新建:
|
||||||
|
|
||||||
|
1. G3SF-101 [修复保证金计算逻辑] 状态: In Progress
|
||||||
|
2. G3SF-98 [交易服务批量撤单性能优化] 状态: To Do
|
||||||
|
3. G3SF-95 [用户服务登录接口重构] 状态: In Progress
|
||||||
|
|
||||||
|
0. 新建一个 G3SF Task
|
||||||
|
|
||||||
|
请输入 (1/2/3/0):
|
||||||
|
```
|
||||||
|
|
||||||
|
### 确认规则
|
||||||
|
|
||||||
|
| 用户输入 | 行为 |
|
||||||
|
|---------|------|
|
||||||
|
| `1`~`N` | 使用对应任务,写入评论 |
|
||||||
|
| `0` | 新建 Jira Task |
|
||||||
|
| `n` / `no` | 等同于 `0`,新建任务 |
|
||||||
|
| 其他 | 重新提问一次 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Jira 字段映射与写入
|
||||||
|
|
||||||
|
### 新建 Issue 字段
|
||||||
|
|
||||||
|
调用 `jira_create_issue`(或鉴权后发现的等效工具),传入:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|------|----|
|
||||||
|
| `project` | `G3SF` |
|
||||||
|
| `issuetype` | `Task` |
|
||||||
|
| `summary` | **必须带 `[G3SF]` 前缀**,格式:`[G3SF] 提炼后的一句话描述`(≤100 字符) |
|
||||||
|
| `description` | 完整影响分析报告(Markdown / ADF 格式) |
|
||||||
|
| `assignee` | **必须为当前登录账号**(传入当前用户 accountId/name,确保新建任务经办人为自己) |
|
||||||
|
|
||||||
|
Summary 生成规则:
|
||||||
|
- **标题必须统一加前缀**:`[G3SF]` + 正文。例如:`[G3SF] [g3fo-trade-service] 修复批量撤单时的并发锁问题`
|
||||||
|
- 正文优先使用原始 commit message 的第一行(若描述清晰),否则根据修改内容生成:`[服务名] 功能描述`
|
||||||
|
|
||||||
|
新建 Issue 成功后,若已配置 `jira_upload.env` 且本任务需要文档,按公司命名将对应 MD 写入本地后上传到该新 Issue。
|
||||||
|
|
||||||
|
### 保存报告为 MD 并上传附件
|
||||||
|
|
||||||
|
1. **按公司规范命名并保存**:取得 Jira 编号(如 `G3SF-123`)后,将 Step 2 生成的各文档内容分别写入本地文件,文件名必须为:
|
||||||
|
- `<JIRA-ID>_Dev_Plan.md`
|
||||||
|
- `<JIRA-ID>_Impact_Analysis.md`
|
||||||
|
- `<JIRA-ID>_Task_Summary.md`
|
||||||
|
仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。
|
||||||
|
|
||||||
|
**保存路径规则**:
|
||||||
|
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
|
||||||
|
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
|
||||||
|
- 若该文件夹不存在,**必须先创建**再保存文件
|
||||||
|
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
|
||||||
|
|
||||||
|
2. **上传到 Jira 附件**:Atlassian MCP 不支持上传文件,使用本技能自带脚本上传上述 MD 到当前 Issue:
|
||||||
|
- 配置文件:在技能目录下配置 `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`),详见 `references/env_config.md`。
|
||||||
|
- 命令示例(PowerShell 用 `;` 连接,勿用 `&&`),按需上传多文件:
|
||||||
|
```powershell
|
||||||
|
# 创建日期目录(若不存在)
|
||||||
|
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
|
||||||
|
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
|
||||||
|
|
||||||
|
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "$dateFolder/G3SF-101_Impact_Analysis.md" --file "$dateFolder/G3SF-101_Dev_Plan.md" --file "$dateFolder/G3SF-101_Task_Summary.md"
|
||||||
|
```
|
||||||
|
- 未配置 `jira_upload.env` 时跳过上传,仅写评论。
|
||||||
|
|
||||||
|
### 写入已有 Issue(评论)
|
||||||
|
|
||||||
|
调用 `jira_add_comment`(或等效工具),字段:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|------|----|
|
||||||
|
| `issue_key` | 用户选择的 issue key,如 `G3SF-101` |
|
||||||
|
| `body` | 完整影响分析报告(与已保存的 MD 内容一致) |
|
||||||
|
|
||||||
|
### Jira 描述格式兼容
|
||||||
|
|
||||||
|
部分 Jira 实例使用 Atlassian Document Format (ADF) 而非纯 Markdown。若 MCP 工具提示格式错误,则:
|
||||||
|
- 将 Markdown 内容作为纯文本写入
|
||||||
|
- 或使用 `{code}` 块包裹(Jira Wiki 格式兼容)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Git Amend 命令详解
|
||||||
|
|
||||||
|
### 检查提交是否已推送
|
||||||
|
|
||||||
|
在执行 amend 前,必须检查提交是否已推送到远程:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 检查提交是否已推送
|
||||||
|
$pushedBranches = git branch -r --contains <hash>
|
||||||
|
if ($pushedBranches) {
|
||||||
|
Write-Host "提交已推送到远程,跳过 amend"
|
||||||
|
Write-Host "已完成 Jira 绑定和文档上传"
|
||||||
|
Write-Host "如需修改 commit message,请手动处理或提供特别说明"
|
||||||
|
# 跳过 amend,直接结束
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**已推送的处理**:
|
||||||
|
- 如果 `git branch -r --contains <hash>` 有输出,说明提交已推送到远程分支
|
||||||
|
- **跳过 amend**,仅完成 Jira 绑定和文档上传
|
||||||
|
- 告知用户提交已推送,无法 amend
|
||||||
|
- 例外:用户明确要求修改已推送的提交时,需确认风险后再执行
|
||||||
|
|
||||||
|
### PowerShell(Windows 环境)
|
||||||
|
|
||||||
|
在 Windows PowerShell 中,直接使用 `$origMsg` 可能会导致编码问题(乱码)。必须显式指定输出编码为 UTF8。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 设置控制台和输出编码为 UTF8,防止中文乱码
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# 获取原始 commit message(保留完整内容)
|
||||||
|
$origMsg = (git log -1 --format="%B").TrimEnd()
|
||||||
|
|
||||||
|
# Jira key(从上一步获取)
|
||||||
|
$jiraKey = "G3SF-123"
|
||||||
|
|
||||||
|
# 追加 Jira 行并 amend(不 push)
|
||||||
|
# 使用 --cleanup=verbatim 确保消息内容不被 git 自动修剪导致编码转换问题
|
||||||
|
git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bash/Linux(供参考)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
origMsg=$(git log -1 --format="%B" | sed 's/[[:space:]]*$//')
|
||||||
|
jiraKey="G3SF-123"
|
||||||
|
git commit --amend -m "$origMsg
|
||||||
|
|
||||||
|
**** Jira $jiraKey"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Amend 注意事项
|
||||||
|
|
||||||
|
- `--amend` 只修改 HEAD,不改变工作区内容
|
||||||
|
- 如果有 GPG 签名要求,告知用户需确认签名
|
||||||
|
- amend 后 commit hash 会变更,这是正常现象
|
||||||
|
- **禁止 push**:本 skill 不执行 `git push`,提交操作到 `amend` 为止
|
||||||
|
|
||||||
|
### 结果验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git log -1 --format="%B" # 确认 Jira 行已追加
|
||||||
|
git log --oneline -1 # 查看 commit 摘要
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 无 MCP:`jira_cli.py` 速查
|
||||||
|
|
||||||
|
与 `upload_attachment.py` **共用** `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`)。从仓库根执行时建议 `--config` 指向技能目录下的配置文件。
|
||||||
|
|
||||||
|
| 操作 | 命令示例 |
|
||||||
|
|------|----------|
|
||||||
|
| 当前用户(accountId) | `python .../jira_cli.py --config ".../jira_upload.env" myself` |
|
||||||
|
| 默认 JQL 搜索(G3SF 进行中、指派给我) | `python .../jira_cli.py --config "..." search --limit 5` |
|
||||||
|
| 自定义 JQL | `python .../jira_cli.py --config "..." search --jql "project = G3SF AND key = G3SF-1" --limit 10` |
|
||||||
|
| 机器可读 | 上述命令加 `--format json` |
|
||||||
|
| 单条 Issue | `python .../jira_cli.py --config "..." issue G3SF-123` |
|
||||||
|
| 新建 Task | `python .../jira_cli.py --config "..." create --summary "[G3SF] 标题" --description-file report.md` |
|
||||||
|
| 新建(内联描述) | `create --summary "..." --description "多段用\n\n分隔"` |
|
||||||
|
| 添加评论 | `python .../jira_cli.py --config "..." comment --issue G3SF-123 --body-file impact.md` |
|
||||||
|
| 不上传经办人 | `create ... --no-assign-self`(若站点禁止创建时指定经办人) |
|
||||||
|
|
||||||
|
**说明**:`create` 默认 `project=G3SF`、`issuetype=Task`;描述与评论由脚本将 **Markdown** 转为 Jira Cloud **ADF**(`#` 标题、列表、`**粗体**`、代码块等会按富文本展示,不再出现 `###` 原文)。上传 MD 附件仍用 `upload_attachment.py`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理表
|
||||||
|
|
||||||
|
| 错误场景 | 检测方式 | 处理方式 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 修订号不存在 | `git rev-parse <hash>` 报错 | **停止**,提示用户提供正确的修订号。 |
|
||||||
|
| 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse <hash>` | 停止,提示用户确认 hash(只有 HEAD 才能 amend)。 |
|
||||||
|
| 提交已推送到远程 | `git branch -r --contains <hash>` 有输出 | **跳过 amend**,仅完成 Jira 绑定和文档上传,告知用户提交已推送。 |
|
||||||
|
| 项目路径不正确 | `git rev-parse --is-inside-work-tree` 失败 | 提示用户在正确的 git 项目目录下操作。 |
|
||||||
|
| 无 MCP 且未配置 jira_upload.env | 无法执行 jira_cli / 上传 | **必须**配置 `jira_upload.env` 或改用 Cursor + MCP。 |
|
||||||
|
| Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,或改用 `jira_cli.py`。 |
|
||||||
|
| Atlassian 插件未安装 | `tools/` 目录为空或 MCP 调用失败 | 使用 `jira_cli.py` + `jira_upload.env`,或在 Cursor 安装 Atlassian 插件 |
|
||||||
|
| Jira 项目 G3SF 不存在 | 创建/搜索返回 404/400 | 提示用户确认 `JIRA_BASE_URL`、项目 key;`create` 可用 `--project` |
|
||||||
|
| 工作区有未提交变更 | `git status --porcelain` 有输出 | 警告:amend 会将未暂存变更排除在外,建议先 `git add` 或 stash |
|
||||||
|
| commit message 中已有 Jira 行 | message 中包含 `**** Jira` | 跳过追加,提示用户该提交已绑定 |
|
||||||
|
| 用户已提供 Jira 编号 | 用户输入包含 Jira key | **跳过搜索和新建**,直接使用用户提供的编号 |
|
||||||
|
| Jira 搜索无结果 | 工具返回空列表 | 直接进入新建流程,无需用户确认 |
|
||||||
|
| 未配置 jira_upload.env | 上传前检查配置不存在 | **有 MCP**:跳过上传,仅评论。**无 MCP**:无法完成 Jira 操作,须配置 env 或使用 MCP。 |
|
||||||
|
| 任务类型判定为「无需文档」 | 按政策表属简单漏洞/琐碎改动 | 不生成报告、不写评论、不上传附件;仍执行 Jira 绑定与 commit --amend,将 Jira 编号追加到 commit message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Atlassian MCP 工具速查
|
||||||
|
|
||||||
|
鉴权成功后,通过列举 `mcps/plugin-atlassian-atlassian/tools/` 确认可用工具。常见工具名:
|
||||||
|
|
||||||
|
| 功能 | 可能的工具名 |
|
||||||
|
|------|------------|
|
||||||
|
| 搜索 Issue | `jira_search_issues`, `jira_jql_search` |
|
||||||
|
| 创建 Issue | `jira_create_issue` |
|
||||||
|
| 获取 Issue 详情 | `jira_get_issue` |
|
||||||
|
| 添加评论 | `jira_add_comment` |
|
||||||
|
| 更新 Issue | `jira_update_issue` |
|
||||||
|
| 获取当前用户 | `jira_get_current_user` |
|
||||||
|
| 获取项目列表 | `jira_get_projects` |
|
||||||
|
| 上传附件 | MCP 不支持,使用 `scripts/upload_attachment.py` + `jira_upload.env` |
|
||||||
|
|
||||||
|
> 实际工具名以 `tools/` 目录下的 JSON 文件名为准,鉴权后务必先列目录确认。上传附件需使用本技能自带的 Python 脚本,见 `references/env_config.md`。
|
||||||
@@ -0,0 +1,673 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Jira REST API CLI — same credentials as upload_attachment.py (jira_upload.env).
|
||||||
|
Use when Atlassian MCP is unavailable (other IDEs).
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
myself Current user (accountId for assignee)
|
||||||
|
search JQL search
|
||||||
|
issue Get one issue by key
|
||||||
|
create Create Task (G3SF by default), assign to self
|
||||||
|
comment Add comment (ADF) to issue
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python jira_cli.py --config ../jira_upload.env search --limit 5
|
||||||
|
python jira_cli.py --config ../jira_upload.env issue G3SF-123
|
||||||
|
python jira_cli.py --config ../jira_upload.env create --summary "[G3SF] fix foo" --description-file report.md
|
||||||
|
python jira_cli.py --config ../jira_upload.env comment --issue G3SF-123 --body-file impact.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: pip install requests", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from jira_env import CONFIG_FILENAME, load_config, require_credentials, session_headers
|
||||||
|
|
||||||
|
DEFAULT_JQL = (
|
||||||
|
"project = G3SF AND assignee = currentUser() AND statusCategory != Done "
|
||||||
|
"ORDER BY updated DESC"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_doc() -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1,
|
||||||
|
"content": [{"type": "paragraph", "content": [{"type": "text", "text": " "}]}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _text_nodes(s: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Single plain text node (non-empty)."""
|
||||||
|
if not s:
|
||||||
|
return []
|
||||||
|
return [{"type": "text", "text": s}]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_inline_adf(s: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Parse inline **bold**, ~~strikethrough~~, `code`, *italic*, [text](url), ***bold italic***
|
||||||
|
into ADF text nodes with marks.
|
||||||
|
"""
|
||||||
|
if not s:
|
||||||
|
return []
|
||||||
|
# Order: links, bold+italic, bold, strikethrough, italic, code (non-greedy).
|
||||||
|
pattern = re.compile(
|
||||||
|
r"\[([^\]]+)\]\(([^)]+)\)" # [label](url)
|
||||||
|
r"|\*\*\*(.+?)\*\*\*" # ***bold italic***
|
||||||
|
r"|\*\*(.+?)\*\*" # **bold**
|
||||||
|
r"|~~(.+?)~~" # ~~strikethrough~~
|
||||||
|
r"|(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)" # *italic* (avoid matching **)
|
||||||
|
r"|(?<!`)`([^`\n]+)`(?!`)" # `code`
|
||||||
|
)
|
||||||
|
nodes: List[Dict[str, Any]] = []
|
||||||
|
last = 0
|
||||||
|
for m in pattern.finditer(s):
|
||||||
|
if m.start() > last:
|
||||||
|
chunk = s[last : m.start()]
|
||||||
|
if chunk:
|
||||||
|
nodes.append({"type": "text", "text": chunk})
|
||||||
|
if m.group(1) is not None and m.group(2) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": m.group(1),
|
||||||
|
"marks": [{"type": "link", "attrs": {"href": m.group(2).strip()}}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif m.group(3) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": m.group(3),
|
||||||
|
"marks": [{"type": "strong"}, {"type": "em"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif m.group(4) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{"type": "text", "text": m.group(4), "marks": [{"type": "strong"}]}
|
||||||
|
)
|
||||||
|
elif m.group(5) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{"type": "text", "text": m.group(5), "marks": [{"type": "strike"}]}
|
||||||
|
)
|
||||||
|
elif m.group(6) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{"type": "text", "text": m.group(6), "marks": [{"type": "em"}]}
|
||||||
|
)
|
||||||
|
elif m.group(7) is not None:
|
||||||
|
nodes.append(
|
||||||
|
{"type": "text", "text": m.group(7), "marks": [{"type": "code"}]}
|
||||||
|
)
|
||||||
|
last = m.end()
|
||||||
|
if last < len(s):
|
||||||
|
tail = s[last:]
|
||||||
|
if tail:
|
||||||
|
nodes.append({"type": "text", "text": tail})
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _split_table_row(line: str) -> List[str]:
|
||||||
|
"""Split a pipe-delimited table row into cells, stripping whitespace."""
|
||||||
|
parts = line.split("|")
|
||||||
|
cells = []
|
||||||
|
for p in parts:
|
||||||
|
c = p.strip()
|
||||||
|
if c:
|
||||||
|
cells.append(c)
|
||||||
|
return cells
|
||||||
|
|
||||||
|
|
||||||
|
def _is_separator_row(cells: List[str]) -> bool:
|
||||||
|
"""Check if a row is a GFM table separator (e.g., |---|---|)."""
|
||||||
|
return bool(cells) and all(re.fullmatch(r"-{3,}", c.strip()) for c in cells)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_table_row(line: str) -> bool:
|
||||||
|
"""Detect if a line looks like a GFM table row."""
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped.startswith("|"):
|
||||||
|
return False
|
||||||
|
cells = _split_table_row(stripped)
|
||||||
|
return len(cells) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_table(lines: List[str], start: int, n: int):
|
||||||
|
"""
|
||||||
|
Parse a GFM table starting at index start.
|
||||||
|
Returns (adf_table_dict, next_index).
|
||||||
|
"""
|
||||||
|
header_cells = _split_table_row(lines[start].strip())
|
||||||
|
num_cols = len(header_cells)
|
||||||
|
i = start + 1
|
||||||
|
|
||||||
|
# Skip separator row if present
|
||||||
|
if i < n and _is_separator_row(_split_table_row(lines[i].strip())):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
data_rows: List[List[str]] = []
|
||||||
|
while i < n:
|
||||||
|
s = lines[i].strip()
|
||||||
|
if not s or not _is_table_row(s):
|
||||||
|
break
|
||||||
|
row_cells = _split_table_row(s)
|
||||||
|
# Pad or trim to match header column count
|
||||||
|
while len(row_cells) < num_cols:
|
||||||
|
row_cells.append("")
|
||||||
|
data_rows.append(row_cells[:num_cols])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Build ADF table
|
||||||
|
def _make_cell(text: str, is_header: bool = False) -> Dict[str, Any]:
|
||||||
|
cell_type = "tableHeader" if is_header else "tableCell"
|
||||||
|
inline_nodes = parse_inline_adf(text) or _text_nodes(text or " ")
|
||||||
|
return {
|
||||||
|
"type": cell_type,
|
||||||
|
"content": [{"type": "paragraph", "content": inline_nodes}],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _make_row(cell_texts: List[str], is_header: bool = False) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [_make_cell(c, is_header=is_header) for c in cell_texts],
|
||||||
|
}
|
||||||
|
|
||||||
|
table_content: List[Dict[str, Any]] = [_make_row(header_cells, is_header=True)]
|
||||||
|
for dr in data_rows:
|
||||||
|
table_content.append(_make_row(dr))
|
||||||
|
|
||||||
|
return {"type": "table", "content": table_content}, i
|
||||||
|
|
||||||
|
|
||||||
|
def _paragraph_from_buffer(lines: List[str]) -> Optional[Dict[str, Any]]:
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
inner: List[Dict[str, Any]] = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if i > 0:
|
||||||
|
inner.append({"type": "hardBreak"})
|
||||||
|
inner.extend(parse_inline_adf(line) or _text_nodes(line))
|
||||||
|
if not inner:
|
||||||
|
return None
|
||||||
|
return {"type": "paragraph", "content": inner}
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_to_adf(text: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convert common Markdown to Atlassian Document Format (headings, lists, bold, code, links).
|
||||||
|
Jira REST API stores comments/descriptions as ADF; plain paragraphs showed ### literally.
|
||||||
|
"""
|
||||||
|
text = text or ""
|
||||||
|
if not text.strip():
|
||||||
|
return _empty_doc()
|
||||||
|
|
||||||
|
lines = text.split("\n")
|
||||||
|
content: List[Dict[str, Any]] = []
|
||||||
|
n = len(lines)
|
||||||
|
i = 0
|
||||||
|
para_buf: List[str] = []
|
||||||
|
|
||||||
|
def flush_paragraph() -> None:
|
||||||
|
nonlocal para_buf
|
||||||
|
if not para_buf:
|
||||||
|
return
|
||||||
|
p = _paragraph_from_buffer(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
if p:
|
||||||
|
content.append(p)
|
||||||
|
|
||||||
|
while i < n:
|
||||||
|
raw = lines[i]
|
||||||
|
stripped = raw.strip()
|
||||||
|
|
||||||
|
if not stripped:
|
||||||
|
flush_paragraph()
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ATX heading # .. ######
|
||||||
|
hm = re.match(r"^(#{1,6})\s+(.+)$", stripped)
|
||||||
|
if hm and len(hm.group(1)) <= 6:
|
||||||
|
flush_paragraph()
|
||||||
|
level = len(hm.group(1))
|
||||||
|
title = hm.group(2).strip()
|
||||||
|
title_nodes = parse_inline_adf(title) or _text_nodes(title)
|
||||||
|
content.append(
|
||||||
|
{"type": "heading", "attrs": {"level": level}, "content": title_nodes}
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Horizontal rule
|
||||||
|
if re.fullmatch(r"[-*_]{3,}", stripped):
|
||||||
|
flush_paragraph()
|
||||||
|
content.append({"type": "rule"})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# GFM Table: | col1 | col2 |
|
||||||
|
if _is_table_row(stripped):
|
||||||
|
flush_paragraph()
|
||||||
|
table_adf, i = _parse_table(lines, i, n)
|
||||||
|
content.append(table_adf)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fenced code block
|
||||||
|
if stripped.startswith("```"):
|
||||||
|
flush_paragraph()
|
||||||
|
lang = stripped[3:].strip() or "plaintext"
|
||||||
|
code_lines: List[str] = []
|
||||||
|
i += 1
|
||||||
|
while i < n:
|
||||||
|
if lines[i].strip().startswith("```"):
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
code_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
code_text = "\n".join(code_lines)
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "codeBlock",
|
||||||
|
"attrs": {"language": lang},
|
||||||
|
"content": [{"type": "text", "text": code_text or " "}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Bullet list (consecutive - or *)
|
||||||
|
if re.match(r"^[-*]\s+", stripped):
|
||||||
|
flush_paragraph()
|
||||||
|
items: List[str] = []
|
||||||
|
while i < n:
|
||||||
|
s = lines[i].strip()
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
bm = re.match(r"^[-*]\s+(.*)$", s)
|
||||||
|
if not bm:
|
||||||
|
break
|
||||||
|
items.append(bm.group(1))
|
||||||
|
i += 1
|
||||||
|
if items:
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "bulletList",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": parse_inline_adf(it)
|
||||||
|
or _text_nodes(it),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for it in items
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ordered list
|
||||||
|
om = re.match(r"^(\d+)\.\s+(.*)$", stripped)
|
||||||
|
if om:
|
||||||
|
flush_paragraph()
|
||||||
|
start_order = int(om.group(1))
|
||||||
|
items = [om.group(2)]
|
||||||
|
i += 1
|
||||||
|
while i < n:
|
||||||
|
s = lines[i].strip()
|
||||||
|
if not s:
|
||||||
|
break
|
||||||
|
m = re.match(r"^\d+\.\s+(.*)$", s)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
items.append(m.group(1))
|
||||||
|
i += 1
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "orderedList",
|
||||||
|
"attrs": {"order": start_order},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "listItem",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": parse_inline_adf(it)
|
||||||
|
or _text_nodes(it),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for it in items
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Blockquote: single line > text
|
||||||
|
if stripped.startswith("> "):
|
||||||
|
flush_paragraph()
|
||||||
|
quote_lines: List[str] = [stripped[2:].strip()]
|
||||||
|
i += 1
|
||||||
|
while i < n and lines[i].strip().startswith("> "):
|
||||||
|
quote_lines.append(lines[i].strip()[2:].strip())
|
||||||
|
i += 1
|
||||||
|
q_inner: List[Dict[str, Any]] = []
|
||||||
|
for j, ql in enumerate(quote_lines):
|
||||||
|
if j > 0:
|
||||||
|
q_inner.append({"type": "hardBreak"})
|
||||||
|
q_inner.extend(parse_inline_adf(ql) or _text_nodes(ql))
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "blockquote",
|
||||||
|
"content": [{"type": "paragraph", "content": q_inner}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
para_buf.append(raw)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
flush_paragraph()
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return _empty_doc()
|
||||||
|
return {"type": "doc", "version": 1, "content": content}
|
||||||
|
|
||||||
|
|
||||||
|
def plain_to_adf(text: str) -> Dict[str, Any]:
|
||||||
|
"""Backward-compatible name: Markdown-aware conversion for Jira ADF."""
|
||||||
|
return markdown_to_adf(text)
|
||||||
|
|
||||||
|
|
||||||
|
def unescape_text(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Unescape common escape sequences that may appear as literal strings
|
||||||
|
in CLI arguments or generated content.
|
||||||
|
Converts \\n -> newline, \\t -> tab, \\\\ -> backslash, etc.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
result = text.replace("\\\\", "\x00ESCAPED_BACKSLASH\x00")
|
||||||
|
result = result.replace("\\n", "\n")
|
||||||
|
result = result.replace("\\t", "\t")
|
||||||
|
result = result.replace("\\r", "\r")
|
||||||
|
result = result.replace("\x00ESCAPED_BACKSLASH\x00", "\\")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _out(data: Any, fmt: str) -> None:
|
||||||
|
if fmt == "json":
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_myself(base: str, email: str, token: str, fmt: str) -> int:
|
||||||
|
url = f"{base}/rest/api/3/myself"
|
||||||
|
r = requests.get(url, headers=session_headers(email, token, json_body=False), timeout=60)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
|
||||||
|
print(r.text[:800], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
j = r.json()
|
||||||
|
if fmt == "json":
|
||||||
|
_out(j, "json")
|
||||||
|
else:
|
||||||
|
print(f"accountId: {j.get('accountId')}")
|
||||||
|
print(f"displayName: {j.get('displayName')}")
|
||||||
|
print(f"email: {j.get('emailAddress')}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_search(base: str, email: str, token: str, jql: str, limit: int, fmt: str) -> int:
|
||||||
|
url = f"{base}/rest/api/3/search/jql"
|
||||||
|
payload = {
|
||||||
|
"jql": jql,
|
||||||
|
"maxResults": limit,
|
||||||
|
"fields": ["key", "summary", "status", "assignee", "updated"],
|
||||||
|
}
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
headers=session_headers(email, token),
|
||||||
|
json=payload,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
|
||||||
|
print(r.text[:800], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
data = r.json()
|
||||||
|
issues = data.get("issues") or []
|
||||||
|
if fmt == "json":
|
||||||
|
_out(data, "json")
|
||||||
|
return 0
|
||||||
|
if not issues:
|
||||||
|
print("(no issues)")
|
||||||
|
return 0
|
||||||
|
for i, iss in enumerate(issues, 1):
|
||||||
|
f = iss.get("fields") or {}
|
||||||
|
st = (f.get("status") or {}).get("name") or "?"
|
||||||
|
summ = (f.get("summary") or "")[:80]
|
||||||
|
print(f"{i}. {iss.get('key')} [{summ}] status: {st}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_issue(base: str, email: str, token: str, key: str, fmt: str) -> int:
|
||||||
|
url = f"{base}/rest/api/3/issue/{key}"
|
||||||
|
params = {"fields": "summary,status,assignee,project,description,created,updated"}
|
||||||
|
r = requests.get(
|
||||||
|
url,
|
||||||
|
headers=session_headers(email, token, json_body=False),
|
||||||
|
params=params,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
|
||||||
|
print(r.text[:800], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
j = r.json()
|
||||||
|
if fmt == "json":
|
||||||
|
_out(j, "json")
|
||||||
|
return 0
|
||||||
|
f = j.get("fields") or {}
|
||||||
|
print(f"key: {j.get('key')}")
|
||||||
|
print(f"summary: {f.get('summary')}")
|
||||||
|
print(f"status: {(f.get('status') or {}).get('name')}")
|
||||||
|
proj = f.get("project") or {}
|
||||||
|
print(f"project: {proj.get('key')} {proj.get('name')}")
|
||||||
|
asn = f.get("assignee")
|
||||||
|
print(f"assignee: {(asn or {}).get('displayName') or asn}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create(
|
||||||
|
base: str,
|
||||||
|
email: str,
|
||||||
|
token: str,
|
||||||
|
project: str,
|
||||||
|
summary: str,
|
||||||
|
description: str,
|
||||||
|
issuetype: str,
|
||||||
|
assign_self: bool,
|
||||||
|
fmt: str,
|
||||||
|
) -> int:
|
||||||
|
fields: Dict[str, Any] = {
|
||||||
|
"project": {"key": project},
|
||||||
|
"summary": summary.strip(),
|
||||||
|
"description": plain_to_adf(description),
|
||||||
|
"issuetype": {"name": issuetype},
|
||||||
|
}
|
||||||
|
if assign_self:
|
||||||
|
mr = requests.get(
|
||||||
|
f"{base}/rest/api/3/myself",
|
||||||
|
headers=session_headers(email, token, json_body=False),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if mr.status_code == 200:
|
||||||
|
aid = mr.json().get("accountId")
|
||||||
|
if aid:
|
||||||
|
fields["assignee"] = {"accountId": aid}
|
||||||
|
payload = {"fields": fields}
|
||||||
|
url = f"{base}/rest/api/3/issue"
|
||||||
|
r = requests.post(url, headers=session_headers(email, token), json=payload, timeout=60)
|
||||||
|
if r.status_code not in (200, 201):
|
||||||
|
# Retry without assignee if permission error
|
||||||
|
if r.status_code == 400 and assign_self and "assignee" in fields:
|
||||||
|
del fields["assignee"]
|
||||||
|
r2 = requests.post(
|
||||||
|
url, headers=session_headers(email, token), json={"fields": fields}, timeout=60
|
||||||
|
)
|
||||||
|
if r2.status_code in (200, 201):
|
||||||
|
key = r2.json().get("key")
|
||||||
|
print(f"WARN: created without assignee (API rejected assignee). key={key}", file=sys.stderr)
|
||||||
|
if fmt == "json":
|
||||||
|
_out(r2.json(), "json")
|
||||||
|
else:
|
||||||
|
print(key)
|
||||||
|
return 0
|
||||||
|
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
|
||||||
|
print(r.text[:1200], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
j = r.json()
|
||||||
|
if fmt == "json":
|
||||||
|
_out(j, "json")
|
||||||
|
else:
|
||||||
|
print(j.get("key", j))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_comment(base: str, email: str, token: str, issue: str, body: str, fmt: str) -> int:
|
||||||
|
url = f"{base}/rest/api/3/issue/{issue.strip()}/comment"
|
||||||
|
payload = {"body": plain_to_adf(body)}
|
||||||
|
r = requests.post(url, headers=session_headers(email, token), json=payload, timeout=60)
|
||||||
|
if r.status_code not in (200, 201):
|
||||||
|
print(f"ERROR: HTTP {r.status_code}", file=sys.stderr)
|
||||||
|
print(r.text[:800], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
j = r.json()
|
||||||
|
if fmt == "json":
|
||||||
|
_out(j, "json")
|
||||||
|
else:
|
||||||
|
print(f"OK comment id={j.get('id')}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def read_body(description: Optional[str], path: Optional[str]) -> str:
|
||||||
|
if path:
|
||||||
|
p = Path(path)
|
||||||
|
if not p.is_file():
|
||||||
|
raise FileNotFoundError(f"File not found: {p}")
|
||||||
|
return unescape_text(p.read_text(encoding="utf-8"))
|
||||||
|
return unescape_text(description or "")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Jira REST CLI (same token as upload_attachment)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default=None,
|
||||||
|
help=f"path to {CONFIG_FILENAME} (default: cwd / script dir / skill dir)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--format", choices=("text", "json"), default="text", dest="fmt")
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p_my = sub.add_parser("myself", help="GET /myself (accountId, email)")
|
||||||
|
p_my.set_defaults(func="myself")
|
||||||
|
|
||||||
|
p_se = sub.add_parser("search", help="JQL search")
|
||||||
|
p_se.add_argument("--jql", default=DEFAULT_JQL, help="JQL (default: G3SF in-progress for current user)")
|
||||||
|
p_se.add_argument("--limit", type=int, default=5)
|
||||||
|
p_se.set_defaults(func="search")
|
||||||
|
|
||||||
|
p_is = sub.add_parser("issue", help="Get issue by key")
|
||||||
|
p_is.add_argument("key", help="e.g. G3SF-123")
|
||||||
|
p_is.set_defaults(func="issue")
|
||||||
|
|
||||||
|
p_cr = sub.add_parser("create", help="Create issue (Task)")
|
||||||
|
p_cr.add_argument("--project", default="G3SF")
|
||||||
|
p_cr.add_argument("--summary", required=True)
|
||||||
|
p_cr.add_argument(
|
||||||
|
"--description",
|
||||||
|
default="",
|
||||||
|
help="Markdown → ADF (headings, lists, bold, code, links)",
|
||||||
|
)
|
||||||
|
p_cr.add_argument("--description-file", dest="description_file", default=None)
|
||||||
|
p_cr.add_argument("--issuetype", default="Task", dest="issuetype")
|
||||||
|
p_cr.add_argument(
|
||||||
|
"--no-assign-self",
|
||||||
|
action="store_true",
|
||||||
|
help="do not set assignee to API token user",
|
||||||
|
)
|
||||||
|
p_cr.set_defaults(func="create")
|
||||||
|
|
||||||
|
p_co = sub.add_parser("comment", help="Add comment to issue")
|
||||||
|
p_co.add_argument("--issue", required=True)
|
||||||
|
p_co.add_argument("--body", default="")
|
||||||
|
p_co.add_argument("--body-file", dest="body_file", default=None)
|
||||||
|
p_co.set_defaults(func="comment")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
|
try:
|
||||||
|
cfg = load_config(args.config, script_dir)
|
||||||
|
base, email, token = require_credentials(cfg)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fmt = args.fmt
|
||||||
|
if args.func == "myself":
|
||||||
|
return cmd_myself(base, email, token, fmt)
|
||||||
|
if args.func == "search":
|
||||||
|
return cmd_search(base, email, token, args.jql, args.limit, fmt)
|
||||||
|
if args.func == "issue":
|
||||||
|
return cmd_issue(base, email, token, args.key.strip(), fmt)
|
||||||
|
if args.func == "create":
|
||||||
|
try:
|
||||||
|
desc = read_body(args.description, args.description_file)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return cmd_create(
|
||||||
|
base,
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
args.project,
|
||||||
|
args.summary,
|
||||||
|
desc,
|
||||||
|
args.issuetype,
|
||||||
|
assign_self=not args.no_assign_self,
|
||||||
|
fmt=fmt,
|
||||||
|
)
|
||||||
|
if args.func == "comment":
|
||||||
|
try:
|
||||||
|
body = read_body(args.body, args.body_file)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if not body.strip():
|
||||||
|
print("ERROR: --body or --body-file required", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return cmd_comment(base, email, token, args.issue, body, fmt)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shared Jira config loading for g3fo-commit-jira scripts.
|
||||||
|
Uses the same jira_upload.env / env vars as upload_attachment.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
CONFIG_FILENAME = "jira_upload.env"
|
||||||
|
|
||||||
|
|
||||||
|
def find_config_file(explicit_path: Optional[str], script_dir: Path) -> Optional[Path]:
|
||||||
|
if explicit_path:
|
||||||
|
p = Path(explicit_path)
|
||||||
|
return p if p.exists() and p.is_file() else None
|
||||||
|
cwd_file = Path.cwd() / CONFIG_FILENAME
|
||||||
|
if cwd_file.exists():
|
||||||
|
return cwd_file
|
||||||
|
script_dir_file = script_dir / CONFIG_FILENAME
|
||||||
|
if script_dir_file.exists():
|
||||||
|
return script_dir_file
|
||||||
|
parent_skill = script_dir.parent / CONFIG_FILENAME
|
||||||
|
if parent_skill.exists():
|
||||||
|
return parent_skill
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path: Optional[str] = None, script_dir: Optional[Path] = None) -> Dict[str, str]:
|
||||||
|
"""Load KEY=VALUE from jira_upload.env. Env vars override file values."""
|
||||||
|
if script_dir is None:
|
||||||
|
script_dir = Path(__file__).resolve().parent
|
||||||
|
path = find_config_file(config_path, script_dir)
|
||||||
|
out: Dict[str, str] = {}
|
||||||
|
if path:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
k, _, v = line.partition("=")
|
||||||
|
out[k.strip()] = v.strip().strip('"').strip("'")
|
||||||
|
for key in ("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"):
|
||||||
|
if key in os.environ:
|
||||||
|
out[key] = os.environ[key]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def require_credentials(cfg: Dict[str, str]) -> tuple:
|
||||||
|
base_url = (cfg.get("JIRA_BASE_URL") or "https://n2nafe.atlassian.net").rstrip("/")
|
||||||
|
email = cfg.get("JIRA_EMAIL")
|
||||||
|
token = cfg.get("JIRA_API_TOKEN")
|
||||||
|
if not email or not token:
|
||||||
|
raise ValueError(
|
||||||
|
"Missing JIRA_EMAIL or JIRA_API_TOKEN. "
|
||||||
|
f"Please configure {CONFIG_FILENAME} file first. "
|
||||||
|
f"You can apply for API token at: https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||||
|
)
|
||||||
|
return base_url, email, token
|
||||||
|
|
||||||
|
|
||||||
|
def auth_header(email: str, token: str) -> str:
|
||||||
|
return base64.b64encode(f"{email}:{token}".encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def session_headers(email: str, token: str, json_body: bool = True) -> Dict[str, str]:
|
||||||
|
h = {
|
||||||
|
"Authorization": f"Basic {auth_header(email, token)}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if json_body:
|
||||||
|
h["Content-Type"] = "application/json"
|
||||||
|
return h
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
requests>=2.28.0
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Upload one or more files as attachments to a Jira Cloud issue via REST API v3.
|
||||||
|
|
||||||
|
Config: from jira_upload.env (current dir, script dir, or skill parent dir) or env vars.
|
||||||
|
Usage:
|
||||||
|
python upload_attachment.py --issue ISSUE_KEY --file path1 [--file path2 ...]
|
||||||
|
python upload_attachment.py --config /path/to/jira_upload.env --issue KEY --file path1
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from jira_env import CONFIG_FILENAME, auth_header, load_config, require_credentials
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Upload files as attachments to a Jira issue")
|
||||||
|
parser.add_argument("--issue", required=True, help="Jira issue key (e.g. G3SF-123)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--file",
|
||||||
|
action="append",
|
||||||
|
required=True,
|
||||||
|
dest="files",
|
||||||
|
help="Path to file to upload (can be repeated)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default=None,
|
||||||
|
help=f"Path to config file (default: {CONFIG_FILENAME} in cwd, script dir, or skill dir)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfg = load_config(args.config, SCRIPT_DIR)
|
||||||
|
base_url, email, token = require_credentials(cfg)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
print(f" Use --config PATH or create {CONFIG_FILENAME}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
issue_key = args.issue.strip()
|
||||||
|
files_to_upload = []
|
||||||
|
for p in args.files:
|
||||||
|
path = Path(p)
|
||||||
|
if not path.exists():
|
||||||
|
print(f"ERROR: File not found: {path}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if not path.is_file():
|
||||||
|
print(f"ERROR: Not a file: {path}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
files_to_upload.append(path)
|
||||||
|
|
||||||
|
url = f"{base_url}/rest/api/3/issue/{issue_key}/attachments"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Basic {auth_header(email, token)}",
|
||||||
|
"X-Atlassian-Token": "no-check",
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in files_to_upload:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
files = {"file": (path.name, f)}
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, headers=headers, files=files, timeout=60)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"ERROR: Request failed for {path.name}: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if resp.status_code not in (200, 201):
|
||||||
|
print(f"ERROR: Upload failed for {path.name}: HTTP {resp.status_code}", file=sys.stderr)
|
||||||
|
if resp.text:
|
||||||
|
print(resp.text[:500], file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
if isinstance(data, list) and data:
|
||||||
|
for att in data:
|
||||||
|
print(f"OK: {att.get('filename', path.name)} (id={att.get('id')}, size={att.get('size', '?')})")
|
||||||
|
else:
|
||||||
|
print(f"OK: {path.name}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"source": "local",
|
||||||
|
"sourceType": "local",
|
||||||
|
"installedAt": "2026-01-22T09:00:00.000Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
---
|
||||||
|
name: g3fo-db-ops
|
||||||
|
description: Comprehensive database operations guide for G3FO project. Use when updating G3FO database data, creating tables, modifying table structures, inserting system menus/routes, or managing database changes that require SQL generation, execution, and Git commit workflows. Includes automated workflows for i18n updates, version management, file naming conventions, and duplicate sequence number detection. Uses Python db_ops tool (not MCP MySQL) for database operations.
|
||||||
|
---
|
||||||
|
|
||||||
|
# G3FO Database Operations
|
||||||
|
|
||||||
|
Guide for performing database operations in the G3FO project, including automated workflows for data updates, table management, and SQL file versioning.
|
||||||
|
|
||||||
|
**Detailed documentation**: See `references/DATABASE_UPDATE_AUTOMATION_GUIDE.md` for complete automation guide.
|
||||||
|
|
||||||
|
## Database Tool (db_ops.py)
|
||||||
|
|
||||||
|
**本技能不再使用 MCP MySQL**,改用 Python 程序 `scripts/db_ops.py` 执行所有数据库操作。
|
||||||
|
|
||||||
|
### 安装依赖与运行前提
|
||||||
|
|
||||||
|
> **重要:本技能依赖本机可用的 Python 解释器。若未安装 Python 或 `python` 不在 PATH 中,在 Cursor 的 Shell 里直接执行脚本会出现 Windows `Exit code: 9009`(命令未找到)错误。**
|
||||||
|
|
||||||
|
1. 在本机安装 Python(3.8+),并确认命令行中可以直接运行:
|
||||||
|
```bash
|
||||||
|
python --version
|
||||||
|
```
|
||||||
|
如命令无效,请将 Python 安装目录加入系统 PATH,或使用 `py` / `python3` 等本机实际命令名。
|
||||||
|
|
||||||
|
2. 安装依赖:
|
||||||
|
```bash
|
||||||
|
pip install -r .claude/skills/g3fo-db-ops/scripts/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 默认连接配置
|
||||||
|
|
||||||
|
与 MCP user-mysql 一致,未指定时自动使用:
|
||||||
|
|
||||||
|
| 参数 | 默认值 |
|
||||||
|
|------|--------|
|
||||||
|
| host | 192.168.3.233 |
|
||||||
|
| port | 3306 |
|
||||||
|
| user | root |
|
||||||
|
| password | afe123456 |
|
||||||
|
| database | g3fo_base |
|
||||||
|
|
||||||
|
**切换数据库**:系统表用 `g3fo_base`,交易相关用 `g3fo_trade`。通过 `--database g3fo_trade` 或环境变量 `MYSQL_DATABASE` 覆盖。
|
||||||
|
|
||||||
|
### 覆盖连接方式
|
||||||
|
|
||||||
|
1. **命令行参数**:`--host`, `--port`, `--user`, `--password`, `--database`
|
||||||
|
2. **环境变量**:`MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`
|
||||||
|
|
||||||
|
### 命令列表
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `query` | 执行 SELECT 查询 |
|
||||||
|
| `execute` | 执行 INSERT/UPDATE/DELETE |
|
||||||
|
| `list_tables` | 列举数据库所有表 |
|
||||||
|
| `describe_table` | 获取表结构(列信息、表注释) |
|
||||||
|
| `call_procedure` | 执行存储过程 |
|
||||||
|
| `batch_execute` | 批量执行 SQL(支持文件、DELIMITER) |
|
||||||
|
| `get_table_comment` | 获取表注释 |
|
||||||
|
| `set_table_comment` | 修改表注释 |
|
||||||
|
| `get_column_comment` | 获取列注释 |
|
||||||
|
| `set_column_comment` | 修改列注释 |
|
||||||
|
| `test_connection` | 测试连接 |
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 skill 根目录或 server 根目录执行,base_dir 为 .claude/skills/g3fo-db-ops 所在路径
|
||||||
|
|
||||||
|
# 测试连接
|
||||||
|
python scripts/db_ops.py test_connection
|
||||||
|
python scripts/db_ops.py --database g3fo_trade test_connection
|
||||||
|
|
||||||
|
# 查询
|
||||||
|
python scripts/db_ops.py query "SELECT * FROM m_system_code LIMIT 5"
|
||||||
|
|
||||||
|
# 执行增删改
|
||||||
|
python scripts/db_ops.py execute "REPLACE INTO g3fo_base.m_system_code (...) VALUES (...)"
|
||||||
|
|
||||||
|
# 列举所有表
|
||||||
|
python scripts/db_ops.py list_tables
|
||||||
|
python scripts/db_ops.py --database g3fo_trade list_tables
|
||||||
|
|
||||||
|
# 获取表结构
|
||||||
|
python scripts/db_ops.py describe_table m_system_code
|
||||||
|
|
||||||
|
# 执行存储过程
|
||||||
|
python scripts/db_ops.py call_procedure InsertSystemMenu --params "菜单备注" "menu-name" "title_remark" "ADMIN"
|
||||||
|
|
||||||
|
# 批量执行 SQL 文件
|
||||||
|
python scripts/db_ops.py batch_execute --file path/to/script.sql
|
||||||
|
|
||||||
|
# 修改表注释
|
||||||
|
python scripts/db_ops.py set_table_comment m_system_code "系统编码表"
|
||||||
|
```
|
||||||
|
|
||||||
|
**在 Cursor 中调用时**:使用 `run_terminal_cmd` 或 Shell 工具执行上述命令,工作目录为 `d:\AFE Git\G3SF\G3FO\server`,脚本路径为 `.claude/skills/g3fo-db-ops/scripts/db_ops.py`。
|
||||||
|
|
||||||
|
## Windows 执行规范(必读)
|
||||||
|
|
||||||
|
### Python 命令优先级(Windows)
|
||||||
|
|
||||||
|
- **优先使用**:`py`
|
||||||
|
- **备选使用**:`python`
|
||||||
|
- **要求**:在 Windows 上执行本 skill 的所有命令时,先尝试 `py`,失败再尝试 `python`(不要反过来)。
|
||||||
|
|
||||||
|
### 控制台编码(避免 UnicodeEncodeError)
|
||||||
|
|
||||||
|
在 Windows/PowerShell 中执行 `db_ops.py` 时,如果输出包含中文,可能因控制台默认 GBK 编码导致 `UnicodeEncodeError`。执行前请统一设置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PowerShell
|
||||||
|
$env:PYTHONIOENCODING='utf-8'; py .claude/skills/g3fo-db-ops/scripts/db_ops.py test_connection
|
||||||
|
```
|
||||||
|
|
||||||
|
## 变更执行流程(双确认,强制)
|
||||||
|
|
||||||
|
1. **生成 SQL 文件**(按版本目录与序号规则落盘)
|
||||||
|
2. **展示 SQL 内容并等待用户确认**(用户确认 `yes` 后才允许执行)
|
||||||
|
3. **执行 SQL**(`batch_execute --file` / `execute`),确认返回 `"success": true`
|
||||||
|
4. **再次询问用户是否提交 Git**(用户确认后才允许 `git pull/add/commit/push`)
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. **Use existing templates**: Prioritize templates and tools in `g3fo-db/common_sql/`
|
||||||
|
2. **Update i18n simultaneously**: When updating tables requiring multilingual support, must also update `m_system_i18n` table
|
||||||
|
3. **Automated workflow**: Follow standardized automation including SQL generation, execution, and Git commit
|
||||||
|
|
||||||
|
## Tables Requiring i18n Updates
|
||||||
|
|
||||||
|
When inserting or updating data in these tables, must simultaneously update `m_system_i18n`:
|
||||||
|
|
||||||
|
| Table | i18n_id Format | Example |
|
||||||
|
|-------|---------------|---------|
|
||||||
|
| `m_system_code` | `CODE-{4位数字ID}` | `CODE-0404` |
|
||||||
|
| `m_system_error_message` | `ERR-{error_code}` | `ERR-17100` |
|
||||||
|
| `m_market` | `MKT-{4位数字ID}` | `MKT-0001` |
|
||||||
|
| `m_currency` | `CURR-{currency_code}` | `CURR-USD` |
|
||||||
|
| `m_channel` | `CH-{channel_code}` | `CH-001` |
|
||||||
|
| `m_exchange` | `EXCH-{exchange_code}` | `EXCH-HKEX` |
|
||||||
|
| `m_country` | `CNTY-{country_code}` | `CNTY-HK` |
|
||||||
|
| `m_exchange_charge` | `EX_CHARGE-{id}` | `EX_CHARGE-1` |
|
||||||
|
| `m_company_charge` | `COM_CHARGE-{id}` | `COM_CHARGE-1` |
|
||||||
|
|
||||||
|
## Automated Workflow
|
||||||
|
|
||||||
|
### 1. Data Input Phase
|
||||||
|
|
||||||
|
Request user input based on table type. For `m_system_code`:
|
||||||
|
- Code_type: [编码类型]
|
||||||
|
- Code_value: [编码值]
|
||||||
|
- Remark: [备注(用于生成多语言描述)]
|
||||||
|
- Sort: [排序号]
|
||||||
|
|
||||||
|
### 2. SQL Generation Phase
|
||||||
|
|
||||||
|
1. **Get max ID** (if applicable):
|
||||||
|
```sql
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 FROM {schema}.{table_name};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate i18n_id**: Use format rules based on table type
|
||||||
|
|
||||||
|
3. **Generate multilingual descriptions**:
|
||||||
|
- Extract or generate English, Simplified Chinese, Traditional Chinese from remark
|
||||||
|
- Vietnamese and French optional
|
||||||
|
|
||||||
|
4. **Generate complete SQL**:
|
||||||
|
- Main table INSERT/UPDATE
|
||||||
|
- m_system_i18n INSERT/UPDATE (if applicable)
|
||||||
|
|
||||||
|
### 3. User Confirmation Phase
|
||||||
|
|
||||||
|
Display generated SQL and ask:
|
||||||
|
```
|
||||||
|
已生成以下 SQL 语句,请确认数据是否正确:
|
||||||
|
|
||||||
|
[显示SQL语句]
|
||||||
|
|
||||||
|
确认数据无误,是否继续下一步?(yes/no)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. SQL Execution Phase
|
||||||
|
|
||||||
|
If user confirms:
|
||||||
|
1. Use **db_ops.py** to execute SQL (run in terminal):
|
||||||
|
- **Default connection** (see "Database Tool" section above): host 192.168.3.233, user root, password afe123456
|
||||||
|
- System tables: `--database g3fo_base`
|
||||||
|
- Trade tables: `--database g3fo_trade`
|
||||||
|
- Override via `--host`, `--port`, `--user`, `--password`, `--database` if user provides different connection info
|
||||||
|
2. For single SELECT: `python .claude/skills/g3fo-db-ops/scripts/db_ops.py query "SQL"`
|
||||||
|
3. For INSERT/UPDATE/DELETE: `python .claude/skills/g3fo-db-ops/scripts/db_ops.py execute "SQL"`
|
||||||
|
4. For batch SQL or file: `python .claude/skills/g3fo-db-ops/scripts/db_ops.py batch_execute --file path/to/file.sql`
|
||||||
|
5. Verify results (check JSON output for `"success": true`)
|
||||||
|
6. If failed, report error and stop
|
||||||
|
|
||||||
|
### 5. Git Commit Phase
|
||||||
|
|
||||||
|
After successful execution, ask:
|
||||||
|
```
|
||||||
|
SQL 执行成功!
|
||||||
|
|
||||||
|
是否需要将 SQL 提交到待执行文件夹中?(yes/no)
|
||||||
|
```
|
||||||
|
|
||||||
|
If confirmed, **execute in this order** to ensure remote repository and local data consistency:
|
||||||
|
|
||||||
|
1. **Switch to g3fo-db directory and pull latest code** (must execute first):
|
||||||
|
```bash
|
||||||
|
cd g3fo-db
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
- This step ensures getting latest file list and changes from remote repository
|
||||||
|
- **If pull fails** (e.g., conflicts), report error and stop workflow
|
||||||
|
|
||||||
|
2. **Determine target folder** (based on latest pulled code):
|
||||||
|
- Find latest "pending release {version}" folder in `update_service_release`
|
||||||
|
- **If not exists, create new folder**:
|
||||||
|
- View all version number folders in `update_service_release` (e.g., `1.5.1.0`, `1.5.1.1`, `1.5.1.17`, etc.)
|
||||||
|
- Find maximum version number
|
||||||
|
- New version = max version + 0.0.0.1 (increment last digit)
|
||||||
|
- Create folder: `pending release {新版本号}`
|
||||||
|
- **Auto-create `set_version_01.sql`**:
|
||||||
|
```sql
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '1.5.1.0 M{版本号最后两位数字}',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.systemVersion';
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '0',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.patchNo';
|
||||||
|
```
|
||||||
|
- **Version mapping rules**:
|
||||||
|
- Extract last digit N from version number (format X.Y.Z.N)
|
||||||
|
- Format as `'1.5.1.0 M{N}'`
|
||||||
|
- Examples:
|
||||||
|
- `1.5.1.17` → extract `17` → `'1.5.1.0 M17'`
|
||||||
|
- `1.5.1.18` → extract `18` → `'1.5.1.0 M18'`
|
||||||
|
- `1.5.1.10` → extract `10` → `'1.5.1.0 M10'`
|
||||||
|
- `1.5.1.9` → extract `9` → `'1.5.1.0 M9'` (no zero padding)
|
||||||
|
- **Note**: Folder list is now latest, avoiding filename conflicts
|
||||||
|
|
||||||
|
3. **Generate filename** (based on latest file list):
|
||||||
|
- Format: `{功能描述英文}_{序号}.sql`
|
||||||
|
- Sequence number rules:
|
||||||
|
- View **all** `.sql` files in target folder (after git pull)
|
||||||
|
- **Calculate sequence number across all files uniformly**, regardless of filename prefix
|
||||||
|
- **Extract sequence number method**:
|
||||||
|
1. Traverse all `.sql` files in folder
|
||||||
|
2. Extract two-digit number after underscore from each filename (format: `_XX.sql`)
|
||||||
|
3. Example: `set_version_01.sql` → extract `01`, `add_system_code_02.sql` → extract `02`
|
||||||
|
4. Find **maximum** sequence number among all
|
||||||
|
5. New sequence = max + 1
|
||||||
|
- Use two-digit format (01-99)
|
||||||
|
- **Example**:
|
||||||
|
- If folder has: `set_version_01.sql` and `add_system_code_02.sql`
|
||||||
|
- Extracted sequences: `01`, `02`
|
||||||
|
- Max sequence: `02`
|
||||||
|
- New file sequence: `03`
|
||||||
|
- New filename: `add_new_feature_03.sql`
|
||||||
|
- **Important**: Must generate after git pull to avoid conflicts
|
||||||
|
|
||||||
|
4. **Detect and fix duplicate sequence numbers** (before saving):
|
||||||
|
- Scan all `.sql` files in target folder
|
||||||
|
- Detect duplicates
|
||||||
|
- If found, follow "Duplicate Sequence Detection and Auto-Fix" rules
|
||||||
|
- Ensure all file sequences are unique and continuous
|
||||||
|
|
||||||
|
5. **Save SQL file** to target folder
|
||||||
|
|
||||||
|
6. **Git commit and push**:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: {简短描述}"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
**If push fails** (remote has new commits):
|
||||||
|
- Execute `git pull origin main` again
|
||||||
|
- Resolve conflicts and push again
|
||||||
|
|
||||||
|
### 6. Completion Message
|
||||||
|
|
||||||
|
```
|
||||||
|
操作完成!
|
||||||
|
|
||||||
|
已执行的操作:
|
||||||
|
✓ SQL 语句已执行
|
||||||
|
✓ SQL 文件已保存到: update_service_release/pending release {version}/{filename}
|
||||||
|
✓ 已推送到 Git 仓库
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Existing Templates
|
||||||
|
|
||||||
|
### Insert System Menu and Route Permissions
|
||||||
|
|
||||||
|
**Important: Adding menus and route permissions must use stored procedure files from `common_sql` folder.**
|
||||||
|
|
||||||
|
**Files to use:**
|
||||||
|
- `references/insert_system_menu_procedure.sql` - Insert system menu
|
||||||
|
- `references/insert_system_route_procedure.sql` - Insert system route
|
||||||
|
|
||||||
|
**Execution and commit rules:**
|
||||||
|
|
||||||
|
1. **SQL execution phase**:
|
||||||
|
- Use **db_ops.py** `batch_execute --file` for SQL files with `DELIMITER` (script handles it)
|
||||||
|
- Or use `call_procedure` for stored procedure calls
|
||||||
|
- Or use `execute` with direct `INSERT INTO` / `REPLACE INTO` if data matches procedure output
|
||||||
|
|
||||||
|
2. **Git commit phase**:
|
||||||
|
- **Must use complete SQL from stored procedure file**
|
||||||
|
- Copy full stored procedure definition from `common_sql` folder
|
||||||
|
- Add `CALL` statement at end to invoke procedure
|
||||||
|
- Reference format in `update_service_release/pending release 1.5.1.18`
|
||||||
|
|
||||||
|
**Insert system menu example:**
|
||||||
|
```sql
|
||||||
|
-- Copy complete stored procedure definition from insert_system_menu_procedure.sql
|
||||||
|
-- ... (complete stored procedure code) ...
|
||||||
|
|
||||||
|
-- Add CALL statement at end
|
||||||
|
CALL InsertSystemMenu('菜单备注', 'menu-name', 'title_remark', 'ADMIN', NULL, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Insert system route example:**
|
||||||
|
```sql
|
||||||
|
-- Copy complete stored procedure definition from insert_system_route_procedure.sql
|
||||||
|
-- ... (complete stored procedure code) ...
|
||||||
|
|
||||||
|
-- Add CALL statement at end
|
||||||
|
CALL InsertSystemRoute('备注', '/route/path', '标题备注', 'ADMIN'|'USER');
|
||||||
|
```
|
||||||
|
|
||||||
|
**File naming:**
|
||||||
|
- Menu: `insert_system_menu_procedure_{序号}.sql`
|
||||||
|
- Route: `insert_system_route_procedure_{序号}.sql`
|
||||||
|
|
||||||
|
### Create New Table
|
||||||
|
|
||||||
|
Reference `references/create_table_template.sql` format:
|
||||||
|
- Use `CREATE TABLE IF NOT EXISTS`
|
||||||
|
- Specify correct schema (e.g., `g3fo_trade`, `g3fo_base`)
|
||||||
|
- Include necessary fields, indexes, comments
|
||||||
|
- Use `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`
|
||||||
|
|
||||||
|
### Table Structure Changes
|
||||||
|
|
||||||
|
Reference `references/table_change_template.sql` method:
|
||||||
|
- Use stored procedure to check if column exists
|
||||||
|
- Use `IF EXISTS` for idempotency checks
|
||||||
|
- Delete procedure after execution
|
||||||
|
|
||||||
|
#### 列新增/DDL(强制使用存储过程模板)
|
||||||
|
|
||||||
|
所有涉及 `ALTER TABLE` 的结构变更(新增列/修改列/删除列等),一律按 `references/table_change_template.sql` 的模式执行:
|
||||||
|
|
||||||
|
- `USE <schema>;`
|
||||||
|
- `DROP PROCEDURE IF EXISTS <ProcName>;`
|
||||||
|
- `CREATE PROCEDURE <ProcName>() BEGIN ... END;`
|
||||||
|
- 在过程内通过 `information_schema.columns` 做存在性判断后再执行 `ALTER TABLE`
|
||||||
|
- `CALL <ProcName>();`
|
||||||
|
- `DROP PROCEDURE IF EXISTS <ProcName>;`
|
||||||
|
|
||||||
|
### Order Table Field Synchronization Rule
|
||||||
|
|
||||||
|
**Important: When `m_order` table adds a field, must simultaneously add same field to:**
|
||||||
|
|
||||||
|
1. `m_order_actions` - Order actions table
|
||||||
|
2. `h_order` - Order history table
|
||||||
|
3. `h_order_actions` - Order actions history table
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Field name must be identical
|
||||||
|
- Field type must be identical
|
||||||
|
- Field constraints (NULL/NOT NULL, DEFAULT, etc.) must be identical
|
||||||
|
- Field comment must be identical
|
||||||
|
- Field position (AFTER clause) should be consistent
|
||||||
|
|
||||||
|
**SQL generation requirements:**
|
||||||
|
- Use stored procedure for idempotency checks (reference `table_change_template.sql`)
|
||||||
|
- Check field existence for each table separately, add if not exists
|
||||||
|
- Ensure all four tables' field changes in same SQL file
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
If need to add field `new_field VARCHAR(100) COMMENT '新字段'` to `m_order` table, must simultaneously add same field to `m_order_actions`, `h_order`, `h_order_actions` tables.
|
||||||
|
|
||||||
|
## SQL Statement Standards
|
||||||
|
|
||||||
|
1. **Use REPLACE INTO** instead of INSERT INTO for idempotency
|
||||||
|
2. **Timestamp fields**:
|
||||||
|
- `create_on`: `NOW()`
|
||||||
|
- `update_on`: `NOW()`
|
||||||
|
- `create_by`: `'sys'`
|
||||||
|
- `update_by`: `'sys'`
|
||||||
|
3. **Add comments**: Add function description comment at SQL file beginning
|
||||||
|
|
||||||
|
## File Naming Standards
|
||||||
|
|
||||||
|
- Format: `{功能描述英文}_{序号}.sql`
|
||||||
|
- Sequence: Two-digit number (01-99), auto-increment
|
||||||
|
- Examples: `add_system_code_02.sql`, `update_system_code_03.sql`
|
||||||
|
|
||||||
|
## Duplicate Sequence Detection and Auto-Fix
|
||||||
|
|
||||||
|
**Important: Must detect and fix duplicate sequence numbers before generating new files or committing to Git**
|
||||||
|
|
||||||
|
### Detection Rules
|
||||||
|
|
||||||
|
1. **Scan target folder**: Check all `.sql` files in `update_service_release/pending release {version}/`
|
||||||
|
2. **Extract sequence**: Extract two-digit number after underscore from each filename
|
||||||
|
3. **Detect duplicates**: If multiple files use same sequence, trigger fix flow
|
||||||
|
|
||||||
|
### Fix Rules
|
||||||
|
|
||||||
|
When duplicate sequences found (e.g., `xxxxxx_03.sql` and `aaaaaaa_03.sql`):
|
||||||
|
|
||||||
|
1. **Analyze file dependencies**:
|
||||||
|
- Compare SQL content of both files
|
||||||
|
- Check for dependencies:
|
||||||
|
- File A references table/field/data created in File B
|
||||||
|
- File A uses stored procedure/function defined in File B
|
||||||
|
- File A updates data inserted in File B
|
||||||
|
- **Dependency detection methods**:
|
||||||
|
- Check if table names, field names, or data values in one file appear in another file
|
||||||
|
- Check if CREATE/DROP operations in one file have corresponding references in another file
|
||||||
|
- Check if INSERT/UPDATE operations in one file have corresponding queries in another file
|
||||||
|
|
||||||
|
2. **Determine file order**:
|
||||||
|
- **If dependencies exist**:
|
||||||
|
- Dependent file (referenced) keeps original sequence
|
||||||
|
- Dependent file gets next available sequence
|
||||||
|
- **If no dependencies**:
|
||||||
|
- Order by file creation time (filesystem time)
|
||||||
|
- Earlier file keeps original sequence
|
||||||
|
- Later file gets next available sequence
|
||||||
|
|
||||||
|
3. **Auto-extend subsequent files**:
|
||||||
|
- Find next available sequence (max + 1)
|
||||||
|
- Rename file needing adjustment to new sequence
|
||||||
|
- **Check if subsequent files need extension**:
|
||||||
|
- If new sequence conflicts with subsequent files, auto-extend them
|
||||||
|
- Recursively process until no conflicts
|
||||||
|
|
||||||
|
4. **Execute fix**:
|
||||||
|
- Rename files (use `git mv` to preserve Git history)
|
||||||
|
- Update Git index
|
||||||
|
- Generate fix report
|
||||||
|
|
||||||
|
### Fix Examples
|
||||||
|
|
||||||
|
**Scenario 1: With dependencies**
|
||||||
|
- Files: `create_table_03.sql` (creates table `m_new_table`), `insert_data_03.sql` (inserts data into `m_new_table`)
|
||||||
|
- Detection: `insert_data_03.sql` depends on `create_table_03.sql`
|
||||||
|
- Fix:
|
||||||
|
- `create_table_03.sql` keeps `03`
|
||||||
|
- `insert_data_03.sql` → `insert_data_04.sql`
|
||||||
|
|
||||||
|
**Scenario 2: No dependencies, order by creation time**
|
||||||
|
- Files: `add_system_code_03.sql` (created: 2024-01-01), `update_setting_03.sql` (created: 2024-01-02)
|
||||||
|
- Detection: No dependencies
|
||||||
|
- Fix:
|
||||||
|
- `add_system_code_03.sql` keeps `03` (earlier)
|
||||||
|
- `update_setting_03.sql` → `update_setting_04.sql` (later)
|
||||||
|
|
||||||
|
**Scenario 3: Need to extend subsequent files**
|
||||||
|
- Files: `file_a_03.sql`, `file_b_03.sql`, `file_c_04.sql`, `file_d_05.sql`
|
||||||
|
- Detection: `file_a_03.sql` and `file_b_03.sql` duplicate, no dependencies, `file_a_03.sql` earlier
|
||||||
|
- Fix:
|
||||||
|
- `file_a_03.sql` keeps `03`
|
||||||
|
- `file_b_03.sql` → `file_b_04.sql`
|
||||||
|
- `file_c_04.sql` → `file_c_05.sql` (extended)
|
||||||
|
- `file_d_05.sql` → `file_d_06.sql` (extended)
|
||||||
|
|
||||||
|
**Scenario 4: Multiple duplicate sequences**
|
||||||
|
- Files: `file_a_03.sql`, `file_b_03.sql`, `file_c_04.sql`, `file_d_04.sql`, `file_e_05.sql`
|
||||||
|
- Detection: `03` duplicate, `04` also duplicate
|
||||||
|
- Fix:
|
||||||
|
- First handle `03` duplicate: `file_a_03.sql` keeps `03`, `file_b_03.sql` → `file_b_04.sql`
|
||||||
|
- Then handle `04` duplicate: `file_c_04.sql` keeps `04`, `file_d_04.sql` → `file_d_05.sql`
|
||||||
|
- Extend: `file_e_05.sql` → `file_e_06.sql`
|
||||||
|
|
||||||
|
### Fix Report Format
|
||||||
|
|
||||||
|
After fix completion, generate report:
|
||||||
|
|
||||||
|
```
|
||||||
|
检测到重复序号,已自动修复:
|
||||||
|
|
||||||
|
修复的文件:
|
||||||
|
- file_b_03.sql → file_b_04.sql(原因:与 file_a_03.sql 重复,无依赖关系,按创建时间调整)
|
||||||
|
- file_c_04.sql → file_c_05.sql(原因:序号冲突,自动顺延)
|
||||||
|
- file_d_05.sql → file_d_06.sql(原因:序号冲突,自动顺延)
|
||||||
|
|
||||||
|
所有文件序号已修复,无重复。
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Timing
|
||||||
|
|
||||||
|
- **Before generating new file**: Detect and fix existing duplicate sequences
|
||||||
|
- **Before committing to Git**: Detect and fix again, ensure no duplicates
|
||||||
|
- **Manual trigger**: When user requests detection and fix
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **SQL execution failure**: Display error, don't execute Git operations
|
||||||
|
- **Git operation failure**: Display error, SQL file saved locally
|
||||||
|
- **Data validation failure**: Validate data format before generating SQL
|
||||||
|
|
||||||
|
## Important Reminders
|
||||||
|
|
||||||
|
1. All table operations requiring i18n must simultaneously update `m_system_i18n` table
|
||||||
|
2. **When `m_order` table adds field, must simultaneously add same field to `m_order_actions`, `h_order`, `h_order_actions`**
|
||||||
|
3. **When adding menus and route permissions, execution can use other methods, but Git commit must use complete SQL from stored procedure file**
|
||||||
|
4. All SQL files must be saved to correct version folder
|
||||||
|
5. **File sequences cannot be duplicated, must be incremental. Must detect and fix duplicates before generating new files and committing to Git**
|
||||||
|
6. Use REPLACE INTO to ensure SQL can be executed repeatedly
|
||||||
|
7. Must get user confirmation before executing SQL
|
||||||
|
|
||||||
|
## Continuous Improvement
|
||||||
|
|
||||||
|
This workflow is continuously updated and improved. If new tables requiring i18n are discovered, new naming conventions are needed, or process issues are found, update the `DATABASE_UPDATE_AUTOMATION_GUIDE.md` document.
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
|
||||||
|
For detailed templates and examples, see:
|
||||||
|
- `references/DATABASE_UPDATE_AUTOMATION_GUIDE.md` - Complete automation guide
|
||||||
|
- `references/create_table_template.sql` - Table creation template
|
||||||
|
- `references/table_change_template.sql` - Table structure change template
|
||||||
|
- `references/insert_system_menu_procedure.sql` - System menu insertion procedure
|
||||||
|
- `references/insert_system_route_procedure.sql` - System route insertion procedure
|
||||||
|
- `references/system_code_update_template.sql` - System code update template
|
||||||
|
- `references/SQL_SITE_NAMING.md` - SQL site naming conventions
|
||||||
@@ -0,0 +1,887 @@
|
|||||||
|
# G3FO 数据库更新自动化流程指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档定义了使用 Cursor 更新 G3FO 项目数据库数据的标准自动化流程。当需要更新数据库时,系统会自动生成 SQL 语句文件并更新到 `g3fo-db` 仓库中。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
g3fo-db/
|
||||||
|
├── common_sql/ # 通用SQL模板和工具
|
||||||
|
│ ├── insert_system_route_procedure.sql # 插入系统路由存储过程
|
||||||
|
│ ├── create_table_template.sql # 创建表模板
|
||||||
|
│ ├── system_code_update_template.sql # 系统代码更新模板
|
||||||
|
│ ├── table_change_template.sql # 表结构变更模板
|
||||||
|
│ └── DATABASE_UPDATE_AUTOMATION_GUIDE.md # 本文档
|
||||||
|
└── update_service_release/ # 版本发布SQL文件夹
|
||||||
|
└── pending release {version}/ # 待执行SQL文件夹(最新版本)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一、使用现有模板和工具
|
||||||
|
|
||||||
|
### 1.1 插入系统菜单和路径权限
|
||||||
|
|
||||||
|
**重要规则:增加菜单和路径权限必须使用 `common_sql` 文件夹中的存储过程文件。**
|
||||||
|
|
||||||
|
**使用的文件:**
|
||||||
|
- `g3fo-db/common_sql/insert_system_menu_procedure.sql` - 插入系统菜单
|
||||||
|
- `g3fo-db/common_sql/insert_system_route_procedure.sql` - 插入系统路由
|
||||||
|
|
||||||
|
**执行和提交规则:**
|
||||||
|
|
||||||
|
#### 执行 SQL 阶段
|
||||||
|
|
||||||
|
使用 **db_ops.py**(Python 数据库工具,替代 MCP MySQL)执行 SQL:
|
||||||
|
|
||||||
|
1. **批量执行含 DELIMITER 的存储过程文件**:
|
||||||
|
- 使用 `python scripts/db_ops.py batch_execute --file path/to/file.sql`
|
||||||
|
- db_ops.py 支持 DELIMITER,可正确解析并执行存储过程
|
||||||
|
|
||||||
|
2. **或使用 call_procedure 调用已存在的存储过程**:
|
||||||
|
- `python scripts/db_ops.py call_procedure InsertSystemMenu --params "..." "..." "..."`
|
||||||
|
|
||||||
|
3. **或使用直接的 INSERT/REPLACE 语句**:
|
||||||
|
- 使用 `execute` 命令执行 `INSERT INTO` 或 `REPLACE INTO`
|
||||||
|
- 确保数据与存储过程生成的数据完全一致
|
||||||
|
|
||||||
|
4. **数据一致性要求**:
|
||||||
|
- 菜单数据必须插入到 `m_system_menus` 表
|
||||||
|
- 路由数据必须插入到 `m_system_routes` 表
|
||||||
|
- 相应的权限记录必须插入到 `m_admin_role_rights` 或 `m_user_role_rights` 表
|
||||||
|
- 所有数据必须与存储过程生成的数据完全一致
|
||||||
|
|
||||||
|
#### 提交到 Git 阶段
|
||||||
|
|
||||||
|
**必须使用存储过程文件生成的完整 SQL:**
|
||||||
|
|
||||||
|
1. **从模板文件复制**:
|
||||||
|
- 从 `g3fo-db/common_sql/insert_system_menu_procedure.sql` 复制完整的存储过程定义
|
||||||
|
- 或从 `g3fo-db/common_sql/insert_system_route_procedure.sql` 复制完整的存储过程定义
|
||||||
|
|
||||||
|
2. **添加 CALL 语句**:
|
||||||
|
- 在文件末尾添加 `CALL` 语句来调用存储过程
|
||||||
|
- 参考 `update_service_release/pending release 1.5.1.18` 中的文件格式
|
||||||
|
|
||||||
|
3. **文件格式示例**:
|
||||||
|
|
||||||
|
**插入系统菜单:**
|
||||||
|
```sql
|
||||||
|
USE g3fo_base;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 存储过程:插入系统菜单数据
|
||||||
|
-- ... (完整的存储过程定义,从 insert_system_menu_procedure.sql 复制) ...
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS InsertSystemMenu;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE InsertSystemMenu(...)
|
||||||
|
BEGIN
|
||||||
|
-- ... 存储过程代码 ...
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 在这里添加需要插入的菜单
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
CALL InsertSystemMenu('菜单备注', 'menu-name', 'title_remark', 'ADMIN', NULL, 1);
|
||||||
|
CALL InsertSystemMenu('菜单备注2', 'menu-name-2', 'title_remark', 'USER', NULL, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
**插入系统路由:**
|
||||||
|
```sql
|
||||||
|
USE g3fo_base;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 存储过程:插入系统路由数据
|
||||||
|
-- ... (完整的存储过程定义,从 insert_system_route_procedure.sql 复制) ...
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS InsertSystemRoute;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE InsertSystemRoute(...)
|
||||||
|
BEGIN
|
||||||
|
-- ... 存储过程代码 ...
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 在这里添加需要插入的路由
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
CALL InsertSystemRoute('备注', '/route/path', '标题备注', 'ADMIN');
|
||||||
|
CALL InsertSystemRoute('备注2', '/route/path2', '标题备注', 'USER');
|
||||||
|
```
|
||||||
|
|
||||||
|
**文件命名规范:**
|
||||||
|
- 菜单:`insert_system_menu_procedure_{序号}.sql`
|
||||||
|
- 路由:`insert_system_route_procedure_{序号}.sql`
|
||||||
|
|
||||||
|
**参考示例:**
|
||||||
|
- 查看 `update_service_release/pending release 1.5.1.18/insert_system_menu_procedure_03.sql` 作为参考
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
|
||||||
|
**InsertSystemMenu 参数:**
|
||||||
|
- `p_remark`: 菜单备注(varchar(50))
|
||||||
|
- `p_menu_name`: 菜单名称,唯一标识(varchar(255))
|
||||||
|
- `p_title_remark`: 标题备注(varchar(50))
|
||||||
|
- `p_menu_type`: 菜单类型,'ADMIN' 或 'USER'(char(8))
|
||||||
|
- `p_description_i18n_id`: 描述国际化ID,可选(char(32)),默认为NULL
|
||||||
|
- `p_active_flag`: 激活标志,可选(smallint),默认为1
|
||||||
|
|
||||||
|
**InsertSystemRoute 参数:**
|
||||||
|
- `p_remark`: 备注
|
||||||
|
- `p_route`: 路由地址
|
||||||
|
- `p_title_remark`: 标题备注
|
||||||
|
- `p_route_type`: 路由类型('ADMIN' 或 'USER')
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- 自动生成ID(菜单从1开始,路由从7000开始)
|
||||||
|
- 自动为相应角色模板创建权限记录
|
||||||
|
- 自动去重检查
|
||||||
|
|
||||||
|
### 1.2 创建新表
|
||||||
|
|
||||||
|
当需要创建新表时,参照 `create_table_template.sql` 中的格式:
|
||||||
|
|
||||||
|
- 使用 `CREATE TABLE IF NOT EXISTS`
|
||||||
|
- 指定正确的 schema(如 `g3fo_trade`, `g3fo_base` 等)
|
||||||
|
- 包含必要的字段、索引、注释
|
||||||
|
- 使用 `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`
|
||||||
|
|
||||||
|
### 1.3 表结构变更
|
||||||
|
|
||||||
|
当需要修改表结构时,参照 `table_change_template.sql` 中的方法:
|
||||||
|
|
||||||
|
- 使用存储过程检查列是否存在
|
||||||
|
- 使用 `IF EXISTS` 进行幂等性检查
|
||||||
|
- 执行后删除存储过程
|
||||||
|
|
||||||
|
### 1.4 订单表字段同步规则
|
||||||
|
|
||||||
|
**重要:当 `m_order` 表需要增加字段时,以下三个表必须同时增加相同的字段:**
|
||||||
|
|
||||||
|
1. `m_order_actions` - 订单操作表(g3fo_trade schema)
|
||||||
|
2. `h_order` - 订单历史表(g3fo_trade schema)
|
||||||
|
3. `h_order_actions` - 订单操作历史表(g3fo_trade schema)
|
||||||
|
|
||||||
|
**规则说明:**
|
||||||
|
- 字段名称必须完全一致
|
||||||
|
- 字段类型必须完全一致
|
||||||
|
- 字段约束(NULL/NOT NULL、DEFAULT 等)必须完全一致
|
||||||
|
- 字段注释必须完全一致
|
||||||
|
- 字段位置(AFTER 子句)应保持一致
|
||||||
|
|
||||||
|
**SQL 生成要求:**
|
||||||
|
- 使用存储过程进行幂等性检查(参照 `table_change_template.sql`)
|
||||||
|
- 为每个表分别检查字段是否存在,不存在则添加
|
||||||
|
- 确保所有四个表的字段变更在同一个 SQL 文件中完成
|
||||||
|
- 使用相同的存储过程模式,为每个表创建独立的检查逻辑
|
||||||
|
|
||||||
|
**SQL 模板示例:**
|
||||||
|
```sql
|
||||||
|
USE g3fo_trade;
|
||||||
|
DROP PROCEDURE IF EXISTS AddOrderField;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE AddOrderField()
|
||||||
|
BEGIN
|
||||||
|
-- 为 m_order 表添加字段
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order'
|
||||||
|
AND column_name = 'new_field'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order`
|
||||||
|
ADD COLUMN `new_field` VARCHAR(100) NULL DEFAULT NULL COMMENT '新字段'
|
||||||
|
AFTER `existing_field`;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 为 m_order_actions 表添加相同字段
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order_actions'
|
||||||
|
AND column_name = 'new_field'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order_actions`
|
||||||
|
ADD COLUMN `new_field` VARCHAR(100) NULL DEFAULT NULL COMMENT '新字段'
|
||||||
|
AFTER `existing_field`;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 为 h_order 表添加相同字段
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order'
|
||||||
|
AND column_name = 'new_field'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order`
|
||||||
|
ADD COLUMN `new_field` VARCHAR(100) NULL DEFAULT NULL COMMENT '新字段'
|
||||||
|
AFTER `existing_field`;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 为 h_order_actions 表添加相同字段
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order_actions'
|
||||||
|
AND column_name = 'new_field'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order_actions`
|
||||||
|
ADD COLUMN `new_field` VARCHAR(100) NULL DEFAULT NULL COMMENT '新字段'
|
||||||
|
AFTER `existing_field`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL AddOrderField();
|
||||||
|
DROP PROCEDURE IF EXISTS AddOrderField;
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事项:**
|
||||||
|
- 所有四个表的字段定义必须完全一致
|
||||||
|
- 必须使用幂等性检查,确保 SQL 可以重复执行
|
||||||
|
- 字段位置(AFTER 子句)应保持一致,便于维护
|
||||||
|
|
||||||
|
## 二、需要同时更新 m_system_i18n 的表
|
||||||
|
|
||||||
|
以下表在更新数据时必须同时更新 `m_system_i18n` 表:
|
||||||
|
|
||||||
|
| 表名 | i18n_id 格式 | 填充值 | 说明 |
|
||||||
|
|------|-------------|--------|------|
|
||||||
|
| `m_system_code` | `CODE-{id}` | 4位数字ID(如 `CODE-0404`) | 系统编码表 |
|
||||||
|
| `m_system_error_message` | `ERR-{id}` | 错误码ID | 系统错误消息表 |
|
||||||
|
| `m_market` | `MKT-{id}` | 4位数字ID | 市场表 |
|
||||||
|
| `m_currency` | `CURR-{code}` | 货币代码 | 货币表 |
|
||||||
|
| `m_channel` | `CH-{code}` | 渠道代码 | 渠道表 |
|
||||||
|
| `m_exchange` | `EXCH-{code}` | 交易所代码 | 交易所表 |
|
||||||
|
| `m_country` | `CNTY-{code}` | 国家代码 | 国家表 |
|
||||||
|
| `m_exchange_charge` | `EX_CHARGE-{id}` | 费用ID | 交易所费用表 |
|
||||||
|
| `m_company_charge` | `COM_CHARGE-{id}` | 费用ID | 公司费用表 |
|
||||||
|
|
||||||
|
**重要:** 所有上述表的插入和更新操作都必须同时更新 `m_system_i18n` 表,以提供多语言支持。
|
||||||
|
|
||||||
|
## 三、自动化数据插入流程
|
||||||
|
|
||||||
|
### 3.1 流程概述
|
||||||
|
|
||||||
|
1. **用户输入数据** → 2. **生成SQL语句** → 3. **用户确认** → 4. **执行SQL** → 5. **确认执行结果** → 6. **询问是否提交到Git** → 7. **Git操作**
|
||||||
|
|
||||||
|
### 3.2 以 m_system_code 为例的详细流程
|
||||||
|
|
||||||
|
#### 步骤 1: 请求用户输入
|
||||||
|
|
||||||
|
当用户需要更新或增加 `m_system_code` 的数据时,Cursor 应请求以下信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
请提供以下数据:
|
||||||
|
- Code_type: [编码类型]
|
||||||
|
- Code_value: [编码值]
|
||||||
|
- Remark: [备注(用于生成多语言描述)]
|
||||||
|
- Sort: [排序号]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2: 自动生成SQL
|
||||||
|
|
||||||
|
Cursor 自动执行以下操作:
|
||||||
|
|
||||||
|
1. **获取当前最大ID**
|
||||||
|
```sql
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 FROM g3fo_base.m_system_code;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **生成 description_i18n_id**
|
||||||
|
- 格式:`CODE-{4位数字ID}`
|
||||||
|
- 例如:ID=404 → `CODE-0404`
|
||||||
|
|
||||||
|
3. **根据 remark 生成多语言描述**
|
||||||
|
- 如果 remark 包含中英文,自动提取
|
||||||
|
- 如果没有,使用 remark 作为英文描述
|
||||||
|
- 简体中文(sc_description):从 remark 提取或使用中文部分
|
||||||
|
- 繁体中文(tc_description):转换简体为繁体
|
||||||
|
- 越南语(vn_description):可选,如果用户提供则使用
|
||||||
|
- 法语(fr_description):可选
|
||||||
|
|
||||||
|
4. **生成 SQL 语句**
|
||||||
|
```sql
|
||||||
|
-- 插入 m_system_code
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_code`
|
||||||
|
(`id`, `code_type`, `code_value`, `description_i18n_id`, `remark`, `sort`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES
|
||||||
|
({new_id}, '{code_type}', '{code_value}', 'CODE-{formatted_id}', '{remark}', {sort}, 'sys', NOW(), 'sys', NOW());
|
||||||
|
|
||||||
|
-- 插入 m_system_i18n
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_i18n`
|
||||||
|
(`id`, `en_description`, `sc_description`, `tc_description`, `vn_description`, `fr_description`, `remark`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES
|
||||||
|
('CODE-{formatted_id}', '{en_description}', '{sc_description}', '{tc_description}', '{vn_description}', '{fr_description}', NULL, 'sys', NOW(), 'sys', NOW());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3: 用户确认
|
||||||
|
|
||||||
|
向用户展示生成的 SQL 语句,询问:
|
||||||
|
|
||||||
|
```
|
||||||
|
已生成以下 SQL 语句,请确认数据是否正确:
|
||||||
|
|
||||||
|
[显示SQL语句]
|
||||||
|
|
||||||
|
数据是否有问题?(yes/no)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4: 执行SQL
|
||||||
|
|
||||||
|
如果用户确认无误,使用 **db_ops.py** 执行 SQL 语句:
|
||||||
|
|
||||||
|
1. 在终端执行:`python .claude/skills/g3fo-db-ops/scripts/db_ops.py execute "SQL"` 或 `batch_execute --file path`
|
||||||
|
2. 默认连接:host 192.168.3.233, user root, password afe123456;可通过 `--database g3fo_base` 或 `g3fo_trade` 指定库
|
||||||
|
3. 用户可提供不同连接信息时,使用 `--host`, `--port`, `--user`, `--password`, `--database` 覆盖
|
||||||
|
4. 验证执行结果(JSON 输出中 `"success": true`)
|
||||||
|
5. 如果成功,继续下一步;如果失败,报告错误并停止
|
||||||
|
|
||||||
|
#### 步骤 5: 询问是否提交到Git
|
||||||
|
|
||||||
|
执行成功后,询问用户:
|
||||||
|
|
||||||
|
```
|
||||||
|
SQL 执行成功!
|
||||||
|
|
||||||
|
是否需要将 SQL 提交到待执行文件夹中?(yes/no)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 6: Git 操作
|
||||||
|
|
||||||
|
如果用户确认提交,**必须按照以下顺序执行**,确保远程仓库和本地数据一致:
|
||||||
|
|
||||||
|
1. **切换到 g3fo-db 目录并拉取最新代码**(必须先执行,确保获取最新文件列表)
|
||||||
|
```bash
|
||||||
|
cd g3fo-db
|
||||||
|
git pull origin main # 或相应的分支名
|
||||||
|
```
|
||||||
|
- **重要**:这一步必须在确定文件夹和生成文件名之前执行
|
||||||
|
- 确保获取远程仓库的最新文件列表和更改
|
||||||
|
- 如果 pull 失败(如有冲突),报告错误并停止流程
|
||||||
|
|
||||||
|
2. **确定目标文件夹**(基于最新拉取的代码)
|
||||||
|
- 查找 `update_service_release` 下最新的 "pending release {version}" 文件夹
|
||||||
|
- **如果不存在,需要创建新的文件夹**:
|
||||||
|
- 查看 `update_service_release` 下所有版本号文件夹(如 `1.5.1.0`, `1.5.1.1`, `1.5.1.17` 等)
|
||||||
|
- 找到最大的版本号,新版本号 = 最大版本号 + 0.0.0.1(最后一位递增)
|
||||||
|
- 例如:如果最大版本号是 `1.5.1.17`,新版本号是 `1.5.1.18`
|
||||||
|
- 创建新文件夹:`pending release {新版本号}`
|
||||||
|
- **自动创建 `set_version_01.sql` 文件**(必须在新文件夹中创建):
|
||||||
|
- 文件内容格式:
|
||||||
|
```sql
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '1.5.1.0 M{版本号最后两位数字}',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.systemVersion';
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '0',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.patchNo';
|
||||||
|
```
|
||||||
|
- **版本号与 param_value 的对应关系**:
|
||||||
|
- 版本号 `1.5.1.17` → `param_value` = `'1.5.1.0 M17'`
|
||||||
|
- 版本号 `1.5.1.18` → `param_value` = `'1.5.1.0 M18'`
|
||||||
|
- 版本号 `1.5.1.19` → `param_value` = `'1.5.1.0 M19'`
|
||||||
|
- 版本号 `1.5.1.10` → `param_value` = `'1.5.1.0 M10'`
|
||||||
|
- **规律**:提取版本号的最后一部分数字(版本号格式为 X.Y.Z.N,提取 N),格式为 `'1.5.1.0 M{N}'`
|
||||||
|
- **示例**:
|
||||||
|
- `1.5.1.17` → 提取 `17` → `'1.5.1.0 M17'`
|
||||||
|
- `1.5.1.18` → 提取 `18` → `'1.5.1.0 M18'`
|
||||||
|
- `1.5.1.10` → 提取 `10` → `'1.5.1.0 M10'`
|
||||||
|
- `1.5.1.9` → 提取 `9` → `'1.5.1.0 M9'`(不补零,保持原数字)
|
||||||
|
- **注意**:此时文件夹列表是最新的,避免文件名冲突
|
||||||
|
|
||||||
|
3. **生成文件名**(基于最新的文件列表)
|
||||||
|
- 格式:`{功能描述英文}_{序号}.sql`
|
||||||
|
- 序号规则:
|
||||||
|
- **必须在 git pull 之后执行**,查看目标文件夹中已有的 SQL 文件(基于最新拉取的代码)
|
||||||
|
- **跨所有文件统一计算序号**,不区分文件名前缀
|
||||||
|
- **提取序号方法**:
|
||||||
|
1. 遍历文件夹中所有 `.sql` 文件
|
||||||
|
2. 从每个文件名中提取下划线 `_` 后面的两位数字(格式:`_XX.sql`)
|
||||||
|
3. 例如:`set_version_01.sql` → 提取 `01`,`add_system_code_02.sql` → 提取 `02`
|
||||||
|
4. 找到所有序号中的**最大值**
|
||||||
|
5. 新序号 = 最大序号 + 1
|
||||||
|
- 使用两位数字格式(01, 02, ..., 99)
|
||||||
|
- **示例**:
|
||||||
|
- 如果文件夹中有:`set_version_01.sql` 和 `add_system_code_02.sql`
|
||||||
|
- 提取的序号:`01`, `02`
|
||||||
|
- 最大序号:`02`
|
||||||
|
- 新文件序号:`03`
|
||||||
|
- 新文件名:`add_new_feature_03.sql`
|
||||||
|
- **重要**:确保序号不重复,必须基于最新拉取的代码生成
|
||||||
|
|
||||||
|
4. **检测并修复重复序号**(在保存文件之前)
|
||||||
|
- 扫描目标文件夹中所有 `.sql` 文件
|
||||||
|
- 检测是否有重复序号
|
||||||
|
- 如果发现重复,按照"5.4 重复序号检测和自动修复"规则处理
|
||||||
|
- 确保所有文件序号唯一且连续
|
||||||
|
- 如果进行了修复,生成修复报告并显示给用户
|
||||||
|
|
||||||
|
5. **保存 SQL 文件**
|
||||||
|
- 将 SQL 语句保存到目标文件夹
|
||||||
|
- 文件名使用生成的名称
|
||||||
|
|
||||||
|
6. **Git 提交和推送**
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: add system code {code_type} - {code_value}"
|
||||||
|
git push origin main # 或相应的分支名
|
||||||
|
```
|
||||||
|
|
||||||
|
**如果 push 失败**(如远程有新的提交):
|
||||||
|
- 再次执行 `git pull origin main`(可能需要处理合并)
|
||||||
|
- 解决冲突后重新 push
|
||||||
|
|
||||||
|
#### 步骤 7: 完成提示
|
||||||
|
|
||||||
|
```
|
||||||
|
操作完成!
|
||||||
|
|
||||||
|
已执行的操作:
|
||||||
|
✓ SQL 语句已执行
|
||||||
|
✓ SQL 文件已保存到: update_service_release/pending release {version}/{filename}
|
||||||
|
✓ 已推送到 Git 仓库
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、其他表的处理规则
|
||||||
|
|
||||||
|
### 4.1 m_system_error_message
|
||||||
|
|
||||||
|
**输入数据:**
|
||||||
|
- Error_code: 错误码(数字)
|
||||||
|
- Message_i18n_id: 自动生成 `ERR-{error_code}`
|
||||||
|
- Remark: 英文描述
|
||||||
|
|
||||||
|
**i18n_id 生成:** `ERR-{error_code}`
|
||||||
|
|
||||||
|
**SQL 模板:**
|
||||||
|
```sql
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_error_message`
|
||||||
|
(`id`, `error_code`, `message_i18n_id`, `remark`)
|
||||||
|
VALUES
|
||||||
|
('{error_code}', 'SFO-{error_code}', 'ERR-{error_code}', '{remark}');
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_i18n`
|
||||||
|
(`id`, `en_description`, `sc_description`, `tc_description`, `vn_description`, `fr_description`, `remark`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES
|
||||||
|
('ERR-{error_code}', '{en_description}', '{sc_description}', '{tc_description}', '{vn_description}', '{fr_description}', NULL, 'sys', NOW(), 'sys', NOW());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 m_market
|
||||||
|
|
||||||
|
**输入数据:**
|
||||||
|
- Market_code: 市场代码
|
||||||
|
- Market_name: 市场名称(用于生成i18n)
|
||||||
|
- 其他必要字段
|
||||||
|
|
||||||
|
**i18n_id 生成:** `MKT-{4位数字ID}`(需要先获取最大ID)
|
||||||
|
|
||||||
|
### 4.3 m_currency, m_channel, m_exchange, m_country
|
||||||
|
|
||||||
|
**输入数据:**
|
||||||
|
- Code: 代码(用于生成i18n_id)
|
||||||
|
- Name: 名称(用于生成i18n)
|
||||||
|
- 其他必要字段
|
||||||
|
|
||||||
|
**i18n_id 生成:**
|
||||||
|
- Currency: `CURR-{currency_code}`
|
||||||
|
- Channel: `CH-{channel_code}`
|
||||||
|
- Exchange: `EXCH-{exchange_code}`
|
||||||
|
- Country: `CNTY-{country_code}`
|
||||||
|
|
||||||
|
### 4.4 m_exchange_charge, m_company_charge
|
||||||
|
|
||||||
|
**输入数据:**
|
||||||
|
- 相关字段
|
||||||
|
- 描述信息(用于生成i18n)
|
||||||
|
|
||||||
|
**i18n_id 生成:**
|
||||||
|
- Exchange Charge: `EX_CHARGE-{id}`
|
||||||
|
- Company Charge: `COM_CHARGE-{id}`
|
||||||
|
|
||||||
|
## 五、文件命名规范
|
||||||
|
|
||||||
|
### 5.1 命名格式
|
||||||
|
|
||||||
|
```
|
||||||
|
{功能描述英文}_{序号}.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 功能描述示例
|
||||||
|
|
||||||
|
- `add_system_code` - 添加系统代码
|
||||||
|
- `update_system_code` - 更新系统代码
|
||||||
|
- `add_system_error_message` - 添加错误消息
|
||||||
|
- `insert_system_route` - 插入系统路由
|
||||||
|
- `create_table_{table_name}` - 创建表
|
||||||
|
- `update_table_{table_name}_column` - 更新表列
|
||||||
|
- `add_{table_name}_data` - 添加表数据
|
||||||
|
|
||||||
|
### 5.3 序号规则
|
||||||
|
|
||||||
|
**重要:跨所有文件统一计算序号,不区分文件名前缀**
|
||||||
|
|
||||||
|
1. **查看目标文件夹中所有 `.sql` 文件**(基于最新拉取的代码)
|
||||||
|
2. **提取序号方法**:
|
||||||
|
- 遍历所有 `.sql` 文件
|
||||||
|
- 从每个文件名中提取下划线 `_` 后面的两位数字(格式:`_XX.sql`)
|
||||||
|
- 例如:
|
||||||
|
- `set_version_01.sql` → 提取 `01`
|
||||||
|
- `add_system_code_02.sql` → 提取 `02`
|
||||||
|
- `update_table_05.sql` → 提取 `05`
|
||||||
|
3. **找到所有序号中的最大值**
|
||||||
|
4. **新文件的序号 = 最大序号 + 1**
|
||||||
|
5. **使用两位数字格式(01-99)**
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
- 如果文件夹中有:`set_version_01.sql`, `add_system_code_02.sql`, `update_table_05.sql`
|
||||||
|
- 提取的序号:`01`, `02`, `05`
|
||||||
|
- 最大序号:`05`
|
||||||
|
- 新文件序号:`06`
|
||||||
|
- 新文件名:`add_new_feature_06.sql`(不限制文件名前缀)
|
||||||
|
|
||||||
|
**关键点:**
|
||||||
|
- 序号是跨所有文件统一计算的,不区分文件名前缀
|
||||||
|
- 即使已有 `xxxxxx_01.sql`,如果最大序号是 `01`,新文件应该是 `xxxxxx_02.sql`
|
||||||
|
- 如果文件夹中只有 `set_version_01.sql`,新文件应该是 `xxxxxx_02.sql`(不是 `01`)
|
||||||
|
|
||||||
|
### 5.4 重复序号检测和自动修复
|
||||||
|
|
||||||
|
**重要:在生成新文件或提交到 Git 之前,必须检测并修复重复序号**
|
||||||
|
|
||||||
|
#### 检测规则
|
||||||
|
|
||||||
|
1. **扫描目标文件夹**:检查 `update_service_release/pending release {version}/` 文件夹中所有 `.sql` 文件
|
||||||
|
2. **提取序号**:从每个文件名中提取下划线 `_` 后面的两位数字
|
||||||
|
3. **检测重复**:如果发现多个文件使用相同的序号,触发修复流程
|
||||||
|
|
||||||
|
#### 修复规则
|
||||||
|
|
||||||
|
当发现重复序号时(例如:`xxxxxx_03.sql` 和 `aaaaaaa_03.sql`),按以下步骤处理:
|
||||||
|
|
||||||
|
1. **分析文件依赖关系**:
|
||||||
|
- 比对两个文件的 SQL 内容
|
||||||
|
- 检查是否存在依赖关系:
|
||||||
|
- 文件 A 中引用了文件 B 创建的表、字段、数据等
|
||||||
|
- 文件 A 中使用了文件 B 中定义的存储过程、函数等
|
||||||
|
- 文件 A 中更新了文件 B 中插入的数据
|
||||||
|
- **依赖关系判断方法**:
|
||||||
|
- 检查 SQL 中的表名、字段名、数据值是否在另一个文件中出现
|
||||||
|
- 检查是否有 CREATE/DROP 操作在另一个文件中有对应的引用
|
||||||
|
- 检查是否有 INSERT/UPDATE 操作在另一个文件中有对应的查询
|
||||||
|
- 检查存储过程、函数的定义和调用关系
|
||||||
|
|
||||||
|
2. **确定文件顺序**:
|
||||||
|
- **如果有依赖关系**:
|
||||||
|
- 被依赖的文件(被引用)排在前面,保持原序号
|
||||||
|
- 依赖其他文件的排在后面,序号改为下一个可用序号
|
||||||
|
- **如果没有依赖关系**:
|
||||||
|
- 按照文件的创建时间(文件系统时间)确定先后顺序
|
||||||
|
- 创建时间较早的文件保持原序号
|
||||||
|
- 创建时间较晚的文件序号改为下一个可用序号
|
||||||
|
|
||||||
|
3. **自动顺延后续文件**:
|
||||||
|
- 找到下一个可用的序号(最大序号 + 1)
|
||||||
|
- 将重复序号中需要调整的文件重命名为新序号
|
||||||
|
- **检查后续文件是否需要顺延**:
|
||||||
|
- 如果新序号与后续文件序号冲突,自动顺延后续文件
|
||||||
|
- 例如:如果 `cccccc_04.sql` 和 `bbbbbbb_05.sql` 存在,且修复后新序号是 `04`
|
||||||
|
- 则 `cccccc_04.sql` → `cccccc_05.sql`,`bbbbbbb_05.sql` → `bbbbbbb_06.sql`
|
||||||
|
- 递归处理,直到没有冲突
|
||||||
|
|
||||||
|
4. **执行修复**:
|
||||||
|
- 使用 `git mv` 重命名文件(保持 Git 历史)
|
||||||
|
- 更新 Git 索引
|
||||||
|
- 生成修复报告,显示所有重命名的文件
|
||||||
|
|
||||||
|
#### 修复示例
|
||||||
|
|
||||||
|
**场景 1:有依赖关系**
|
||||||
|
- 文件:`create_table_03.sql`(创建表 `m_new_table`),`insert_data_03.sql`(向 `m_new_table` 插入数据)
|
||||||
|
- 检测:`insert_data_03.sql` 依赖 `create_table_03.sql`(引用了 `m_new_table`)
|
||||||
|
- 修复:
|
||||||
|
- `create_table_03.sql` 保持 `03`
|
||||||
|
- `insert_data_03.sql` → `insert_data_04.sql`
|
||||||
|
|
||||||
|
**场景 2:无依赖关系,按创建时间**
|
||||||
|
- 文件:`add_system_code_03.sql`(创建时间:2024-01-01),`update_setting_03.sql`(创建时间:2024-01-02)
|
||||||
|
- 检测:无依赖关系
|
||||||
|
- 修复:
|
||||||
|
- `add_system_code_03.sql` 保持 `03`(较早)
|
||||||
|
- `update_setting_03.sql` → `update_setting_04.sql`(较晚)
|
||||||
|
|
||||||
|
**场景 3:需要顺延后续文件**
|
||||||
|
- 文件:`file_a_03.sql`, `file_b_03.sql`, `file_c_04.sql`, `file_d_05.sql`
|
||||||
|
- 检测:`file_a_03.sql` 和 `file_b_03.sql` 重复,无依赖关系,`file_a_03.sql` 较早
|
||||||
|
- 修复:
|
||||||
|
- `file_a_03.sql` 保持 `03`
|
||||||
|
- `file_b_03.sql` → `file_b_04.sql`
|
||||||
|
- `file_c_04.sql` → `file_c_05.sql`(顺延,因为 `04` 已被占用)
|
||||||
|
- `file_d_05.sql` → `file_d_06.sql`(顺延,因为 `05` 已被占用)
|
||||||
|
|
||||||
|
**场景 4:多个重复序号**
|
||||||
|
- 文件:`file_a_03.sql`, `file_b_03.sql`, `file_c_04.sql`, `file_d_04.sql`, `file_e_05.sql`
|
||||||
|
- 检测:`03` 重复,`04` 也重复
|
||||||
|
- 修复:
|
||||||
|
- 先处理 `03` 的重复:`file_a_03.sql` 保持 `03`,`file_b_03.sql` → `file_b_04.sql`
|
||||||
|
- 再处理 `04` 的重复:`file_c_04.sql` 保持 `04`,`file_d_04.sql` → `file_d_05.sql`
|
||||||
|
- 顺延:`file_e_05.sql` → `file_e_06.sql`
|
||||||
|
|
||||||
|
#### 执行时机
|
||||||
|
|
||||||
|
- **在生成新文件之前**:先检测并修复现有文件的重复序号
|
||||||
|
- **在提交到 Git 之前**:再次检测并修复,确保没有重复序号
|
||||||
|
- **可以手动触发**:用户要求检测和修复时
|
||||||
|
|
||||||
|
#### 修复报告格式
|
||||||
|
|
||||||
|
修复完成后,生成报告:
|
||||||
|
|
||||||
|
```
|
||||||
|
检测到重复序号,已自动修复:
|
||||||
|
|
||||||
|
修复的文件:
|
||||||
|
- file_b_03.sql → file_b_04.sql(原因:与 file_a_03.sql 重复,无依赖关系,按创建时间调整)
|
||||||
|
- file_c_04.sql → file_c_05.sql(原因:序号冲突,自动顺延)
|
||||||
|
- file_d_05.sql → file_d_06.sql(原因:序号冲突,自动顺延)
|
||||||
|
|
||||||
|
所有文件序号已修复,无重复。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、SQL 语句规范
|
||||||
|
|
||||||
|
### 6.1 使用 REPLACE INTO
|
||||||
|
|
||||||
|
对于数据插入和更新,使用 `REPLACE INTO` 而不是 `INSERT INTO`,以确保幂等性:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
REPLACE INTO `schema`.`table_name`
|
||||||
|
(`column1`, `column2`, ...)
|
||||||
|
VALUES
|
||||||
|
('value1', 'value2', ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 时间戳字段
|
||||||
|
|
||||||
|
- `create_on`: `NOW()` 或 `CURRENT_TIMESTAMP`
|
||||||
|
- `update_on`: `NOW()` 或 `CURRENT_TIMESTAMP`
|
||||||
|
- `create_by`: `'sys'`(系统操作)或实际用户名
|
||||||
|
- `update_by`: `'sys'`(系统操作)或实际用户名
|
||||||
|
|
||||||
|
### 6.3 注释
|
||||||
|
|
||||||
|
在 SQL 文件开头添加注释说明:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ===================================================================
|
||||||
|
-- 功能描述:添加系统代码
|
||||||
|
-- 表名:m_system_code, m_system_i18n
|
||||||
|
-- 创建时间:{date}
|
||||||
|
-- ===================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、Git 操作规范
|
||||||
|
|
||||||
|
### 7.1 操作顺序(重要)
|
||||||
|
|
||||||
|
**必须严格按照以下顺序执行 Git 操作,确保远程仓库和本地数据一致:**
|
||||||
|
|
||||||
|
1. **先执行 `git pull`** - 获取最新代码和文件列表
|
||||||
|
2. **然后确定目标文件夹** - 基于最新拉取的代码
|
||||||
|
3. **生成文件名** - 基于最新的文件列表,避免序号冲突
|
||||||
|
4. **保存文件** - 创建 SQL 文件
|
||||||
|
5. **最后执行 `git add`, `commit`, `push`** - 提交更改
|
||||||
|
|
||||||
|
**为什么必须先 pull?**
|
||||||
|
- 确保获取远程仓库的最新文件列表
|
||||||
|
- 避免文件名序号冲突(其他人可能已经添加了新文件)
|
||||||
|
- 避免覆盖其他人的更改
|
||||||
|
- 确保本地和远程仓库的一致性
|
||||||
|
|
||||||
|
### 7.2 Commit 消息格式
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: {简短描述}
|
||||||
|
|
||||||
|
详细说明(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
- `feat: add system code OrderType - 015`
|
||||||
|
- `feat: add system error message SFO-17100`
|
||||||
|
- `feat: insert system route /trade/order/retryOrder`
|
||||||
|
|
||||||
|
### 7.3 分支管理
|
||||||
|
|
||||||
|
- 默认推送到 `main` 分支
|
||||||
|
- 如果项目使用其他分支策略,遵循项目规范
|
||||||
|
|
||||||
|
### 7.4 冲突处理
|
||||||
|
|
||||||
|
如果 `git pull` 时出现冲突:
|
||||||
|
1. 报告冲突文件
|
||||||
|
2. 询问用户是否手动解决
|
||||||
|
3. 或提供解决建议
|
||||||
|
4. **不要继续执行后续步骤**,直到冲突解决
|
||||||
|
|
||||||
|
如果 `git push` 时失败(远程有新的提交):
|
||||||
|
1. 再次执行 `git pull origin main`(可能需要处理合并)
|
||||||
|
2. 解决冲突后重新 push
|
||||||
|
|
||||||
|
## 八、错误处理
|
||||||
|
|
||||||
|
### 8.1 SQL 执行失败
|
||||||
|
|
||||||
|
- 显示错误信息
|
||||||
|
- 不执行 Git 操作
|
||||||
|
- 询问用户是否重试
|
||||||
|
|
||||||
|
### 8.2 Git 操作失败
|
||||||
|
|
||||||
|
- 显示错误信息
|
||||||
|
- SQL 文件已保存到本地
|
||||||
|
- 提供手动 Git 操作建议
|
||||||
|
|
||||||
|
### 8.3 数据验证失败
|
||||||
|
|
||||||
|
- 在生成 SQL 前验证数据格式
|
||||||
|
- 如果验证失败,提示用户修正
|
||||||
|
|
||||||
|
## 九、重要提醒
|
||||||
|
|
||||||
|
### 9.1 必须同时更新的表
|
||||||
|
|
||||||
|
1. **i18n 表更新**:所有需要多语言支持的表操作都必须同时更新 `m_system_i18n` 表
|
||||||
|
2. **订单表字段同步**:当 `m_order` 表增加字段时,必须同时在以下三个表中增加相同字段:
|
||||||
|
- `m_order_actions` - 订单操作表
|
||||||
|
- `h_order` - 订单历史表
|
||||||
|
- `h_order_actions` - 订单操作历史表
|
||||||
|
|
||||||
|
### 9.2 菜单和路径权限处理规则
|
||||||
|
|
||||||
|
**重要:增加菜单和路径权限必须使用存储过程文件**
|
||||||
|
|
||||||
|
1. **执行 SQL 阶段**:
|
||||||
|
- 使用 **db_ops.py** 的 `batch_execute --file` 执行含 DELIMITER 的存储过程文件
|
||||||
|
- 或使用 `call_procedure` 调用已存在的存储过程
|
||||||
|
- 或使用 `execute` 配合直接的 `INSERT INTO` / `REPLACE INTO`,确保数据与存储过程生成的数据完全一致
|
||||||
|
|
||||||
|
2. **提交到 Git 阶段**:
|
||||||
|
- **必须使用存储过程文件生成的完整 SQL**
|
||||||
|
- 从 `common_sql` 文件夹复制完整的存储过程定义
|
||||||
|
- 在文件末尾添加 `CALL` 语句
|
||||||
|
- 参考 `update_service_release/pending release 1.5.1.18` 中的文件格式
|
||||||
|
|
||||||
|
### 9.3 其他重要规则
|
||||||
|
|
||||||
|
1. 所有 SQL 文件必须保存到正确的版本文件夹中
|
||||||
|
2. **文件序号不能重复,必须递增。在生成新文件和提交到 Git 之前,必须检测并修复重复序号**
|
||||||
|
3. 使用 REPLACE INTO 确保 SQL 可以重复执行
|
||||||
|
4. 执行 SQL 前必须获得用户确认
|
||||||
|
5. 表结构变更必须使用存储过程进行幂等性检查
|
||||||
|
|
||||||
|
## 十、持续改进
|
||||||
|
|
||||||
|
本流程是一个持续更新和改进的流程。在使用过程中,如果发现以下情况,应该更新本文档:
|
||||||
|
|
||||||
|
1. 发现新的需要 i18n 的表
|
||||||
|
2. 发现新的需要同步字段的表(如订单表字段同步规则)
|
||||||
|
3. 发现新的命名规范或格式要求
|
||||||
|
4. 发现流程中的问题或改进点
|
||||||
|
5. 添加新的模板或工具
|
||||||
|
|
||||||
|
## 十一、使用示例
|
||||||
|
|
||||||
|
### 示例 1: 添加系统代码
|
||||||
|
|
||||||
|
**用户输入:**
|
||||||
|
```
|
||||||
|
我需要添加一个新的系统代码:
|
||||||
|
- Code_type: OrderType
|
||||||
|
- Code_value: 016
|
||||||
|
- Remark: Market Order (HK)
|
||||||
|
- Sort: 17
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cursor 处理:**
|
||||||
|
1. 查询最大ID:假设当前最大ID是 404
|
||||||
|
2. 生成新ID:405
|
||||||
|
3. 生成 i18n_id:CODE-0405
|
||||||
|
4. 生成多语言描述:
|
||||||
|
- en: Market Order (HK)
|
||||||
|
- sc: 市价单(香港)
|
||||||
|
- tc: 市價單(香港)
|
||||||
|
- vn: (可选)
|
||||||
|
5. 生成 SQL 并展示
|
||||||
|
6. 用户确认后执行
|
||||||
|
7. 执行成功后询问是否提交到 Git
|
||||||
|
8. 如果确认,保存文件并推送到 Git
|
||||||
|
|
||||||
|
### 示例 2: 插入系统路由
|
||||||
|
|
||||||
|
**用户输入:**
|
||||||
|
```
|
||||||
|
我需要添加一个新的系统路由:
|
||||||
|
- Route: /trade/order/cancel
|
||||||
|
- Route_type: USER
|
||||||
|
- Remark: 取消订单
|
||||||
|
- Title_remark: 订单
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cursor 处理:**
|
||||||
|
1. 使用 `insert_system_route_procedure.sql` 模板
|
||||||
|
2. 生成 SQL:
|
||||||
|
```sql
|
||||||
|
CALL InsertSystemRoute('取消订单', '/trade/order/cancel', '订单', 'USER');
|
||||||
|
```
|
||||||
|
3. 执行并提交到 Git
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. 常用 SQL 查询
|
||||||
|
|
||||||
|
**获取最大ID:**
|
||||||
|
```sql
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 FROM g3fo_base.m_system_code;
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查记录是否存在:**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) FROM g3fo_base.m_system_code
|
||||||
|
WHERE code_type = '{code_type}' AND code_value = '{code_value}';
|
||||||
|
```
|
||||||
|
|
||||||
|
**获取当前版本号:**
|
||||||
|
```sql
|
||||||
|
SELECT param_value FROM g3fo_base.m_system_setting
|
||||||
|
WHERE param_name = 'base.systemVersion';
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. 多语言描述生成规则
|
||||||
|
|
||||||
|
1. **如果 remark 包含中英文:**
|
||||||
|
- 提取英文部分作为 `en_description`
|
||||||
|
- 提取中文部分作为 `sc_description`
|
||||||
|
- 转换简体为繁体作为 `tc_description`
|
||||||
|
|
||||||
|
2. **如果 remark 只有中文:**
|
||||||
|
- 使用 remark 作为 `sc_description` 和 `tc_description`
|
||||||
|
- 询问用户英文描述,或使用拼音/音译
|
||||||
|
|
||||||
|
3. **如果 remark 只有英文:**
|
||||||
|
- 使用 remark 作为 `en_description`
|
||||||
|
- 询问用户中文描述
|
||||||
|
|
||||||
|
### C. 联系和支持
|
||||||
|
|
||||||
|
如有问题或建议,请更新本文档或联系项目维护者。
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# SQL Site Naming Convention
|
||||||
|
|
||||||
|
本文档说明了项目中使用的 SQL 站点命名规则及其对应关系。
|
||||||
|
|
||||||
|
## 站点命名对照表
|
||||||
|
|
||||||
|
| 目录名称 | 对应站点 | IP地址 | 描述 |
|
||||||
|
|---------|---------------|--------|------|
|
||||||
|
| afe_ap | Alpha Site | | |
|
||||||
|
| dls_ap | DLS | | |
|
||||||
|
| hing_wai_ap | HingWai | | |
|
||||||
|
| vn-demo | VN Demo | | |
|
||||||
|
| vn-demo-local | VN Demo Local | | |
|
||||||
|
| 226 | UAT | | |
|
||||||
|
|
||||||
|
|
||||||
|
## 目录结构与执行机制
|
||||||
|
|
||||||
|
### update_service_release 目录结构
|
||||||
|
|
||||||
|
项目中的 SQL 文件按照以下结构组织:
|
||||||
|
|
||||||
|
```
|
||||||
|
update_service_release/
|
||||||
|
├── 版本号/ # 如 1.5.1.20
|
||||||
|
│ ├── sitename/ # 如 afe_ap, dls_ap 等
|
||||||
|
│ │ ├── patch/ # 补丁目录
|
||||||
|
│ │ │ └── 数字版本/ # 如 1, 2 等
|
||||||
|
│ │ │ └── *.sql # 补丁 SQL 文件
|
||||||
|
│ │ └── *.sql # 该站点特定的 SQL 文件
|
||||||
|
│ └── *.sql # 通用 SQL 文件
|
||||||
|
└── pending release x.x.x.x/ # 当前开发中的版本
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL 执行机制
|
||||||
|
|
||||||
|
1. **版本发布目录**:
|
||||||
|
- 每个版本号目录下包含适用于不同站点的 SQL 文件
|
||||||
|
- 站点特定的 SQL 文件放在对应的 `sitename` 子目录中
|
||||||
|
- 补丁文件放在对应站点的 `patch` 子目录中,按数字版本号分组
|
||||||
|
- 通用 SQL 文件直接放在版本号目录下
|
||||||
|
|
||||||
|
2. **当前开发版本**:
|
||||||
|
- `update_service_release/pending release x.x.x.x` 是当前正在开发的版本目录
|
||||||
|
- 每个版本下必须包含一个修改版本号的 SQL 文件:`set_version_01.sql`
|
||||||
|
- 该文件负责更新系统版本信息到 `m_system_setting` 表
|
||||||
|
|
||||||
|
3. **Jenkins 自动化执行**:
|
||||||
|
- 通过执行 `1-g3fo-db`/`g3fo-db-patch` 任务运行 SQL
|
||||||
|
- 大版本升级执行`1-g3fo-db`,只能执行一次
|
||||||
|
- 补丁执行`g3fo-db-patch`,可重复执行
|
||||||
|
- 系统会根据指定的站点和版本号执行对应目录下的 SQL 文件
|
||||||
|
- 默认执行版本号最大的 patch(可重复执行,保证幂等性)
|
||||||
|
|
||||||
|
### 版本号更新文件示例
|
||||||
|
|
||||||
|
每个版本必须包含 `set_version_01.sql` 文件,其内容如下(控实际情况修改版本号,以下是1.5.1.20版本示例):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '1.5.1.0 M20',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.systemVersion';
|
||||||
|
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '0',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE
|
||||||
|
`param_name` = 'base.patchNo';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命名一致性要求
|
||||||
|
|
||||||
|
为了确保自动化部署和执行流程的正常运行,`sitename` 目录命名必须与以下配置保持严格一致:
|
||||||
|
|
||||||
|
1. **Jenkins 配置**:
|
||||||
|
- Jenkins -> 系统管理 -> Publish over SSH -> SSH Server Name 中配置的服务名
|
||||||
|
- 例如:如果 Jenkins 中配置的 SSH Server Name 为 `afe_ap`,则目录名必须也为 `afe_ap`
|
||||||
|
|
||||||
|
2. **执行服务器配置**:
|
||||||
|
- 与执行 SQL 的服务器(Build 机或 support 机)上的 `/data/g3dbExecution/db_config.json` 文件中的 key 保持一致
|
||||||
|
- 该 JSON 文件中的 key 值必须与目录名称完全匹配
|
||||||
|
|
||||||
|
这种一致性确保了:
|
||||||
|
- Jenkins 能够正确地将 SQL 文件传输到目标服务器
|
||||||
|
- SQL 脚本能够在正确的环境中执行
|
||||||
|
- 部署流程能够自动化进行而不会出现配置错误
|
||||||
|
|
||||||
|
## 添加新环境
|
||||||
|
|
||||||
|
当需要添加新的部署环境时,请遵循以下命名规则:
|
||||||
|
- 生产环境:使用客户名称的缩写加 `_ap` 后缀
|
||||||
|
- 测试环境:使用环境用途加版本号或标识符的形式
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有命名应保持简洁明了
|
||||||
|
- 同一类环境应保持命名风格一致
|
||||||
|
- 添加新环境后应及时更新此文档
|
||||||
|
- 修改现有环境名称前需确认关联的所有配置均已同步更新
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `g3fo_trade`.`m_trading_planned_order` (
|
||||||
|
`creator` CHAR(36) NOT NULL,
|
||||||
|
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
`planned_order_id` VARCHAR(40) NOT NULL,
|
||||||
|
`strategy_id` CHAR(36) NOT NULL,
|
||||||
|
`strategy_name` CHAR(64) NOT NULL,
|
||||||
|
`account_type` VARCHAR(3) NOT NULL,
|
||||||
|
`account_id` VARCHAR(20) NOT NULL,
|
||||||
|
`trading_day` DATE NOT NULL,
|
||||||
|
`order_dt` DATETIME(6) NULL DEFAULT NULL,
|
||||||
|
`exchange` VARCHAR(8) NOT NULL,
|
||||||
|
`instrument` VARCHAR(64) NOT NULL,
|
||||||
|
`name` VARCHAR(64) NOT NULL,
|
||||||
|
`direction` VARCHAR(1) NOT NULL,
|
||||||
|
`offset_flag` VARCHAR(4) NOT NULL,
|
||||||
|
`original_order_qty` INT NOT NULL DEFAULT 0,
|
||||||
|
`order_qty` INT NOT NULL DEFAULT 0,
|
||||||
|
`order_price` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`order_type` VARCHAR(1) NOT NULL,
|
||||||
|
`order_status` SMALLINT NOT NULL,
|
||||||
|
`status_msg` VARCHAR(64) NOT NULL,
|
||||||
|
`order_params` JSON NULL DEFAULT NULL,
|
||||||
|
`order_placed_dt` DATETIME(6) NULL DEFAULT NULL,
|
||||||
|
`order_key` VARCHAR(32) NULL DEFAULT '',
|
||||||
|
`entrust_no` VARCHAR(32) NULL DEFAULT '',
|
||||||
|
`algo_order_id` BIGINT NULL DEFAULT 0,
|
||||||
|
`stop_profit_price` DOUBLE NULL DEFAULT 0,
|
||||||
|
`stop_loss_price` DOUBLE NULL DEFAULT 0,
|
||||||
|
|
||||||
|
`system_order_count` BIGINT NULL,
|
||||||
|
`system_account_id` BIGINT NULL,
|
||||||
|
`modified_qty` INT NULL DEFAULT 0,
|
||||||
|
`modified_price` DOUBLE NULL DEFAULT 0,
|
||||||
|
`system_user_id` BIGINT NULL,
|
||||||
|
`execute_status` CHAR(4) NULL DEFAULT '0',
|
||||||
|
`eng_name` CHAR(128) NULL,
|
||||||
|
`local_name` CHAR(128) NULL,
|
||||||
|
`sc_name` CHAR(128) NULL,
|
||||||
|
|
||||||
|
`create_on` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`create_by` VARCHAR(32) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci' NULL DEFAULT NULL,
|
||||||
|
`update_on` TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
`update_by` VARCHAR(32) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_0900_ai_ci' NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`planned_order_id`),
|
||||||
|
INDEX `idx_strategy_id` (`strategy_id`),
|
||||||
|
INDEX `idx_user_id` (`system_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||||
|
COMMENT='quant strategy order plan';
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Hing Wai AP 清理与初始化脚本
|
||||||
|
-- 将原先分散的脚本合并,按模块分组执行
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- g3fo_trade 历史与当日数据清理
|
||||||
|
-- ===============================
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`h_order`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`h_order_actions`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`h_strategy_daily_profit`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`h_trade`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_order`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_order_actions`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_order_approve_detail`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_strategy_daily_profit`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_trade`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_trading_planned_order`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_user_account_chunk`;
|
||||||
|
-- TRUNCATE TABLE `g3fo_trade`.`m_user_account_map`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_user_api_binding`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_user_approve_order_record`;
|
||||||
|
TRUNCATE TABLE `g3fo_trade`.`m_user_credit_usage_sum`;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- g3fo_dx 相关表清理
|
||||||
|
-- ===============================
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_async_task`;
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_async_task_history`;
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_dx_message_in_history`;
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_dx_message_out_history`;
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_dx_session`;
|
||||||
|
TRUNCATE TABLE `g3fo_dx`.`m_import_history`;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 日志表清理
|
||||||
|
-- ===============================
|
||||||
|
TRUNCATE TABLE `g3fo_admin`.`m_report_export_record`;
|
||||||
|
TRUNCATE TABLE `g3fo_admin`.`m_table_data_change_log`;
|
||||||
|
TRUNCATE TABLE `g3fo_base`.`m_fix_message_log`;
|
||||||
|
TRUNCATE TABLE `nacos`.`his_config_info`;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- g3fo_user 账户、客户与用户相关表清理
|
||||||
|
-- ===============================
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_bank`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_bcan_history`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_black_white_list`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_cash_hold`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_cash_movement`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_cash_open`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_company_charge`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_order_hold`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_product_hold`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_product_margin`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_product_movement`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_product_open`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_account_trade_license`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_alert_message`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_aml`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_authorized_person`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_black_white_list`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_broker_mapping`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_client`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_client_account_relations`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_client_hot_list`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_group`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_group_item`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_settled_trade`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_settled_trade_charge`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_settled_trade_dtl`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_settled_trade_qty`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_settled_trade_rebate`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_change_password`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_client_relations`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_login`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_login_history`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_role_relation_def`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`m_user_trade_license`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`t_user_account_ref`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`t_user_child_user_ref`;
|
||||||
|
TRUNCATE TABLE `g3fo_user`.`t_user_client_ref`;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- g3fo_admin 管理员相关表清理
|
||||||
|
-- ===============================
|
||||||
|
TRUNCATE TABLE `g3fo_admin`.`m_admin`;
|
||||||
|
TRUNCATE TABLE `g3fo_admin`.`m_admin_login`;
|
||||||
|
TRUNCATE TABLE `g3fo_admin`.`m_admin_role_relation_def`;
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 初始化 g3fo_user 账户与客户数据
|
||||||
|
-- ===============================
|
||||||
|
REPLACE INTO `g3fo_user`.`m_account` (`id`, `account_type`, `account_code`, `account_class`, `account_status`, `local_name`, `eng_name`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `margin_pct`, `cover_pct`, `margin_limit`, `risk_agree`, `professional_investor_flag`, `aml_risk_level`, `client_risk_level`, `ae_id`, `parent_id`, `staff_account_flag`, `black_white_list_code`, `alert_pro_account`, `promotion_flag`, `hkidr_consent_flag`, `hkidr_consent_date`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `virtual_asset_assessment_date`, `by_pass_check`, `create_by`, `create_on`, `update_by`, `update_on`, `member_type`, `default_ap_id`, `bank_gateway`, `bg_cia_flg`, `bg_offline_flg`, `online_cia_flg`, `online_monthly_rpt_flg`, `online_rights_sub_flg`, `online_stock_transfer_flg`, `online_trade_status`, `online_withdrawal`, `oct_flg`, `acct_grp`, `cash_limit`, `credit_limit`, `margin_call_pct`, `max_daily_transfer_limit`, `max_margin_flg`, `mdar`, `used_allocated_credit`, `used_credit`, `allocated_credit`, `bfld`, `bfld_discount`, `over_limit_proc`, `stop_buy_pct`, `stop_withdrawal_pct`, `concentration_risk`, `confidential`, `force_sell_flg`, `force_sell_note`, `force_sell_pct`, `force_sell_period`, `day_to_force_sell`, `t_plus_flg`, `t_plus_term`, `email_statement`, `close_date`, `phone_code`)
|
||||||
|
VALUES (1, 'CASH', 'afe', 'CASH', 'OPEN', 'AFE', 'afe.hk', 999999999.99, 999999999.99, 999999999.99, 999999999.99, 5.000000, 6.000000, 7.00, 'EQUY,BOND,WART', '1', NULL, NULL, NULL, '', '1', '', '', 1, 1, '2024-06-12 00:00:00', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', NULL, NULL, '', '', '', '', 'afe@afe.hk', '', '', '', '', NULL, 0, 'sys', '2024-05-15 11:32:52', 'sys', '2025-02-24 17:48:02', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_account` (`id`, `account_type`, `account_code`, `account_class`, `account_status`, `local_name`, `eng_name`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `margin_pct`, `cover_pct`, `margin_limit`, `risk_agree`, `professional_investor_flag`, `aml_risk_level`, `client_risk_level`, `ae_id`, `parent_id`, `staff_account_flag`, `black_white_list_code`, `alert_pro_account`, `promotion_flag`, `hkidr_consent_flag`, `hkidr_consent_date`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `virtual_asset_assessment_date`, `by_pass_check`, `create_by`, `create_on`, `update_by`, `update_on`, `member_type`, `default_ap_id`, `bank_gateway`, `bg_cia_flg`, `bg_offline_flg`, `online_cia_flg`, `online_monthly_rpt_flg`, `online_rights_sub_flg`, `online_stock_transfer_flg`, `online_trade_status`, `online_withdrawal`, `oct_flg`, `acct_grp`, `cash_limit`, `credit_limit`, `margin_call_pct`, `max_daily_transfer_limit`, `max_margin_flg`, `mdar`, `used_allocated_credit`, `used_credit`, `allocated_credit`, `bfld`, `bfld_discount`, `over_limit_proc`, `stop_buy_pct`, `stop_withdrawal_pct`, `concentration_risk`, `confidential`, `force_sell_flg`, `force_sell_note`, `force_sell_pct`, `force_sell_period`, `day_to_force_sell`, `t_plus_flg`, `t_plus_term`, `email_statement`, `close_date`, `phone_code`)
|
||||||
|
VALUES (1943513637545185280, 'CASH', 'APPSTORE1', 'CUST', 'OPEN', 'APPSTORE1', 'APPSTORE1', 0.00, 0.00, 0.00, 0.00, 0.000000, 0.000000, 999999999999999.99, 'EQUY,BOND,TRUT', '0', NULL, NULL, 'C05', '', '0', '', '', 0, 1, '2025-04-03 00:00:00', '', '', '', '', '', '', '紅磡德豐街 18 號海濱廣場一期19樓1907B-1910A室', '', '', '', '', '', '', '', '', NULL, NULL, '', '', 'HK', '', '', '', '', '', '', NULL, 0, 'sys', '2025-07-11 11:32:15', 'DX', '2025-08-22 13:52:44', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_account` (`id`, `account_type`, `account_code`, `account_class`, `account_status`, `local_name`, `eng_name`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `margin_pct`, `cover_pct`, `margin_limit`, `risk_agree`, `professional_investor_flag`, `aml_risk_level`, `client_risk_level`, `ae_id`, `parent_id`, `staff_account_flag`, `black_white_list_code`, `alert_pro_account`, `promotion_flag`, `hkidr_consent_flag`, `hkidr_consent_date`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `virtual_asset_assessment_date`, `by_pass_check`, `create_by`, `create_on`, `update_by`, `update_on`, `member_type`, `default_ap_id`, `bank_gateway`, `bg_cia_flg`, `bg_offline_flg`, `online_cia_flg`, `online_monthly_rpt_flg`, `online_rights_sub_flg`, `online_stock_transfer_flg`, `online_trade_status`, `online_withdrawal`, `oct_flg`, `acct_grp`, `cash_limit`, `credit_limit`, `margin_call_pct`, `max_daily_transfer_limit`, `max_margin_flg`, `mdar`, `used_allocated_credit`, `used_credit`, `allocated_credit`, `bfld`, `bfld_discount`, `over_limit_proc`, `stop_buy_pct`, `stop_withdrawal_pct`, `concentration_risk`, `confidential`, `force_sell_flg`, `force_sell_note`, `force_sell_pct`, `force_sell_period`, `day_to_force_sell`, `t_plus_flg`, `t_plus_term`, `email_statement`, `close_date`, `phone_code`) VALUES (1980186019865169920, 'CASH', 'HOUSE_ACCOUNT', 'CASH', 'OPEN', '', 'HOUSE_ACCOUNT', 99999999999.00, 99999999999.00, 99999999999.00, 99999999999.00, 5.000000, 6.000000, 7.00, 'WART,BOND,EQUY,TRUT', '1', NULL, NULL, NULL, '', '0', '', '', 0, 1, '2025-10-20 00:00:00', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', NULL, NULL, '', '', '', '', '', '', '', '', '', NULL, 0, 'afe', '2025-10-20 16:15:12', 'afe', '2025-10-20 16:27:38', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_account_trade_license` (`id`, `account_id`, `account_code`, `exchange_code`, `market_ids`, `account_status`, `risk_rule_group_code`, `over_limit_proc`, `clear_type`, `base_currency_code`, `clear_currency_code`, `payee`, `beneficiary`, `last_txn_date`, `last_trader_id`, `acct_terminated_date`, `single_order_limit`, `daily_limit`, `margin_pct`, `multiplier`, `recall_time`, `investor_id`, `bcan`, `bcan_validated_flag`, `ttep_flag`, `bcan_range_from`, `bcan_range_to`, `bcan_range_validated_list`, `create_by`, `create_on`, `update_by`, `update_on`, `smp_id`, `short_sell_flag`, `broker_ids`, `risk_class`, `acct_class`)
|
||||||
|
VALUES (1943513736174243840, 1943513637545185280, 'APPSTORE1', 'SEHK', '1,7,8,9', 'OPEN', 'CASH', 'OVER', 'T', 'HKD', 'HKD', '', '', NULL, '', NULL, 0.00, 0.00, 0.000000, NULL, NULL, '', 'ABL962.2266', 0, 0, '', '', '', 'sys', '2025-07-11 11:32:38', 'DX', '2025-08-22 13:47:50', '', 0, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `g3fo_user`.`m_account_trade_license` (`id`, `account_id`, `account_code`, `exchange_code`, `market_ids`, `account_status`, `risk_rule_group_code`, `over_limit_proc`, `clear_type`, `base_currency_code`, `clear_currency_code`, `payee`, `beneficiary`, `last_txn_date`, `last_trader_id`, `acct_terminated_date`, `single_order_limit`, `daily_limit`, `margin_pct`, `multiplier`, `recall_time`, `investor_id`, `bcan`, `bcan_validated_flag`, `ttep_flag`, `bcan_range_from`, `bcan_range_to`, `bcan_range_validated_list`, `create_by`, `create_on`, `update_by`, `update_on`, `smp_id`, `short_sell_flag`, `broker_ids`, `risk_class`, `acct_class`) VALUES (1980186084428091392, 1980186019865169920, 'HOUSE_ACCOUNT', 'SEHK', '1,7,8,9', 'OPEN', 'CASH', 'OVER', 'T', 'HKD', 'HKD', '', '', NULL, '', NULL, 0.00, 0.00, 0.000000, NULL, NULL, '', '', 0, 0, '', '', '', 'afe', '2025-10-20 16:15:27', 'afe', '2025-10-20 21:36:47', '', 1, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client` (`id`, `client_code`, `local_name`, `eng_name`, `group_code`, `branch_code`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `notify_lang`, `trade_notify_method`, `price_alert_notify_method`, `system_msg_notify_method`, `news_notify_method`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `owner_user_id`, `create_by`, `create_on`, `update_by`, `update_on`, `custody_id`, `bank_gateway`, `client_signature`, `omnibus_acc_flg`, `sms_flg`, `id_issue_date`, `id_issue_place`, `id_type`, `trading_code`, `trading_code_issue_date`, `residence_expiry_date`)
|
||||||
|
VALUES (1943513539612381184, 'APPSTORE1', 'APPSTORE1', 'APPSTORE1', 'COMPANY-GROUP', 'DEF', 0.00, 0.00, 0.00, 0.00, '', '', '', NULL, NULL, 'MOBI', '', '', '', '', '', '紅磡德豐街 18 號海濱廣場一期19樓1907B-1910A室', '', '', '', '', '', '', '', '', NULL, 'M', '', '', 'Hong Kong', 'HK', '', '', NULL, NULL, NULL, NULL, 'sys', '2025-07-11 11:31:51', 'sys', '2025-08-22 13:58:02', 'APPSTORE1', NULL, '', 0, NULL, NULL, 'HK', 'I', NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client` (`id`, `client_code`, `local_name`, `eng_name`, `group_code`, `branch_code`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `notify_lang`, `trade_notify_method`, `price_alert_notify_method`, `system_msg_notify_method`, `news_notify_method`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `owner_user_id`, `create_by`, `create_on`, `update_by`, `update_on`, `custody_id`, `bank_gateway`, `client_signature`, `omnibus_acc_flg`, `sms_flg`, `id_issue_date`, `id_issue_place`, `id_type`, `trading_code`, `trading_code_issue_date`, `residence_expiry_date`)
|
||||||
|
VALUES (1, 'afe', 'AFE', 'afe.hk', NULL, '001', 0.00, 0.00, 0.00, 0.00, '1111', '2222', '3333', '4444', NULL, 'EMAL', NULL, NULL, NULL, '888888888', '999999999', 'j11111111', 'j22222222', 'j333333333', 'j4444444', 'b11111111', 'b2222222', 'b333333333', 'b44444444', '11111111', NULL, NULL, '', '', '', '', 'man.zhong@afe.hk', '', '', '', '', NULL, 'admin', '2023-09-12 14:53:46', 'sys', '2025-03-19 15:51:22', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client` (`id`, `client_code`, `local_name`, `eng_name`, `group_code`, `branch_code`, `single_order_limit`, `daily_limit`, `cny_single_order_limit`, `cny_daily_limit`, `tel_no_home`, `tel_no_office`, `tel_no_mobile`, `tel_no_sms`, `notify_lang`, `trade_notify_method`, `price_alert_notify_method`, `system_msg_notify_method`, `news_notify_method`, `fax_no_home`, `fax_no_office`, `address1_home`, `address2_home`, `address3_home`, `address4_home`, `address1_office`, `address2_office`, `address3_office`, `address4_office`, `area_code`, `birth_date`, `sex`, `occupation`, `referee`, `nationality`, `country_code`, `email`, `remark`, `buy_side_id`, `attention`, `id_card`, `owner_user_id`, `create_by`, `create_on`, `update_by`, `update_on`, `custody_id`, `bank_gateway`, `client_signature`, `omnibus_acc_flg`, `sms_flg`, `id_issue_date`, `id_issue_place`, `id_type`, `trading_code`, `trading_code_issue_date`, `residence_expiry_date`) VALUES (1980185872708014080, 'HOUSE_ACCOUNT', 'HOUSE_ACCOUNT', 'HOUSE_ACCOUNT', 'COMPANY-GROUP', 'DEFAULT', 0.00, 0.00, 0.00, 0.00, '', '', '', '', NULL, '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', NULL, 'M', '', '', '', 'AU', 'leon@e-hingwai.com', '', '', '', '', NULL, 'afe', '2025-10-20 16:14:37', 'afe', '2025-10-20 16:26:40', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client_account_relations` (`id`, `client_id`, `client_code`, `account_id`, `account_code`, `account_type`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1, 1, 'afe', 1, 'afe', 'CASH', 'sys', '2025-02-24 17:52:23', NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client_account_relations` (`id`, `client_id`, `client_code`, `account_id`, `account_code`, `account_type`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1958767774625042432, 1943513539612381184, 'APPSTORE1', 1943513637545185280, 'APPSTORE1', 'CASH', 'DX', '2025-08-22 13:46:44', 'sys', '2025-08-22 13:46:44');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_client_account_relations` (`id`, `client_id`, `client_code`, `account_id`, `account_code`, `account_type`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES (1980189147855458304, 1980185872708014080, 'HOUSE_ACCOUNT', 1980186019865169920, 'HOUSE_ACCOUNT', 'CASH', 'afe', '2025-10-20 16:27:38', 'afe', '2025-10-20 16:27:38');
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_group` (`id`, `group_code`, `group_type`, `local_name`, `eng_name`, `description`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1, 'COMPANY-GROUP', 'CG', '公司组', 'COMPANY-GROUP', NULL, 'sql', '2025-05-07 10:56:09', NULL, NULL);
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_group_item` (`id`, `group_type`, `group_id`, `group_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1943513539805319168, 'CG', 1, 'COMPANY-GROUP', 'C', 1943513539612381184, 'APPSTORE1', 1, 'sys', '2025-07-11 11:31:51', 'sys', '2025-07-11 11:31:51');
|
||||||
|
INSERT INTO `g3fo_user`.`m_group_item` (`id`, `group_type`, `group_id`, `group_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES (1980188905231749120, 'CG', 1, 'COMPANY-GROUP', 'C', 1980185872708014080, 'HOUSE_ACCOUNT', 1, 'afe', '2025-10-20 16:26:40', 'afe', '2025-10-20 16:26:40');
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 初始化 g3fo_user 用户与交易权限
|
||||||
|
-- ===============================
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `create_by`, `create_on`, `update_by`, `update_on`, `trade_oddlot_flag`, `max_client_pct`, `single_override_limit_pct`, `max_deduct_pct`, `single_order_limit`, `single_override_limit`, `daily_override_limit`, `exposure_limit`, `cny_max_client_pct`, `cny_single_override_limit_pct`, `cny_single_order_limit`, `cny_single_override_limit`, `cny_daily_override_limit`, `otp_logon`, `otp_chgpwd`, `otp_method`, `credit_approval_role`, `effective_date`, `passport_id`, `delete_flag`, `delete_time`, `notification_opt`, `notification_method`, `change_password_flag`, `soft_key`, `reset_pass`, `mobile_device_id`, `mobile_platform_id`, `mobile_push_token`)
|
||||||
|
VALUES (1943512935934595072, 'APPSTORE1', 'APPSTORE1', 'APPSTORE1', 'CL', '0', '', 'DEFAULT', '5', '', 'man.zhong@afe.hk', 'sys', '2025-07-11 11:29:27', 'sys', '2025-10-14 13:26:59', 0, 20.000000, 100.000000, 0.000000, NULL, NULL, NULL, 0.00000000, 20.000000, 100.000000, NULL, NULL, NULL, 'DISD', 'DISD', 'SMS', 'NONE', '2025-07-11 11:28:46', '', 0, NULL, 'DISD', 'MOBI', 0, NULL, 'DISD', NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `create_by`, `create_on`, `update_by`, `update_on`, `trade_oddlot_flag`, `max_client_pct`, `single_override_limit_pct`, `max_deduct_pct`, `single_order_limit`, `single_override_limit`, `daily_override_limit`, `exposure_limit`, `cny_max_client_pct`, `cny_single_override_limit_pct`, `cny_single_order_limit`, `cny_single_override_limit`, `cny_daily_override_limit`, `otp_logon`, `otp_chgpwd`, `otp_method`, `credit_approval_role`, `effective_date`, `passport_id`, `delete_flag`, `delete_time`, `notification_opt`, `notification_method`, `change_password_flag`, `soft_key`, `reset_pass`, `mobile_device_id`, `mobile_platform_id`, `mobile_push_token`)
|
||||||
|
VALUES (1925469397361799170, 'afe', 'afe', 'afe', 'AE', '0', '', 'DEFAULT', '1', '', 'john.kong@afe.hk', 'sys', '2025-05-22 16:30:52', 'opr', '2025-10-14 13:26:59', 0, 20.000000, 100.000000, 0.000000, NULL, NULL, NULL, 0.00000000, 20.000000, 100.000000, NULL, NULL, NULL, 'DISD', 'ENG', 'NON', 'NONE', '2025-05-22 16:30:28', '', 0, NULL, 'TCHI', 'EMAL', 0, NULL, 'ENG', NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `create_by`, `create_on`, `update_by`, `update_on`, `trade_oddlot_flag`, `max_client_pct`, `single_override_limit_pct`, `max_deduct_pct`, `single_order_limit`, `single_override_limit`, `daily_override_limit`, `exposure_limit`, `cny_max_client_pct`, `cny_single_override_limit_pct`, `cny_single_order_limit`, `cny_single_override_limit`, `cny_daily_override_limit`, `otp_logon`, `otp_chgpwd`, `otp_method`, `credit_approval_role`, `effective_date`, `passport_id`, `delete_flag`, `delete_time`, `notification_opt`, `notification_method`, `change_password_flag`, `soft_key`, `reset_pass`, `mobile_device_id`, `mobile_platform_id`, `mobile_push_token`) VALUES (1978367120546861056, 'g2sfsupport', 'g2sfsupport', 'g2sfsupport', 'AE', '0', '', 'DEFAULT', '1', '6800-8690', 'g2sfsupport@afe.hk', 'afe', '2025-10-15 15:47:32', 'sys', '2025-10-18 11:20:29', 0, 20.000000, 100.000000, 0.000000, NULL, NULL, NULL, 0.00000000, 20.000000, 100.000000, NULL, NULL, NULL, 'DISD', 'DISD', 'NON', 'NONE', '2025-10-15 15:46:45', '', 0, NULL, 'DISD', 'EMAL', 0, NULL, 'DISD', NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `create_by`, `create_on`, `update_by`, `update_on`, `trade_oddlot_flag`, `max_client_pct`, `single_override_limit_pct`, `max_deduct_pct`, `single_order_limit`, `single_override_limit`, `daily_override_limit`, `exposure_limit`, `cny_max_client_pct`, `cny_single_override_limit_pct`, `cny_single_order_limit`, `cny_single_override_limit`, `cny_daily_override_limit`, `otp_logon`, `otp_chgpwd`, `otp_method`, `credit_approval_role`, `effective_date`, `passport_id`, `delete_flag`, `delete_time`, `notification_opt`, `notification_method`, `change_password_flag`, `soft_key`, `reset_pass`, `mobile_device_id`, `mobile_platform_id`, `mobile_push_token`) VALUES (1980073989309075456, 'g3sfsupport', 'g3sfsupport', 'g3sfsupport', 'AE', '0', NULL, 'DEFAULT', '1', NULL, 'g2sfsupport@afe.hk', 'afe', '2025-10-20 08:50:02', 'afe', '2025-10-20 08:49:12', 0, 20.000000, 100.000000, 0.000000, NULL, NULL, NULL, 0.00000000, 20.000000, 100.000000, NULL, NULL, NULL, 'DISD', 'DISD', 'NON', 'NONE', '2025-10-20 08:49:12', NULL, 0, NULL, 'DISD', NULL, 0, NULL, 'DISD', NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `create_by`, `create_on`, `update_by`, `update_on`, `trade_oddlot_flag`, `max_client_pct`, `single_override_limit_pct`, `max_deduct_pct`, `single_order_limit`, `single_override_limit`, `daily_override_limit`, `exposure_limit`, `cny_max_client_pct`, `cny_single_override_limit_pct`, `cny_single_order_limit`, `cny_single_override_limit`, `cny_daily_override_limit`, `otp_logon`, `otp_chgpwd`, `otp_method`, `credit_approval_role`, `effective_date`, `passport_id`, `delete_flag`, `delete_time`, `notification_opt`, `notification_method`, `change_password_flag`, `soft_key`, `reset_pass`, `mobile_device_id`, `mobile_platform_id`, `mobile_push_token`) VALUES (1980074724268576768, 'szg3sfsupport', 'szg3sfsupport', 'szg3sfsupport', 'AE', '0', NULL, 'DEFAULT', '1', NULL, 'g2sfsupport@afe.hk', 'afe', '2025-10-20 08:52:57', 'afe', '2025-10-20 08:52:22', 0, 20.000000, 100.000000, 0.000000, NULL, NULL, NULL, 0.00000000, 20.000000, 100.000000, NULL, NULL, NULL, 'DISD', 'DISD', 'NON', 'NONE', '2025-10-20 08:52:22', NULL, 0, NULL, 'DISD', 'EMAL', 0, NULL, 'DISD', NULL, NULL, NULL);
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_client_relations` (`id`, `parent_type`, `parent_user_id`, `parent_user_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1925478879873642498, 'U', 1925469397361799170, 'afe', 'C', 1, 'afe', 1, 'sys', '2025-05-22 17:08:33', 'sys', '2025-05-22 17:08:33');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_client_relations` (`id`, `parent_type`, `parent_user_id`, `parent_user_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1943514529233244160, 'U', 1943512935934595072, 'APPSTORE1', 'C', 1943513539612381184, 'APPSTORE1', 1, 'sys', '2025-07-11 11:35:47', 'sys', '2025-07-11 11:35:47');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_client_relations` (`id`, `parent_type`, `parent_user_id`, `parent_user_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES (1980091573093928960, 'U', 1980074724268576768, 'szg3sfsupport', 'CG', 1, 'COMPANY-GROUP', 1, 'sys', '2025-10-20 09:59:54', 'sys', '2025-10-20 09:59:54');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_client_relations` (`id`, `parent_type`, `parent_user_id`, `parent_user_code`, `child_type`, `child_id`, `child_code`, `enable_flag`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES (1980116781104238592, 'U', 1980073989309075456, 'g3sfsupport', 'U', 1978367120546861056, 'g2sfsupport', 1, 'opr', '2025-10-20 11:40:04', 'opr', '2025-10-20 11:40:04');
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_trade_license` (`id`, `user_id`, `exchange_code`, `market_ids`, `active_flag`, `create_on`, `create_by`, `update_on`, `update_by`)
|
||||||
|
VALUES (1925469430924619778, 1925469397361799170, 'SEHK', '1,7,8,9', 1, '2025-05-22 16:31:00', 'sys', '2025-05-22 16:31:00', 'sys');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_trade_license` (`id`, `user_id`, `exchange_code`, `market_ids`, `active_flag`, `create_on`, `create_by`, `update_on`, `update_by`)
|
||||||
|
VALUES (1943512964262924288, 1943512935934595072, 'SEHK', '1,7,8,9', 1, '2025-07-11 11:29:34', 'sys', '2025-07-11 11:29:34', 'sys');
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`t_user_account_ref` (`user_id`, `account_id`, `create_on`)
|
||||||
|
VALUES (1925469397361799170, 1, '2025-09-03 16:40:50');
|
||||||
|
REPLACE INTO `g3fo_user`.`t_user_child_user_ref` (`user_id`, `child_user_id`, `create_on`)
|
||||||
|
VALUES (1925469397361799170, 1925469397361799170, '2025-09-03 16:40:50');
|
||||||
|
REPLACE INTO `g3fo_user`.`t_user_client_ref` (`user_id`, `client_id`, `create_on`)
|
||||||
|
VALUES (1925469397361799170, 1, '2025-09-03 16:40:50');
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 初始化 g3fo_user 登录与角色
|
||||||
|
-- ===============================
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1943513267687264256, 1943512935934595072, 'APPSTORE1', 'WS', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2026-01-08 14:58:12', NULL, 'sys', '2025-07-11 11:30:46', 'sys', '2025-10-15 09:30:18', 0, NULL, 1, '', '2025-12-09 14:58:12', NULL, '2025-10-10 14:58:12', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1962711560971489280, 1943512935934595072, 'APPSTORE1', 'IH', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2026-01-08 14:58:12', NULL, 'L_TANG', '2025-09-02 10:57:56', 'afe', '2025-10-13 17:29:37', 0, NULL, 1, '443', '2025-12-09 14:58:12', NULL, '2025-10-10 14:58:12', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1925469490802503681, 1925469397361799170, 'afe', 'IN', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2025-11-23 11:38:06', NULL, 'sys', '2025-05-22 16:31:15', 'sys', '2025-08-25 11:38:05', 0, NULL, 1, NULL, '2025-10-24 11:38:06', NULL, '2025-08-25 11:38:06', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1925470040684146690, 1925469397361799170, 'afe', 'PT', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2025-11-23 11:38:06', NULL, 'sys', '2025-05-22 16:33:26', 'afe', '2025-08-25 11:38:05', 0, NULL, 1, '', '2025-10-24 11:38:06', NULL, '2025-08-25 11:38:06', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1978367310318145536, 1978367120546861056, 'g2sfsupport', 'PT', NULL, '16AFD883D4C47276FB9410E521A1B227DA0D8F5FE02D0C3E54145D9815FCBE11', 0, '2026-01-16 09:57:30', NULL, 'afe', '2025-10-15 15:48:18', 'afe', '2025-11-03 16:16:43', 0, NULL, 1, '', '2025-12-17 09:57:30', NULL, '2025-10-18 09:57:30', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1980074162877763584, 1980073989309075456, 'g3sfsupport', 'PT', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2026-01-18 08:50:43', NULL, 'afe', '2025-10-20 08:50:43', 'afe', '2025-10-21 15:13:28', 0, NULL, 1, '', '2025-12-19 08:50:43', NULL, '2025-10-20 08:50:43', NULL);
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_login` (`id`, `user_id`, `user_code`, `channel_code`, `login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`, `suspense_flag`, `enable_dt_info_flag`, `mult_session_flag`, `info_package_id`, `next_remind_date`, `remind_day`, `change_pwd_date`, `token`)
|
||||||
|
VALUES (1980074798025412608, 1980074724268576768, 'szg3sfsupport', 'PT', NULL, 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2026-01-18 08:53:15', NULL, 'afe', '2025-10-20 08:53:15', 'afe', '2025-10-20 08:53:14', 0, NULL, 1, '', '2025-12-19 08:53:15', NULL, '2025-10-20 08:53:15', NULL);
|
||||||
|
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1943512984248782848, 1943512935934595072, 'APPSTORE1', 9, 1, 'sys', '2025-07-11 11:29:39', 'sys', '2025-07-11 11:29:39');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1962711775682105344, 1943512935934595072, 'APPSTORE1', 10, 1, 'L_TANG', '2025-09-02 10:58:47', 'L_TANG', '2025-09-02 10:58:47');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1925469448863657985, 1925469397361799170, 'afe', 1, 1, 'sys', '2025-05-22 16:31:05', 'sys', '2025-05-22 16:31:05');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1959823319108816896, 1925469397361799170, 'afe', 2, 1, 'afe', '2025-08-25 11:41:06', 'afe', '2025-08-25 11:41:06');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1978734167583756288, 1978367120546861056, 'g2sfsupport', 1, 1, 'sys', '2025-10-16 16:06:03', 'sys', '2025-10-16 16:06:03');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1980074075913064448, 1980073989309075456, 'g3sfsupport', 1, 1, 'afe', '2025-10-20 08:50:22', 'afe', '2025-10-20 08:50:22');
|
||||||
|
REPLACE INTO `g3fo_user`.`m_user_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1980074746255118336, 1980074724268576768, 'szg3sfsupport', 1, 1, 'afe', '2025-10-20 08:53:02', 'afe', '2025-10-20 08:53:02');
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 初始化 g3fo_admin 管理员
|
||||||
|
-- ===============================
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `expiry_date`, `create_by`, `create_on`, `update_by`, `update_on`, `otp_logon`, `otp_method`, `soft_key`, `passport_id`)
|
||||||
|
VALUES (1, 'sys', 'sys', 'sys', '1', '0', '', '', '4', '', 'man.zhong@afe.hk', '2030-12-31 14:59:32', 'sys', '2023-09-06 14:59:40', '2', '2025-05-22 17:16:47', '', '', '', '');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `expiry_date`, `create_by`, `create_on`, `update_by`, `update_on`, `otp_logon`, `otp_method`, `soft_key`, `passport_id`)
|
||||||
|
VALUES (1927187036663549954, 'opr', 'opr', 'opr', '0', '0', '', '', '5', '', 'man.zhong@afe.hk', NULL, 'sys', '2025-05-27 10:16:09', 'sys', NULL, 'DISD', 'NON', NULL, '');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `expiry_date`, `create_by`, `create_on`, `update_by`, `update_on`, `otp_logon`, `otp_method`, `soft_key`, `passport_id`)
|
||||||
|
VALUES (1927187170034028545, 'afe', 'afe', 'afe', '0', '0', '', '', '5', '', 'john.kong@afe.hk', NULL, 'sys', '2025-05-27 10:16:41', 'sys', '2025-05-30 14:13:18', 'ENG', 'NON', 'b99a24f0b2152335c17ec192970335b372a8dc5d', 'A123456');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin` (`id`, `user_code`, `local_name`, `eng_name`, `user_type`, `user_status`, `title`, `branch_code`, `department`, `mobile_no`, `email`, `expiry_date`, `create_by`, `create_on`, `update_by`, `update_on`, `otp_logon`, `otp_method`, `soft_key`, `passport_id`)
|
||||||
|
VALUES (1978983781352595458, 'g3bsupport', 'g3bsupport', 'g3bsupport', '0', '0', '', 'DEFAULT', '1', '', '', NULL, 'sys', '2025-10-17 08:37:55', 'sys', NULL, 'DISD', 'NON', NULL, '');
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_login` (`id`, `user_id`, `user_code`, `channel_code`, `admin_login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1, 1, 'sys', 'OM', '1', 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, '2023-09-06 15:02:48', '2023-09-06 15:02:51', '1', '2023-09-06 15:02:55', '1', '2023-09-06 15:02:59');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_login` (`id`, `user_id`, `user_code`, `channel_code`, `admin_login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1927187036743241729, 1927187036663549954, 'opr', 'OM', '1', 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_login` (`id`, `user_id`, `user_code`, `channel_code`, `admin_login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1927187170126303234, 1927187170034028545, 'afe', 'OM', '1', 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_login` (`id`, `user_id`, `user_code`, `channel_code`, `admin_login_status`, `password`, `retry_count`, `pwd_expiry_date`, `last_login_date`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1978983781449064449, 1978983781352595458, 'g3bsupport', 'OM', '1', 'F877376BAD1A22E67333F299305674CDEC789F2BA2E5BFD074A6B40F0D245CDB', 0, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1927187036705492994, 1927187036663549954, 'opr', 4, NULL, 'sys', '2025-05-27 10:16:10', 'sys', '2025-05-27 10:16:10');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1927187170080165890, 1927187170034028545, 'afe', 4, NULL, 'sys', '2025-05-27 10:16:41', 'opr', '2025-05-30 14:13:19');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1, 1, 'sys', 4, NULL, 'sys', '2023-11-13 09:37:56', 'sys', '2025-05-22 17:16:47');
|
||||||
|
REPLACE INTO `g3fo_admin`.`m_admin_role_relation_def` (`id`, `user_id`, `user_code`, `user_role_templates_id`, `active_flag`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES (1978983781377761282, 1978983781352595458, 'g3bsupport', 4, NULL, 'sys', '2025-10-17 08:37:56', 'sys', '2025-10-17 08:37:56');
|
||||||
|
|
||||||
|
|
||||||
|
-- ===============================
|
||||||
|
-- 市场 API 会话与系统参数更新
|
||||||
|
-- ===============================
|
||||||
|
UPDATE `g3fo_base`.`m_exchange`
|
||||||
|
SET `market_session_code` = 'OCG';
|
||||||
|
|
||||||
|
DELETE FROM `g3fo_base`.`m_market_api_session_info`;
|
||||||
|
|
||||||
|
INSERT INTO `g3fo_base`.`m_market_api_session_info` (`id`, `market_api_info_id`, `session_code`, `exchange_code`, `sender_comp_id`, `target_comp_id`, `rsakey`, `username`, `password`, `new_password`, `broker_list`, `auto_sync_seq_num_flag`, `auto_get_broker_id_flag`, `throttle_limit`, `remark`, `create_on`, `create_by`, `update_on`, `update_by`)
|
||||||
|
VALUES (1, 1, 'OCG', 'SEHK', 'CO01433001', 'HKEXCO', 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3bn6Y9Uz6lmtQZXl3cQnTshv8G3X8F3G35p1gdAD1pphD6FzfYnrwxrzqrbu27x4RQ9MpACi/8ct7OmyKiW06jekCJAGuy5EpHJWLnjYJS0ZcpYEQLfg3dEoY1X8J1S5S8oK0DVfk5gQrtyS9XOhU7D8aJbF6O/rZU5U5Nz/2p7e87sZeWAQigntFYN4pn68TkFzaVcoL7o4RN9AnQtDyXJcxwnP0bZxiOxOft5Jj6UB0I/5SOCZF61c9xxsd8fA3JkSTmudO80bnSwsmJZO7oCvu6R3FlHTWy8lZNwH0Wo5kaMCvKrn1i72fcUVVN5OcdmFIl+Yj3PqRGLIhGzM6wIDAQAB', NULL, 'Hw171025', 'Hw171025', '1399,1390,1391,1398', 0, 1, 0, '', NOW(), NULL, NOW(), 'sys');
|
||||||
|
INSERT INTO `g3fo_base`.`m_market_api_session_info` (`id`, `market_api_info_id`, `session_code`, `exchange_code`, `sender_comp_id`, `target_comp_id`, `rsakey`, `username`, `password`, `new_password`, `broker_list`, `auto_sync_seq_num_flag`, `auto_get_broker_id_flag`, `throttle_limit`, `remark`, `create_on`, `create_by`, `update_on`, `update_by`)
|
||||||
|
VALUES (2, 1, 'OCG-DC', 'SEHK', 'CC01433001', 'HKEXCC', 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3bn6Y9Uz6lmtQZXl3cQnTshv8G3X8F3G35p1gdAD1pphD6FzfYnrwxrzqrbu27x4RQ9MpACi/8ct7OmyKiW06jekCJAGuy5EpHJWLnjYJS0ZcpYEQLfg3dEoY1X8J1S5S8oK0DVfk5gQrtyS9XOhU7D8aJbF6O/rZU5U5Nz/2p7e87sZeWAQigntFYN4pn68TkFzaVcoL7o4RN9AnQtDyXJcxwnP0bZxiOxOft5Jj6UB0I/5SOCZF61c9xxsd8fA3JkSTmudO80bnSwsmJZO7oCvu6R3FlHTWy8lZNwH0Wo5kaMCvKrn1i72fcUVVN5OcdmFIl+Yj3PqRGLIhGzM6wIDAQAB', NULL, 'Hw171025', 'Hw171025', '1399,1390,1391,1398', 0, 1, 0, '', NOW(), NULL, NOW(), 'sys');
|
||||||
|
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '3082014B0201003082012C06072A8648CE3804013082011F02818100FD7F53811D75122952DF4A9C2EECE4E7F611B7523CEF4400C31E3F80B6512669455D402251FB593D8D58FABFC5F5BA30F6CB9B556CD7813B801D346FF26660B76B9950A5A49F9FE8047B1022C24FBBA9D7FEB7C61BF83B57E7C6A8A6150F04FB83F6D3C51EC3023554135A169132F675F3AE2B61D72AEFF22203199DD14801C70215009760508F15230BCCB292B982A2EB840BF0581CF502818100F7E1A085D69B3DDECBBCAB5C36B857B97994AFBBFA3AEA82F9574C0B3D0782675159578EBAD4594FE67107108180B449167123E84C281613B7CF09328CC8A6E13C167A8B547C8D28E0A3AE1E2BB3A675916EA37F0BFA213562F1FB627A01243BCCA4F1BEA8519089A883DFE15AE59F06928B665E807B552564014C3BFECF492A041602141FD4151E4E63E45D0E480CC880E04E0E4746F393',
|
||||||
|
`description_i18n_id` = NULL,
|
||||||
|
`remark` = 'Infobase Single Logon Private Key',
|
||||||
|
`create_by` = 'ken',
|
||||||
|
`create_on` = '2024-02-22 09:21:43',
|
||||||
|
`update_by` = 'ken',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE `param_name` = 'base.infoBaseLogonKey';
|
||||||
|
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = '683',
|
||||||
|
`description_i18n_id` = NULL,
|
||||||
|
`remark` = 'Site ID for Single pass Logon',
|
||||||
|
`create_by` = 'ken',
|
||||||
|
`create_on` = '2024-02-22 09:18:26',
|
||||||
|
`update_by` = 'ken',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE `param_name` = 'base.siteId';
|
||||||
|
|
||||||
|
UPDATE `g3fo_base`.`m_system_setting`
|
||||||
|
SET `param_value` = 'hingwai',
|
||||||
|
`description_i18n_id` = NULL,
|
||||||
|
`remark` = 'Site Name (group name required)',
|
||||||
|
`create_by` = 'ken',
|
||||||
|
`create_on` = '2024-02-22 09:18:06',
|
||||||
|
`update_by` = 'ken',
|
||||||
|
`update_on` = NOW()
|
||||||
|
WHERE `param_name` = 'base.siteName';
|
||||||
|
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
USE g3fo_base;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 存储过程:插入系统菜单数据
|
||||||
|
-- 功能:
|
||||||
|
-- 1. 自动生成ID(最大ID+1)并插入系统菜单记录
|
||||||
|
-- 2. 当menu_type为'ADMIN'时,自动为所有管理员角色模板创建权限记录
|
||||||
|
-- 3. 当menu_type为'USER'时,自动为所有用户角色模板创建权限记录
|
||||||
|
-- 4. 数据去重检查:
|
||||||
|
-- - m_system_menus: 检查 menu_name + menu_type 组合
|
||||||
|
-- - m_admin_role_rights: 检查 user_role_templates_id + user_right_id + user_right_type 组合
|
||||||
|
-- - m_user_role_rights: 检查 user_role_templates_id + user_right_id + user_right_type 组合
|
||||||
|
-- 参数:
|
||||||
|
-- p_remark: 备注
|
||||||
|
-- p_menu_name: 菜单名称
|
||||||
|
-- p_title_remark: 标题备注
|
||||||
|
-- p_menu_type: 菜单类型('ADMIN' 或 'USER')
|
||||||
|
-- p_description_i18n_id: 描述国际化ID(可选,默认为NULL)
|
||||||
|
-- p_active_flag: 激活标志(可选,默认为1)
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS InsertSystemMenu;
|
||||||
|
|
||||||
|
DELIMITER $$
|
||||||
|
|
||||||
|
CREATE PROCEDURE InsertSystemMenu(
|
||||||
|
IN p_remark VARCHAR(50),
|
||||||
|
IN p_menu_name VARCHAR(255),
|
||||||
|
IN p_title_remark VARCHAR(50),
|
||||||
|
IN p_menu_type CHAR(8),
|
||||||
|
IN p_description_i18n_id CHAR(32),
|
||||||
|
IN p_active_flag SMALLINT
|
||||||
|
)
|
||||||
|
proc_label: BEGIN
|
||||||
|
DECLARE v_new_menu_id INT;
|
||||||
|
DECLARE v_done INT DEFAULT FALSE;
|
||||||
|
DECLARE v_template_id BIGINT;
|
||||||
|
DECLARE v_new_right_id INT;
|
||||||
|
DECLARE v_rights_count INT DEFAULT 0;
|
||||||
|
DECLARE v_existing_menu_id INT DEFAULT NULL;
|
||||||
|
DECLARE v_existing_right_count INT DEFAULT 0;
|
||||||
|
|
||||||
|
-- 声明游标结束处理器
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第一步:检查菜单是否已存在(menu_name + menu_type 组合去重)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SELECT id INTO v_existing_menu_id
|
||||||
|
FROM g3fo_base.m_system_menus
|
||||||
|
WHERE menu_name = p_menu_name AND menu_type = p_menu_type
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- 如果菜单已存在,直接退出存储过程
|
||||||
|
IF v_existing_menu_id IS NOT NULL THEN
|
||||||
|
LEAVE proc_label; -- 退出存储过程
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第二步:插入系统菜单数据
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 获取当前最大ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 INTO v_new_menu_id
|
||||||
|
FROM g3fo_base.m_system_menus;
|
||||||
|
|
||||||
|
-- 设置默认值
|
||||||
|
IF p_active_flag IS NULL THEN
|
||||||
|
SET p_active_flag = 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 插入系统菜单数据
|
||||||
|
INSERT INTO g3fo_base.m_system_menus (
|
||||||
|
id,
|
||||||
|
remark,
|
||||||
|
menu_name,
|
||||||
|
description_i18n_id,
|
||||||
|
active_flag,
|
||||||
|
title_remark,
|
||||||
|
menu_type,
|
||||||
|
create_by,
|
||||||
|
create_on,
|
||||||
|
update_by,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_menu_id,
|
||||||
|
p_remark,
|
||||||
|
p_menu_name,
|
||||||
|
p_description_i18n_id,
|
||||||
|
p_active_flag,
|
||||||
|
p_title_remark,
|
||||||
|
p_menu_type,
|
||||||
|
'sys',
|
||||||
|
NOW(),
|
||||||
|
'sys',
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第三步:根据menu_type创建相应的角色权限记录
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
IF UPPER(p_menu_type) = 'ADMIN' THEN
|
||||||
|
-- ========================================================
|
||||||
|
-- 为管理员角色模板创建权限记录
|
||||||
|
-- ========================================================
|
||||||
|
BEGIN
|
||||||
|
DECLARE admin_cursor CURSOR FOR
|
||||||
|
SELECT id FROM g3fo_admin.m_admin_role_templates;
|
||||||
|
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
SET v_done = FALSE;
|
||||||
|
OPEN admin_cursor;
|
||||||
|
|
||||||
|
admin_loop: LOOP
|
||||||
|
FETCH admin_cursor INTO v_template_id;
|
||||||
|
|
||||||
|
IF v_done THEN
|
||||||
|
LEAVE admin_loop;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查该权限记录是否已存在(去重检查)
|
||||||
|
SET v_existing_right_count = 0;
|
||||||
|
SELECT COUNT(*) INTO v_existing_right_count
|
||||||
|
FROM g3fo_admin.m_admin_role_rights
|
||||||
|
WHERE user_role_templates_id = v_template_id
|
||||||
|
AND user_right_id = v_new_menu_id
|
||||||
|
AND user_right_type = 'MENU';
|
||||||
|
|
||||||
|
-- 只有当权限记录不存在时才插入
|
||||||
|
IF v_existing_right_count = 0 THEN
|
||||||
|
-- 获取当前最大的权限ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 INTO v_new_right_id
|
||||||
|
FROM g3fo_admin.m_admin_role_rights;
|
||||||
|
|
||||||
|
-- 插入管理员角色权限记录
|
||||||
|
INSERT INTO g3fo_admin.m_admin_role_rights (
|
||||||
|
id,
|
||||||
|
user_role_templates_id,
|
||||||
|
user_right_id,
|
||||||
|
user_right_type,
|
||||||
|
create_on,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_right_id,
|
||||||
|
v_template_id,
|
||||||
|
v_new_menu_id,
|
||||||
|
'MENU',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
SET v_rights_count = v_rights_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE admin_cursor;
|
||||||
|
END;
|
||||||
|
|
||||||
|
ELSEIF UPPER(p_menu_type) = 'USER' THEN
|
||||||
|
-- ========================================================
|
||||||
|
-- 为用户角色模板创建权限记录(排除id=9)
|
||||||
|
-- ========================================================
|
||||||
|
BEGIN
|
||||||
|
DECLARE user_cursor CURSOR FOR
|
||||||
|
SELECT id FROM g3fo_user.m_user_role_templates WHERE id != 9;
|
||||||
|
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
SET v_done = FALSE;
|
||||||
|
OPEN user_cursor;
|
||||||
|
|
||||||
|
user_loop: LOOP
|
||||||
|
FETCH user_cursor INTO v_template_id;
|
||||||
|
|
||||||
|
IF v_done THEN
|
||||||
|
LEAVE user_loop;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查该权限记录是否已存在(去重检查)
|
||||||
|
SET v_existing_right_count = 0;
|
||||||
|
SELECT COUNT(*) INTO v_existing_right_count
|
||||||
|
FROM g3fo_user.m_user_role_rights
|
||||||
|
WHERE user_role_templates_id = v_template_id
|
||||||
|
AND user_right_id = v_new_menu_id
|
||||||
|
AND user_right_type = 'MENU';
|
||||||
|
|
||||||
|
-- 只有当权限记录不存在时才插入
|
||||||
|
IF v_existing_right_count = 0 THEN
|
||||||
|
-- 获取当前最大的权限ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 INTO v_new_right_id
|
||||||
|
FROM g3fo_user.m_user_role_rights;
|
||||||
|
|
||||||
|
-- 插入用户角色权限记录
|
||||||
|
INSERT INTO g3fo_user.m_user_role_rights (
|
||||||
|
id,
|
||||||
|
user_role_templates_id,
|
||||||
|
user_right_id,
|
||||||
|
user_right_type,
|
||||||
|
create_on,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_right_id,
|
||||||
|
v_template_id,
|
||||||
|
v_new_menu_id,
|
||||||
|
'MENU',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
SET v_rights_count = v_rights_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE user_cursor;
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 返回结果
|
||||||
|
-- ============================================================
|
||||||
|
END$$
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 在这里添加需要插入的菜单
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- CALL InsertSystemMenu('菜单备注', 'menu-name', 'title_remark', 'ADMIN', NULL, 1);
|
||||||
|
-- CALL InsertSystemMenu('菜单备注', 'menu-name', 'title_remark', 'USER', NULL, 1);
|
||||||
|
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 使用示例:
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- 示例1: 插入管理员菜单(会在 m_system_menus 中插入一条记录,
|
||||||
|
-- 同时在 m_admin_role_rights 中为所有管理员角色模板创建权限记录)
|
||||||
|
-- CALL InsertSystemMenu('用户管理', 'user-management', 'user', 'ADMIN', NULL, 1);
|
||||||
|
|
||||||
|
-- 示例2: 插入用户菜单(会在 m_system_menus 中插入一条记录,
|
||||||
|
-- 同时在 m_user_role_rights 中为所有用户角色模板(除了id=9)创建权限记录)
|
||||||
|
-- CALL InsertSystemMenu('订单查询', 'order-query', 'order', 'USER', NULL, 1);
|
||||||
|
|
||||||
|
-- 示例3: 插入带国际化ID的管理员菜单
|
||||||
|
-- CALL InsertSystemMenu('报表管理', 'report-management', 'report', 'ADMIN', 'REPORT-0001', 1);
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 说明:
|
||||||
|
-- ===================================================================
|
||||||
|
-- 当 menu_type = 'ADMIN' 时:
|
||||||
|
-- - 在 g3fo_base.m_system_menus 中插入一条菜单记录
|
||||||
|
-- - 自动获取 g3fo_admin.m_admin_role_templates 中所有id
|
||||||
|
-- - 为每个角色模板在 g3fo_admin.m_admin_role_rights 中创建一条权限记录
|
||||||
|
-- * id: 自动递增(最大ID + 1)
|
||||||
|
-- * user_role_templates_id: 角色模板ID
|
||||||
|
-- * user_right_id: 新插入的菜单ID
|
||||||
|
-- * user_right_type: 'MENU'
|
||||||
|
--
|
||||||
|
-- 当 menu_type = 'USER' 时:
|
||||||
|
-- - 在 g3fo_base.m_system_menus 中插入一条菜单记录
|
||||||
|
-- - 自动获取 g3fo_user.m_user_role_templates 中所有id(排除 id=9)
|
||||||
|
-- - 为每个角色模板在 g3fo_user.m_user_role_rights 中创建一条权限记录
|
||||||
|
-- * id: 自动递增(最大ID + 1)
|
||||||
|
-- * user_role_templates_id: 角色模板ID
|
||||||
|
-- * user_right_id: 新插入的菜单ID
|
||||||
|
-- * user_right_type: 'MENU'
|
||||||
|
--
|
||||||
|
-- 去重逻辑:
|
||||||
|
-- 1. 菜单去重:如果 menu_name + menu_type 组合已存在,返回错误,不插入任何数据
|
||||||
|
-- 2. 权限去重:对于每个角色模板,检查 user_role_templates_id + user_right_id + user_right_type
|
||||||
|
-- 组合是否已存在,如果已存在则跳过该条记录,继续处理下一个
|
||||||
|
--
|
||||||
|
-- 参数说明:
|
||||||
|
-- p_remark: 菜单备注(varchar(50))
|
||||||
|
-- p_menu_name: 菜单名称,唯一标识(varchar(255))
|
||||||
|
-- p_title_remark: 标题备注(varchar(50))
|
||||||
|
-- p_menu_type: 菜单类型,'ADMIN' 或 'USER'(char(8))
|
||||||
|
-- p_description_i18n_id: 描述国际化ID,可选(char(32)),默认为NULL
|
||||||
|
-- p_active_flag: 激活标志,可选(smallint),默认为1
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
USE g3fo_base;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 存储过程:插入系统路由数据
|
||||||
|
-- 功能:
|
||||||
|
-- 1. 自动生成ID(最大ID+1,最小从7000开始)并插入系统路由记录
|
||||||
|
-- 2. 当route_type为'ADMIN'时,自动为所有管理员角色模板创建权限记录
|
||||||
|
-- 3. 当route_type为'USER'时,自动为所有用户角色模板创建权限记录
|
||||||
|
-- 4. 数据去重检查:
|
||||||
|
-- - m_system_routes: 检查 route + route_type 组合
|
||||||
|
-- - m_admin_role_rights: 检查 user_role_templates_id + user_right_id + user_right_type 组合
|
||||||
|
-- - m_user_role_rights: 检查 user_role_templates_id + user_right_id + user_right_type 组合
|
||||||
|
-- 参数:
|
||||||
|
-- p_remark: 备注
|
||||||
|
-- p_route: 路由地址
|
||||||
|
-- p_title_remark: 标题备注
|
||||||
|
-- p_route_type: 路由类型('ADMIN' 或 'USER')
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS InsertSystemRoute;
|
||||||
|
|
||||||
|
DELIMITER $$
|
||||||
|
|
||||||
|
CREATE PROCEDURE InsertSystemRoute(
|
||||||
|
IN p_remark VARCHAR(50),
|
||||||
|
IN p_route VARCHAR(255),
|
||||||
|
IN p_title_remark VARCHAR(50),
|
||||||
|
IN p_route_type CHAR(8)
|
||||||
|
)
|
||||||
|
proc_label: BEGIN
|
||||||
|
DECLARE v_new_route_id INT;
|
||||||
|
DECLARE v_done INT DEFAULT FALSE;
|
||||||
|
DECLARE v_template_id BIGINT;
|
||||||
|
DECLARE v_new_right_id INT;
|
||||||
|
DECLARE v_rights_count INT DEFAULT 0;
|
||||||
|
DECLARE v_existing_route_id INT DEFAULT NULL;
|
||||||
|
DECLARE v_existing_right_count INT DEFAULT 0;
|
||||||
|
|
||||||
|
-- 声明游标结束处理器
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第一步:检查路由是否已存在(route + route_type 组合去重)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SELECT id INTO v_existing_route_id
|
||||||
|
FROM g3fo_base.m_system_routes
|
||||||
|
WHERE route = p_route AND route_type = p_route_type
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- 如果路由已存在,直接返回错误信息
|
||||||
|
IF v_existing_route_id IS NOT NULL THEN
|
||||||
|
LEAVE proc_label; -- 退出存储过程
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第二步:插入系统路由数据
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 获取当前最大ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 6999) + 1 INTO v_new_route_id
|
||||||
|
FROM g3fo_base.m_system_routes;
|
||||||
|
|
||||||
|
-- 如果ID少于7000,从7000开始
|
||||||
|
IF v_new_route_id < 7000 THEN
|
||||||
|
SET v_new_route_id = 7000;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 插入系统路由数据
|
||||||
|
INSERT INTO g3fo_base.m_system_routes (
|
||||||
|
id,
|
||||||
|
remark,
|
||||||
|
route,
|
||||||
|
title_remark,
|
||||||
|
route_type,
|
||||||
|
active_flag,
|
||||||
|
create_on,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_route_id,
|
||||||
|
p_remark,
|
||||||
|
p_route,
|
||||||
|
p_title_remark,
|
||||||
|
p_route_type,
|
||||||
|
1, -- 默认激活状态
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 第三步:根据route_type创建相应的角色权限记录
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
IF UPPER(p_route_type) = 'ADMIN' THEN
|
||||||
|
-- ========================================================
|
||||||
|
-- 为管理员角色模板创建权限记录
|
||||||
|
-- ========================================================
|
||||||
|
BEGIN
|
||||||
|
DECLARE admin_cursor CURSOR FOR
|
||||||
|
SELECT id FROM g3fo_admin.m_admin_role_templates;
|
||||||
|
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
SET v_done = FALSE;
|
||||||
|
OPEN admin_cursor;
|
||||||
|
|
||||||
|
admin_loop: LOOP
|
||||||
|
FETCH admin_cursor INTO v_template_id;
|
||||||
|
|
||||||
|
IF v_done THEN
|
||||||
|
LEAVE admin_loop;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查该权限记录是否已存在(去重检查)
|
||||||
|
SET v_existing_right_count = 0;
|
||||||
|
SELECT COUNT(*) INTO v_existing_right_count
|
||||||
|
FROM g3fo_admin.m_admin_role_rights
|
||||||
|
WHERE user_role_templates_id = v_template_id
|
||||||
|
AND user_right_id = v_new_route_id
|
||||||
|
AND user_right_type = 'ROTE';
|
||||||
|
|
||||||
|
-- 只有当权限记录不存在时才插入
|
||||||
|
IF v_existing_right_count = 0 THEN
|
||||||
|
-- 获取当前最大的权限ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 INTO v_new_right_id
|
||||||
|
FROM g3fo_admin.m_admin_role_rights;
|
||||||
|
|
||||||
|
-- 插入管理员角色权限记录
|
||||||
|
INSERT INTO g3fo_admin.m_admin_role_rights (
|
||||||
|
id,
|
||||||
|
user_role_templates_id,
|
||||||
|
user_right_id,
|
||||||
|
user_right_type,
|
||||||
|
create_on,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_right_id,
|
||||||
|
v_template_id,
|
||||||
|
v_new_route_id,
|
||||||
|
'ROTE',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
SET v_rights_count = v_rights_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE admin_cursor;
|
||||||
|
END;
|
||||||
|
|
||||||
|
ELSEIF UPPER(p_route_type) = 'USER' THEN
|
||||||
|
-- ========================================================
|
||||||
|
-- 为用户角色模板创建权限记录(排除id=9)
|
||||||
|
-- ========================================================
|
||||||
|
BEGIN
|
||||||
|
DECLARE user_cursor CURSOR FOR
|
||||||
|
SELECT id FROM g3fo_user.m_user_role_templates WHERE id != 9;
|
||||||
|
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
|
||||||
|
|
||||||
|
SET v_done = FALSE;
|
||||||
|
OPEN user_cursor;
|
||||||
|
|
||||||
|
user_loop: LOOP
|
||||||
|
FETCH user_cursor INTO v_template_id;
|
||||||
|
|
||||||
|
IF v_done THEN
|
||||||
|
LEAVE user_loop;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查该权限记录是否已存在(去重检查)
|
||||||
|
SET v_existing_right_count = 0;
|
||||||
|
SELECT COUNT(*) INTO v_existing_right_count
|
||||||
|
FROM g3fo_user.m_user_role_rights
|
||||||
|
WHERE user_role_templates_id = v_template_id
|
||||||
|
AND user_right_id = v_new_route_id
|
||||||
|
AND user_right_type = 'ROTE';
|
||||||
|
|
||||||
|
-- 只有当权限记录不存在时才插入
|
||||||
|
IF v_existing_right_count = 0 THEN
|
||||||
|
-- 获取当前最大的权限ID并加1
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1 INTO v_new_right_id
|
||||||
|
FROM g3fo_user.m_user_role_rights;
|
||||||
|
|
||||||
|
-- 插入用户角色权限记录
|
||||||
|
INSERT INTO g3fo_user.m_user_role_rights (
|
||||||
|
id,
|
||||||
|
user_role_templates_id,
|
||||||
|
user_right_id,
|
||||||
|
user_right_type,
|
||||||
|
create_on,
|
||||||
|
update_on
|
||||||
|
) VALUES (
|
||||||
|
v_new_right_id,
|
||||||
|
v_template_id,
|
||||||
|
v_new_route_id,
|
||||||
|
'ROTE',
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
SET v_rights_count = v_rights_count + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
CLOSE user_cursor;
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 返回结果
|
||||||
|
-- ============================================================
|
||||||
|
END$$
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 在这里添加需要插入的路由
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
CALL InsertSystemRoute('重试订单', '/trade/order/retryOrder', '订单', 'USER');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 使用示例:
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- 示例1: 插入管理员路由(会在 m_system_routes 中插入一条记录,
|
||||||
|
-- 同时在 m_admin_role_rights 中为所有管理员角色模板创建权限记录)
|
||||||
|
-- CALL InsertSystemRoute('用户管理', '/admin/user/list', '用户管理', 'ADMIN');
|
||||||
|
|
||||||
|
-- 示例2: 插入用户路由(会在 m_system_routes 中插入一条记录,
|
||||||
|
-- 同时在 m_user_role_rights 中为所有用户角色模板(除了id=9)创建权限记录)
|
||||||
|
-- CALL InsertSystemRoute('订单查询', '/user/order/list', '订单查询', 'USER');
|
||||||
|
|
||||||
|
-- ===================================================================
|
||||||
|
-- 说明:
|
||||||
|
-- ===================================================================
|
||||||
|
-- 当 route_type = 'ADMIN' 时:
|
||||||
|
-- - 在 g3fo_base.m_system_routes 中插入一条路由记录
|
||||||
|
-- - 自动获取 g3fo_admin.m_admin_role_templates 中所有id
|
||||||
|
-- - 为每个角色模板在 g3fo_admin.m_admin_role_rights 中创建一条权限记录
|
||||||
|
-- * id: 自动递增(最大ID + 1)
|
||||||
|
-- * user_role_templates_id: 角色模板ID
|
||||||
|
-- * user_right_id: 新插入的路由ID
|
||||||
|
-- * user_right_type: 'ROTE'
|
||||||
|
--
|
||||||
|
-- 当 route_type = 'USER' 时:
|
||||||
|
-- - 在 g3fo_base.m_system_routes 中插入一条路由记录
|
||||||
|
-- - 自动获取 g3fo_user.m_user_role_templates 中所有id(排除 id=9)
|
||||||
|
-- - 为每个角色模板在 g3fo_user.m_user_role_rights 中创建一条权限记录
|
||||||
|
-- * id: 自动递增(最大ID + 1)
|
||||||
|
-- * user_role_templates_id: 角色模板ID
|
||||||
|
-- * user_right_id: 新插入的路由ID
|
||||||
|
-- * user_right_type: 'ROTE'
|
||||||
|
--
|
||||||
|
-- 去重逻辑:
|
||||||
|
-- 1. 路由去重:如果 route + route_type 组合已存在,返回错误,不插入任何数据
|
||||||
|
-- 2. 权限去重:对于每个角色模板,检查 user_role_templates_id + user_right_id + user_right_type
|
||||||
|
-- 组合是否已存在,如果已存在则跳过该条记录,继续处理下一个
|
||||||
|
--
|
||||||
|
-- 返回结果:
|
||||||
|
-- - new_route_id: 新插入的路由ID
|
||||||
|
-- - route_type: 路由类型
|
||||||
|
-- - rights_created: 成功创建的权限记录数量
|
||||||
|
-- - status: 执行状态('Success' 或错误信息)
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
REPLACE INTO `g3fo_base`.`m_system_code` (`id`, `code_type`, `code_value`, `description_i18n_id`, `remark`, `sort`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES ('404', 'OrderType', '015', 'CODE-0404', 'PCA Order (VN)', '16', 'sys', '2023-12-14 15:40:51', 'sys', '2024-07-05 18:23:49');
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_i18n` (`id`, `en_description`, `sc_description`, `tc_description`, `vn_description`, `create_by`, `create_on`, `update_by`, `update_on`) VALUES ('CODE-0404', 'PCA', 'PCA', 'PCA', 'PCA', 'sys', '2024-06-27 10:43:29', 'sys', '2024-07-05 13:59:04');
|
||||||
|
|
||||||
|
-- 订单类型自动转换配置(初始为空值,需要时再启用)
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_setting`
|
||||||
|
(`param_name`, `param_value`, `description_i18n_id`, `remark`, `create_by`, `create_on`, `update_by`, `update_on`)
|
||||||
|
VALUES
|
||||||
|
('trade.orderTypeAutoConversion',
|
||||||
|
'04|02-03',
|
||||||
|
NULL,
|
||||||
|
'订单类型自动转换:格式为源类型1|源类型2-目标类型,例如04|02-03表示将LO和ELO转换为ALO',
|
||||||
|
'sys',
|
||||||
|
NOW(),
|
||||||
|
'sys',
|
||||||
|
NOW());
|
||||||
|
|
||||||
|
REPLACE INTO `g3fo_base`.`m_system_setting` (`param_name`, `param_value`, `remark`, `create_by`) VALUES ('trade.maxAuctionOrderPriceChangeRatio', '15@SEHK', '竞价限价单最大价格区间(%)', 'sys');
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
USE g3fo_trade;
|
||||||
|
DROP PROCEDURE IF EXISTS UpdateBrokerIdColumnLength;
|
||||||
|
CREATE PROCEDURE UpdateBrokerIdColumnLength()
|
||||||
|
BEGIN
|
||||||
|
-- 更新 m_order 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 m_order 表的 counter_party 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order'
|
||||||
|
AND column_name = 'counter_party'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order` MODIFY COLUMN `counter_party` VARCHAR(32) COMMENT '对手方';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 m_order_actions 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order_actions'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order_actions` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 m_order_actions 表的 counter_party 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_order_actions'
|
||||||
|
AND column_name = 'counter_party'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_order_actions` MODIFY COLUMN `counter_party` VARCHAR(32) COMMENT '对手方';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_order 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_order 表的 counter_party 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order'
|
||||||
|
AND column_name = 'counter_party'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order` MODIFY COLUMN `counter_party` VARCHAR(32) COMMENT '对手方';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_order_actions 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order_actions'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order_actions` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_order_actions 表的 counter_party 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_order_actions'
|
||||||
|
AND column_name = 'counter_party'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_order_actions` MODIFY COLUMN `counter_party` VARCHAR(32) COMMENT '对手方';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 m_trade 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_trade'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_trade` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 m_trade 表的 counter_broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'm_trade'
|
||||||
|
AND column_name = 'counter_broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`m_trade` MODIFY COLUMN `counter_broker_id` VARCHAR(32) COMMENT '对手经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_trade 表的 broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_trade'
|
||||||
|
AND column_name = 'broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_trade` MODIFY COLUMN `broker_id` VARCHAR(32) COMMENT '经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 更新 h_trade 表的 counter_broker_id 字段长度从 VARCHAR(8) 到 VARCHAR(32)
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'g3fo_trade'
|
||||||
|
AND table_name = 'h_trade'
|
||||||
|
AND column_name = 'counter_broker_id'
|
||||||
|
AND data_type = 'varchar'
|
||||||
|
AND character_maximum_length = 8
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `g3fo_trade`.`h_trade` MODIFY COLUMN `counter_broker_id` VARCHAR(32) COMMENT '对手经纪商ID';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END;
|
||||||
|
CALL UpdateBrokerIdColumnLength();
|
||||||
|
DROP PROCEDURE IF EXISTS UpdateBrokerIdColumnLength;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
--- Nacos2.5.1 升级 Nacos3.1.1
|
||||||
|
ALTER TABLE `nacos`.`config_info` MODIFY COLUMN `encrypted_data_key` varchar(1024) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL DEFAULT '' COMMENT '密钥' AFTER `c_schema`;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`permissions` COLLATE = utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`permissions` MODIFY COLUMN `role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'role' FIRST;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`permissions` MODIFY COLUMN `resource` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'resource' AFTER `role`;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`permissions` MODIFY COLUMN `action` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'action' AFTER `resource`;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`roles` COLLATE = utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`roles` MODIFY COLUMN `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'username' FIRST;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`roles` MODIFY COLUMN `role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'role' AFTER `username`;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`users` COLLATE = utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`users` MODIFY COLUMN `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'username' FIRST;
|
||||||
|
|
||||||
|
ALTER TABLE `nacos`.`users` MODIFY COLUMN `password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'password' AFTER `username`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `nacos`.`config_info_aggr`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `nacos`.`config_info_beta`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `nacos`.`config_info_tag`;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `nacos`.`flyway_schema_history`;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# G3FO Database Operations Script
|
||||||
|
|
||||||
|
替代 MCP MySQL 的 Python 数据库操作工具。
|
||||||
|
|
||||||
|
## 连接管理(无内存泄漏)
|
||||||
|
|
||||||
|
- **单次执行**:每次命令是独立进程,执行完即退出
|
||||||
|
- **连接生命周期**:连接仅在单次命令执行期间存在(通常几毫秒到几秒),`finally` 中统一 `conn.close()`
|
||||||
|
- **无长连接/连接池**:用完即关,不会累积连接
|
||||||
|
- **连接超时**:默认 10 秒,防止网络慢时长时间挂起
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# 或
|
||||||
|
pip install mysql-connector-python
|
||||||
|
```
|
||||||
|
|
||||||
|
## 默认连接
|
||||||
|
|
||||||
|
与 MCP user-mysql 一致:
|
||||||
|
- host: 192.168.3.233
|
||||||
|
- port: 3306
|
||||||
|
- user: root
|
||||||
|
- password: afe123456
|
||||||
|
- database: g3fo_base
|
||||||
|
|
||||||
|
## 覆盖连接
|
||||||
|
|
||||||
|
- 命令行:`--host`, `--port`, `--user`, `--password`, `--database`, `--connection-timeout`
|
||||||
|
- 环境变量:`MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `MYSQL_CONNECTION_TIMEOUT`
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| query | SELECT 查询 |
|
||||||
|
| execute | INSERT/UPDATE/DELETE |
|
||||||
|
| list_tables | 列举所有表 |
|
||||||
|
| describe_table | 表结构 |
|
||||||
|
| call_procedure | 执行存储过程 |
|
||||||
|
| batch_execute | 批量 SQL(支持文件) |
|
||||||
|
| get_table_comment | 获取表注释 |
|
||||||
|
| set_table_comment | 修改表注释 |
|
||||||
|
| get_column_comment | 获取列注释 |
|
||||||
|
| set_column_comment | 修改列注释 |
|
||||||
|
| test_connection | 测试连接 |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python db_ops.py test_connection
|
||||||
|
python db_ops.py query "SELECT 1"
|
||||||
|
python db_ops.py --database g3fo_trade list_tables
|
||||||
|
python db_ops.py describe_table m_order
|
||||||
|
python db_ops.py batch_execute --file script.sql
|
||||||
|
```
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
G3FO Database Operations Tool
|
||||||
|
|
||||||
|
替代 MCP MySQL,提供完整的数据库操作能力:
|
||||||
|
- 增删改查 (query, execute)
|
||||||
|
- 列举所有表 (list_tables)
|
||||||
|
- 执行存储过程 (call_procedure)
|
||||||
|
- 批量执行 SQL (batch_execute)
|
||||||
|
- 读取表结构 (describe_table)
|
||||||
|
- 读取/修改表和列注释 (get/set comments)
|
||||||
|
|
||||||
|
默认连接配置与 MCP user-mysql 一致,可通过参数或环境变量覆盖。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any, Optional
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
try:
|
||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import Error as MySQLError
|
||||||
|
except ImportError:
|
||||||
|
print("Error: mysql-connector-python is required. Run: pip install mysql-connector-python", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 默认连接配置(与 MCP user-mysql 一致)
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"host": "192.168.3.233",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "root",
|
||||||
|
"password": "afe123456",
|
||||||
|
"database": "g3fo_base",
|
||||||
|
"connection_timeout": 10, # 连接超时(秒),防止长时间挂起
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV_MAPPING = {
|
||||||
|
"host": "MYSQL_HOST",
|
||||||
|
"port": "MYSQL_PORT",
|
||||||
|
"user": "MYSQL_USER",
|
||||||
|
"password": "MYSQL_PASSWORD",
|
||||||
|
"database": "MYSQL_DATABASE",
|
||||||
|
"connection_timeout": "MYSQL_CONNECTION_TIMEOUT",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection_config(
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
user: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
database: Optional[str] = None,
|
||||||
|
connection_timeout: Optional[int] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""从默认配置、环境变量、参数中合并连接配置,参数优先级最高。"""
|
||||||
|
config = {}
|
||||||
|
params = {"host": host, "port": port, "user": user, "password": password, "database": database, "connection_timeout": connection_timeout}
|
||||||
|
for key, default in DEFAULT_CONFIG.items():
|
||||||
|
env_key = ENV_MAPPING.get(key)
|
||||||
|
env_val = os.environ.get(env_key) if env_key else None
|
||||||
|
param_val = params.get(key)
|
||||||
|
if param_val is not None:
|
||||||
|
config[key] = int(param_val) if key in ("port", "connection_timeout") else param_val
|
||||||
|
elif env_val is not None:
|
||||||
|
config[key] = int(env_val) if key in ("port", "connection_timeout") else env_val
|
||||||
|
else:
|
||||||
|
config[key] = default
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(config: dict):
|
||||||
|
"""
|
||||||
|
创建数据库连接。
|
||||||
|
连接仅在单次命令执行期间存在,命令结束或异常时在 finally 中关闭,不会长期占用。
|
||||||
|
"""
|
||||||
|
return mysql.connector.connect(
|
||||||
|
host=config["host"],
|
||||||
|
port=config["port"],
|
||||||
|
user=config["user"],
|
||||||
|
password=config["password"],
|
||||||
|
database=config["database"],
|
||||||
|
charset="utf8mb4",
|
||||||
|
collation="utf8mb4_unicode_ci",
|
||||||
|
connection_timeout=config.get("connection_timeout", 10),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_value(value: Any) -> Any:
|
||||||
|
"""将 MySQL 返回的值转换为可 JSON 序列化的类型。"""
|
||||||
|
if isinstance(value, (datetime, date, time)):
|
||||||
|
return value.isoformat()
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
# 大多数场景下用 float 即可,避免 JSON 不支持 Decimal
|
||||||
|
return float(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_query(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""执行 SELECT 查询。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute(args.sql, args.params or [])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
normalized_rows = [
|
||||||
|
{k: _normalize_value(v) for k, v in row.items()} for row in rows
|
||||||
|
]
|
||||||
|
return {"success": True, "data": normalized_rows, "rowCount": len(normalized_rows)}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_execute(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""执行 INSERT/UPDATE/DELETE。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(args.sql, args.params or [])
|
||||||
|
conn.commit()
|
||||||
|
affected = cursor.rowcount
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "affectedRows": affected}
|
||||||
|
except MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list_tables(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""列举数据库中的所有表。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, TABLE_COMMENT "
|
||||||
|
"FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME",
|
||||||
|
(config["database"],),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
tables = [
|
||||||
|
{
|
||||||
|
"schema": r[0],
|
||||||
|
"name": r[1],
|
||||||
|
"type": r[2],
|
||||||
|
"comment": r[3] or "",
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
return {"success": True, "tables": tables, "count": len(tables)}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_describe_table(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""获取表结构(列信息)。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
table = args.table
|
||||||
|
schema = args.schema or config["database"]
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, COLUMN_COMMENT,
|
||||||
|
EXTRA
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
||||||
|
ORDER BY ORDINAL_POSITION
|
||||||
|
""",
|
||||||
|
(schema, table),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
"name": r[0],
|
||||||
|
"type": r[1],
|
||||||
|
"nullable": r[2],
|
||||||
|
"key": r[3] or "",
|
||||||
|
"default": r[4],
|
||||||
|
"comment": r[5] or "",
|
||||||
|
"extra": r[6] or "",
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
# 获取表注释
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||||
|
(schema, table),
|
||||||
|
)
|
||||||
|
tbl = cursor.fetchone()
|
||||||
|
table_comment = tbl[0] if tbl else ""
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "table": table, "schema": schema, "columns": columns, "tableComment": table_comment}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_call_procedure(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""执行存储过程。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
placeholders = ", ".join(["%s"] * len(args.params)) if args.params else ""
|
||||||
|
sql = f"CALL {args.procedure}({placeholders})" if placeholders else f"CALL {args.procedure}()"
|
||||||
|
cursor.execute(sql, args.params or [])
|
||||||
|
rows = []
|
||||||
|
if cursor.description:
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
while cursor.nextset():
|
||||||
|
if cursor.description:
|
||||||
|
rows.extend(cursor.fetchall())
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "data": rows, "rowCount": len(rows)}
|
||||||
|
except MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_batch_execute(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""批量执行 SQL(支持多条语句、存储过程、DELIMITER)。"""
|
||||||
|
sql_text = args.sql or ""
|
||||||
|
if args.file:
|
||||||
|
with open(args.file, "r", encoding="utf-8") as f:
|
||||||
|
sql_text = f.read()
|
||||||
|
if not sql_text.strip():
|
||||||
|
return {"success": False, "error": "No SQL provided. Use 'sql' argument or --file"}
|
||||||
|
# 处理 DELIMITER(存储过程等):移除 DELIMITER 行,将 $$ 替换为 ;
|
||||||
|
if "DELIMITER" in sql_text.upper():
|
||||||
|
lines = []
|
||||||
|
for line in sql_text.split("\n"):
|
||||||
|
if line.strip().upper().startswith("DELIMITER"):
|
||||||
|
continue
|
||||||
|
lines.append(line.replace("$$", ";"))
|
||||||
|
sql_text = "\n".join(lines)
|
||||||
|
conn = get_connection(config)
|
||||||
|
results = []
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# mysql-connector-python 9.2+ 已移除 multi 参数,直接 execute 即可执行多条语句
|
||||||
|
cursor.execute(sql_text)
|
||||||
|
# 消费所有结果集(SELECT 返回行、DML 返回 affected rows)
|
||||||
|
while True:
|
||||||
|
if cursor.description:
|
||||||
|
results.append({"type": "query", "rowCount": len(cursor.fetchall())})
|
||||||
|
else:
|
||||||
|
results.append({"type": "execute", "affectedRows": cursor.rowcount})
|
||||||
|
if not cursor.nextset():
|
||||||
|
break
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "results": results}
|
||||||
|
except MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_table_comment(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""获取表注释。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
schema = args.schema or config["database"]
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
|
||||||
|
(schema, args.table),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "table": args.table, "schema": schema, "comment": row[0] if row else ""}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set_table_comment(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""修改表注释。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
schema = args.schema or config["database"]
|
||||||
|
sql = f"ALTER TABLE `{schema}`.`{args.table}` COMMENT = %s"
|
||||||
|
cursor.execute(sql, (args.comment,))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "table": args.table, "schema": schema}
|
||||||
|
except MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_column_comment(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""获取列注释。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
schema = args.schema or config["database"]
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COLUMN_COMMENT FROM information_schema.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
|
||||||
|
(schema, args.table, args.column),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "table": args.table, "column": args.column, "comment": row[0] if row else ""}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set_column_comment(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""修改列注释(需要 COLUMN_TYPE,可通过 describe_table 获取)。"""
|
||||||
|
conn = get_connection(config)
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
schema = args.schema or config["database"]
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
|
||||||
|
(schema, args.table, args.column),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
cursor.close()
|
||||||
|
return {"success": False, "error": f"Column {args.column} not found"}
|
||||||
|
col_type = row[0]
|
||||||
|
sql = f"ALTER TABLE `{schema}`.`{args.table}` MODIFY COLUMN `{args.column}` {col_type} COMMENT %s"
|
||||||
|
cursor.execute(sql, (args.comment,))
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "table": args.table, "column": args.column}
|
||||||
|
except MySQLError as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_test_connection(args: argparse.Namespace, config: dict) -> dict:
|
||||||
|
"""测试数据库连接。"""
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = get_connection(config)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT 1 AS conn_ok")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
return {"success": True, "message": "Connection OK", "conn_ok": row[0] if row else 1}
|
||||||
|
except MySQLError as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="G3FO Database Operations Tool")
|
||||||
|
parser.add_argument("--host", help="MySQL host (default: 192.168.3.233)")
|
||||||
|
parser.add_argument("--port", type=int, help="MySQL port (default: 3306)")
|
||||||
|
parser.add_argument("--user", help="MySQL user (default: root)")
|
||||||
|
parser.add_argument("--password", help="MySQL password")
|
||||||
|
parser.add_argument("--database", help="MySQL database (default: g3fo_base)")
|
||||||
|
parser.add_argument("--connection-timeout", type=int, dest="connection_timeout", help="Connection timeout in seconds (default: 10)")
|
||||||
|
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# query
|
||||||
|
p_query = subparsers.add_parser("query", help="Execute SELECT query")
|
||||||
|
p_query.add_argument("sql", help="SQL SELECT statement")
|
||||||
|
p_query.add_argument("--params", nargs="*", help="Query parameters")
|
||||||
|
|
||||||
|
# execute
|
||||||
|
p_execute = subparsers.add_parser("execute", help="Execute INSERT/UPDATE/DELETE")
|
||||||
|
p_execute.add_argument("sql", help="SQL statement")
|
||||||
|
p_execute.add_argument("--params", nargs="*", help="Query parameters")
|
||||||
|
|
||||||
|
# list_tables
|
||||||
|
subparsers.add_parser("list_tables", help="List all tables in database")
|
||||||
|
|
||||||
|
# describe_table
|
||||||
|
p_desc = subparsers.add_parser("describe_table", help="Get table structure")
|
||||||
|
p_desc.add_argument("table", help="Table name")
|
||||||
|
p_desc.add_argument("--schema", help="Schema/database (default: current database)")
|
||||||
|
|
||||||
|
# call_procedure
|
||||||
|
p_proc = subparsers.add_parser("call_procedure", help="Execute stored procedure")
|
||||||
|
p_proc.add_argument("procedure", help="Procedure name (e.g. InsertSystemMenu)")
|
||||||
|
p_proc.add_argument("--params", nargs="*", help="Procedure parameters")
|
||||||
|
|
||||||
|
# batch_execute
|
||||||
|
p_batch = subparsers.add_parser("batch_execute", help="Execute batch SQL")
|
||||||
|
p_batch.add_argument("sql", nargs="?", help="SQL text (multiple statements separated by ;)")
|
||||||
|
p_batch.add_argument("--file", "-f", help="SQL file path")
|
||||||
|
p_batch.add_argument("--continue-on-error", action="store_true", dest="continue_on_error", help="Continue on error")
|
||||||
|
|
||||||
|
# get_table_comment
|
||||||
|
p_gtc = subparsers.add_parser("get_table_comment", help="Get table comment")
|
||||||
|
p_gtc.add_argument("table", help="Table name")
|
||||||
|
p_gtc.add_argument("--schema", help="Schema (default: current database)")
|
||||||
|
|
||||||
|
# set_table_comment
|
||||||
|
p_stc = subparsers.add_parser("set_table_comment", help="Set table comment")
|
||||||
|
p_stc.add_argument("table", help="Table name")
|
||||||
|
p_stc.add_argument("comment", help="Comment text")
|
||||||
|
p_stc.add_argument("--schema", help="Schema (default: current database)")
|
||||||
|
|
||||||
|
# get_column_comment
|
||||||
|
p_gcc = subparsers.add_parser("get_column_comment", help="Get column comment")
|
||||||
|
p_gcc.add_argument("table", help="Table name")
|
||||||
|
p_gcc.add_argument("column", help="Column name")
|
||||||
|
p_gcc.add_argument("--schema", help="Schema (default: current database)")
|
||||||
|
|
||||||
|
# set_column_comment
|
||||||
|
p_scc = subparsers.add_parser("set_column_comment", help="Set column comment")
|
||||||
|
p_scc.add_argument("table", help="Table name")
|
||||||
|
p_scc.add_argument("column", help="Column name")
|
||||||
|
p_scc.add_argument("comment", help="Comment text")
|
||||||
|
p_scc.add_argument("--schema", help="Schema (default: current database)")
|
||||||
|
|
||||||
|
# test_connection
|
||||||
|
subparsers.add_parser("test_connection", help="Test database connection")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
config = get_connection_config(
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
user=args.user,
|
||||||
|
password=args.password,
|
||||||
|
database=args.database,
|
||||||
|
connection_timeout=getattr(args, "connection_timeout", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"query": cmd_query,
|
||||||
|
"execute": cmd_execute,
|
||||||
|
"list_tables": cmd_list_tables,
|
||||||
|
"describe_table": cmd_describe_table,
|
||||||
|
"call_procedure": cmd_call_procedure,
|
||||||
|
"batch_execute": cmd_batch_execute,
|
||||||
|
"get_table_comment": cmd_get_table_comment,
|
||||||
|
"set_table_comment": cmd_set_table_comment,
|
||||||
|
"get_column_comment": cmd_get_column_comment,
|
||||||
|
"set_column_comment": cmd_set_column_comment,
|
||||||
|
"test_connection": cmd_test_connection,
|
||||||
|
}
|
||||||
|
handler = commands[args.command]
|
||||||
|
result = handler(args, config)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
sys.exit(0 if result.get("success", False) else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
mysql-connector-python>=8.0.0
|
||||||
@@ -14,6 +14,7 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
|
|||||||
- **分析测试流程**:参考 `references/business_flows/_TEST_FLOW_TEMPLATE.md`。
|
- **分析测试流程**:参考 `references/business_flows/_TEST_FLOW_TEMPLATE.md`。
|
||||||
- **查标准运维流程**:去 `references/middleware/` 或 `references/system-init/`。
|
- **查标准运维流程**:去 `references/middleware/` 或 `references/system-init/`。
|
||||||
- **查中间件总览/高可用架构**:查阅 `references/middleware/middleware-comprehensive-guide.md`。
|
- **查中间件总览/高可用架构**:查阅 `references/middleware/middleware-comprehensive-guide.md`。
|
||||||
|
- **查中间件与标准组件版本**:查阅 `references/middleware/middleware-versions.md`(含 `g3fo-*-service` 应用版本、中间件与 JDK;**Web Server** 仅 Nginx / Vue / Flutter,其余为 **AP Server**;用户要「所有版本」时须分两类列出,见 §2.D)。
|
||||||
- **查具体环境/客户资产**:
|
- **查具体环境/客户资产**:
|
||||||
- 内部环境(Dev/UAT):查阅 `references/inventory/internal.md`。
|
- 内部环境(Dev/UAT):查阅 `references/inventory/internal.md`。
|
||||||
- 外部客户(客户A、B等):查阅 `references/inventory/clients/[客户名].md`。
|
- 外部客户(客户A、B等):查阅 `references/inventory/clients/[客户名].md`。
|
||||||
@@ -61,6 +62,13 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
|
|||||||
4. **表格生成**:汇总信息,输出包含“步骤”、“涉及服务”、“关键接口/代码逻辑”、“测试内容/预期结果”的表格。
|
4. **表格生成**:汇总信息,输出包含“步骤”、“涉及服务”、“关键接口/代码逻辑”、“测试内容/预期结果”的表格。
|
||||||
- **格式参考**:`references/business_flows/_TEST_FLOW_TEMPLATE.md`。
|
- **格式参考**:`references/business_flows/_TEST_FLOW_TEMPLATE.md`。
|
||||||
|
|
||||||
|
### D. 版本清单问答
|
||||||
|
1. **文档来源**:以 `references/middleware/middleware-versions.md` 为唯一标准版本表(除非用户指定环境资产清单覆盖)。
|
||||||
|
2. **角色区分**:
|
||||||
|
- **Web Server**:仅 **Nginx、Vue、Flutter**。
|
||||||
|
- **AP Server**:全部 **`g3fo-*-service`**、**全部中间件**(MySQL、Redis、RocketMQ、Nacos、PowerJob、Beszel、Dozzle、dufs 等)、**OpenJDK**。
|
||||||
|
3. **用户问「所有版本 / 完整版本 / 各组件版本号」等**:必须分两块输出,且标题或小节名明确为 **Web Server** 与 **AP Server**(可先 Web 后 AP,或先 AP 后 Web,但两类不可混为一张无标签表)。
|
||||||
|
|
||||||
## 3. 示例 Prompt (用户可参考)
|
## 3. 示例 Prompt (用户可参考)
|
||||||
- **业务咨询**:
|
- **业务咨询**:
|
||||||
- “我想测试下单业务流程,请分析代码并列出详细的测试步骤表格。”
|
- “我想测试下单业务流程,请分析代码并列出详细的测试步骤表格。”
|
||||||
@@ -71,6 +79,8 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
|
|||||||
- “请根据 `references/middleware/middleware-comprehensive-guide.md` 和 `references/middleware/nacos/deploy.md`,结合客户资产清单生成 Nacos 部署配置。”
|
- “请根据 `references/middleware/middleware-comprehensive-guide.md` 和 `references/middleware/nacos/deploy.md`,结合客户资产清单生成 Nacos 部署配置。”
|
||||||
- **故障排查**:
|
- **故障排查**:
|
||||||
- “我的 MySQL 出现了复制冲突,报错 `Duplicate entry`,请根据 `references/middleware/mysql/fault-analysis.md` 提供排查脚本 and 修复建议。”
|
- “我的 MySQL 出现了复制冲突,报错 `Duplicate entry`,请根据 `references/middleware/mysql/fault-analysis.md` 提供排查脚本 and 修复建议。”
|
||||||
|
- **版本清单**:
|
||||||
|
- “请根据 `middleware-versions.md` 列出当前约定的全部组件版本,并按 Web Server / AP Server 分开。”
|
||||||
|
|
||||||
## 4. 资源地图
|
## 4. 资源地图
|
||||||
- **业务与服务**:
|
- **业务与服务**:
|
||||||
@@ -79,6 +89,7 @@ description: g3fo 系统相关文档。管理业务流程说明、服务职责
|
|||||||
- **架构概览**: `architecture/domain_overview.md`
|
- **架构概览**: `architecture/domain_overview.md`
|
||||||
- **中间件标准**(部署/故障分析等详见各子目录):
|
- **中间件标准**(部署/故障分析等详见各子目录):
|
||||||
- **总览**:`middleware/middleware-comprehensive-guide.md`
|
- **总览**:`middleware/middleware-comprehensive-guide.md`
|
||||||
|
- **版本清单**:`middleware/middleware-versions.md`(Web:Nginx/Vue/Flutter;AP:`g3fo-*-service` + 中间件 + JDK)
|
||||||
- **Keepalived**: `middleware/keepalived/`
|
- **Keepalived**: `middleware/keepalived/`
|
||||||
- **MySQL**: `middleware/mysql/`
|
- **MySQL**: `middleware/mysql/`
|
||||||
- **Nacos**: `middleware/nacos/`
|
- **Nacos**: `middleware/nacos/`
|
||||||
|
|||||||
@@ -4,23 +4,26 @@
|
|||||||
|
|
||||||
## 故障场景表
|
## 故障场景表
|
||||||
|
|
||||||
| 场景编号 | 故障场景描述 | 节点 A 状态(MySQL/Keepalived) | 节点 B 状态(MySQL/Keepalived) | 节点 C 状态(Keepalived) | 各节点有效优先级 | VIP 最终归属 | 关键说明 |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
| 场景编号 | 故障场景描述 | 节点 A 状态(MySQL/Keepalived) | 节点 B 状态(MySQL/Keepalived) | 节点 C 状态(Keepalived) | 各节点有效优先级 | VIP 最终归属 | 关键说明 |
|
||||||
| 1 | 初始正常状态 | 正常/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级最高,成为 Master |
|
| ---- | ---------------------------- | ------------------------- | ------------------------- | ------------------- | ---------------- | --------- | ------------------------------ |
|
||||||
| 2 | A 的 MySQL 停机,Keepalived 正常 | 故障/正常 | 正常/正常 | 正常 | A=60、B=100、C=40 | Node B | A 扣权重后,B 优先级更高接管 VIP |
|
| 1 | 初始正常状态 | 正常/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级最高,成为 Master |
|
||||||
| 3 | 场景 2 后,A 的 MySQL 恢复 | 恢复/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级恢复,30 秒后抢占 VIP |
|
| 2 | A 的 MySQL 停机,Keepalived 正常 | 故障/正常 | 正常/正常 | 正常 | A=60、B=100、C=40 | Node B | A 扣权重后,B 优先级更高接管 VIP |
|
||||||
| 4 | A 的 Keepalived 停机 | 正常/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 无选举资格,B 接管 |
|
| 3 | 场景 2 后,A 的 MySQL 恢复 | 恢复/正常 | 正常/正常 | 正常 | A=110、B=100、C=40 | Node A | A 优先级恢复,30 秒后抢占 VIP |
|
||||||
| 5 | B 的 MySQL 停机 | 正常/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | B 扣权重后,A 仍为最高 |
|
| 4 | A 的 Keepalived 停机 | 正常/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 无选举资格,B 接管 |
|
||||||
| 6 | B 的 Keepalived 停机 | 正常/正常 | 正常/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 无选举资格,A 保持 Master |
|
| 5 | B 的 MySQL 停机 | 正常/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | B 扣权重后,A 仍为最高 |
|
||||||
| 7 | A、B MySQL 均停机 | 故障/正常 | 故障/正常 | 正常 | A=60、B=50、C=40 | Node A | 无可用 MySQL,但 Keepalived 仍按优先级选举 |
|
| 6 | B 的 Keepalived 停机 | 正常/正常 | 正常/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 无选举资格,A 保持 Master |
|
||||||
| 8 | A MySQL 停机 + B Keepalived 停机 | 故障/正常 | 无意义/故障(离线) | 正常 | A=60、B=离线、C=40 | Node A | B 离线,A 优先级高于 C(但 MySQL 不可用) |
|
| 7 | A、B MySQL 均停机 | 故障/正常 | 故障/正常 | 正常 | A=60、B=50、C=40 | Node A | 无可用 MySQL,但 Keepalived 仍按优先级选举 |
|
||||||
| 9 | A Keepalived 停机 + B MySQL 停机 | 正常/故障(离线) | 故障/正常 | 正常 | A=离线、B=50、C=40 | Node B | A 离线,B 优先级高于 C(但 MySQL 不可用) |
|
| 8 | A MySQL 停机 + B Keepalived 停机 | 故障/正常 | 无意义/故障(离线) | 正常 | A=60、B=离线、C=40 | Node A | B 离线,A 优先级高于 C(但 MySQL 不可用) |
|
||||||
| 10 | A、B Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 正常 | A=离线、B=离线、C=40 | 无节点绑定 VIP | C 无 VIP 配置,仅参与选举不持有 VIP |
|
| 9 | A Keepalived 停机 + B MySQL 停机 | 正常/故障(离线) | 故障/正常 | 正常 | A=离线、B=50、C=40 | Node B | A 离线,B 优先级高于 C(但 MySQL 不可用) |
|
||||||
| 11 | A MySQL+Keepalived 均停机 | 故障/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 完全离线,B 正常接管 |
|
| 10 | A、B Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 正常 | A=离线、B=离线、C=40 | 无节点绑定 VIP | C 无 VIP 配置,仅参与选举不持有 VIP |
|
||||||
| 12 | B MySQL+Keepalived 均停机 | 正常/正常 | 故障/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 完全离线,A 保持 Master |
|
| 11 | A MySQL+Keepalived 均停机 | 故障/故障(离线) | 正常/正常 | 正常 | A=离线、B=100、C=40 | Node B | A 完全离线,B 正常接管 |
|
||||||
| 13 | 场景 7 后,A MySQL 恢复 | 恢复/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | A 优先级恢复最高,接管 VIP |
|
| 12 | B MySQL+Keepalived 均停机 | 正常/正常 | 故障/故障(离线) | 正常 | A=110、B=离线、C=40 | Node A | B 完全离线,A 保持 Master |
|
||||||
| 14 | 场景 7 后,B MySQL 恢复 | 故障/正常 | 恢复/正常 | 正常 | A=60、B=100、C=40 | Node B | B 优先级高于 A,接管 VIP |
|
| 13 | 场景 7 后,A MySQL 恢复 | 恢复/正常 | 故障/正常 | 正常 | A=110、B=50、C=40 | Node A | A 优先级恢复最高,接管 VIP |
|
||||||
| 15 | C Keepalived 停机 | 正常/正常 | 正常/正常 | 故障(离线) | A=110、B=100、C=离线 | Node A | C 仅为仲裁,离线不影响 A/B 选举 |
|
| 14 | 场景 7 后,B MySQL 恢复 | 故障/正常 | 恢复/正常 | 正常 | A=60、B=100、C=40 | Node B | B 优先级高于 A,接管 VIP |
|
||||||
| 16 | A、B MySQL 均停机 + C 停机 | 故障/正常 | 故障/正常 | 故障(离线) | A=60、B=50、C=离线 | Node A | C 离线不影响 A/B 选举 |
|
| 15 | C Keepalived 停机 | 正常/正常 | 正常/正常 | 故障(离线) | A=110、B=100、C=离线 | Node A | C 仅为仲裁,离线不影响 A/B 选举 |
|
||||||
| 17 | 所有节点 Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 故障(离线) | 全离线 | 无节点绑定 VIP | 无 Keepalived 参与选举,VIP 失联 |
|
| 16 | A、B MySQL 均停机 + C 停机 | 故障/正常 | 故障/正常 | 故障(离线) | A=60、B=50、C=离线 | Node A | C 离线不影响 A/B 选举 |
|
||||||
| 18 | A 恢复但复制异常(check_preempt 失败) | 恢复/正常 | 正常/正常 | 正常 | A=90、B=100、C=40 | Node B | 副库端口正常但 A 本地复制异常,A 降权不抢占 |
|
| 17 | 所有节点 Keepalived 均停机 | 正常/故障(离线) | 正常/故障(离线) | 故障(离线) | 全离线 | 无节点绑定 VIP | 无 Keepalived 参与选举,VIP 失联 |
|
||||||
|
| 18 | A 恢复但复制异常(check_preempt 失败) | 恢复/正常 | 正常/正常 | 正常 | A=90、B=100、C=40 | Node B | 副库端口正常但 A 本地复制异常,A 降权不抢占 |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 中间件、应用服务与标准组件版本清单
|
||||||
|
|
||||||
|
本文档列出 g3fo 相关环境中**约定使用的镜像、发行或应用版本**,便于部署对齐、升级评估与问题排查。若与某客户现场或分支实际镜像不一致,以该环境资产清单或运行中的 `docker images` / 构建配置为准。
|
||||||
|
|
||||||
|
## 角色划分:AP Server 与 Web Server
|
||||||
|
|
||||||
|
| 角色 | 范围 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| **AP Server** | 全部 `g3fo-*-service` 业务应用、**全部中间件**(MySQL、Redis、RocketMQ、Nacos、PowerJob、观测与辅助组件等)、Java 运行时(OpenJDK) | 后端与基础设施侧;**不**含 Nginx / Vue / Flutter。 |
|
||||||
|
| **Web Server** | 仅 **Nginx**、**Vue**、**Flutter** | 反向代理 / 静态与前端构建链;与 AP 侧分开罗列。 |
|
||||||
|
|
||||||
|
**AI / 读者输出约定**:当用户询问「所有版本」「完整版本清单」「环境上各组件版本」等时,须**分两节**回答:**先列 Web Server**,**再列 AP Server**(或按用户要求的顺序,但必须明确标注两类,不可混成一张不分角色的总表)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web Server(仅下列三项)
|
||||||
|
|
||||||
|
| 组件 | 版本 / 标识 | 说明 |
|
||||||
|
| ---- | ------------- | ---- |
|
||||||
|
| Nginx | `1.27.3` | 反向代理 / 网关前置 |
|
||||||
|
| Vue | `3.5.17` | 前端框架(与构建依赖对齐) |
|
||||||
|
| Flutter | `3.38.4` | 移动端 / 跨端构建工具链 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AP Server
|
||||||
|
|
||||||
|
### 业务应用(`g3fo-*-service`)
|
||||||
|
|
||||||
|
以下应用版本统一为 **`1.5.1.22`**。
|
||||||
|
|
||||||
|
| 服务 | 版本 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| g3fo-trade-service | `1.5.1.22` |
|
||||||
|
| g3fo-product-service | `1.5.1.22` |
|
||||||
|
| g3fo-base-service | `1.5.1.22` |
|
||||||
|
| g3fo-admin-service | `1.5.1.22` |
|
||||||
|
| g3fo-monitor-service | `1.5.1.22` |
|
||||||
|
| g3fo-dx-service | `1.5.1.22` |
|
||||||
|
| g3fo-user-service | `1.5.1.22` |
|
||||||
|
| g3fo-margin-service | `1.5.1.22` |
|
||||||
|
| g3fo-notification-service | `1.5.1.22` |
|
||||||
|
| g3fo-exchange-fix-engine-service | `1.5.1.22` |
|
||||||
|
| g3fo-gateway-service | `1.5.1.22` |
|
||||||
|
| g3fo-push-service | `1.5.1.22` |
|
||||||
|
| g3fo-utility-service | `1.5.1.22` |
|
||||||
|
|
||||||
|
### 中间件与运行时(同属 AP Server 侧)
|
||||||
|
|
||||||
|
| 组件 | 版本 / 镜像标识 | 说明 |
|
||||||
|
| ---- | ---------------- | ---- |
|
||||||
|
| MySQL | `8.4.7` | 关系型数据库 |
|
||||||
|
| Redis | `8.4.0` | 缓存与会话等 |
|
||||||
|
| RocketMQ | `5.3.2` | 消息队列 |
|
||||||
|
| RocketMQ Dashboard | `2.1` | RocketMQ 管理控制台 |
|
||||||
|
| Nacos Server | `v3.1.1` | 注册与配置中心 |
|
||||||
|
| PowerJob Server | `v5.1.2` | 分布式任务调度 |
|
||||||
|
| Beszel | `v0.18.4` | 轻量监控相关(容器化部署时按此标签) |
|
||||||
|
| Dozzle | `v9.0.3` | 容器日志查看 |
|
||||||
|
| dufs | `v0.43.0` | 文件服务(静态/上传等场景) |
|
||||||
|
| OpenJDK | `amazoncorretto:17-al2023` | Java 运行时(Amazon Corretto 17,AL2023 基础镜像) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
- **中间件**:部署、Compose 或 K8s 清单中的镜像 tag 建议与本表一致,避免隐式 `latest`。
|
||||||
|
- **OpenJDK**:表内为 Docker 镜像习惯写法;本地非容器环境需安装同主版本 JDK 17 并与团队规范一致。
|
||||||
|
- **Vue / Flutter**:仅归入 **Web Server**;升级时需同步 CI、锁文件与团队文档。
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- 中间件高可用与部署总览:`middleware-comprehensive-guide.md`
|
||||||
|
- 各组件细则:同目录下 `mysql/`、`redis/`、`rocketmq/`、`nacos/`、`powerjob/` 等子目录
|
||||||
@@ -1,26 +1,32 @@
|
|||||||
# MySQL 8.4 双主(Source-Source)同步部署文档(跨主机统一目录/容器名版)
|
# MySQL 8.4 双主(Source-Source)同步部署文档(跨主机统一目录/容器名版)
|
||||||
|
|
||||||
> **AI 响应规范 (MySQL 执行策略)**:
|
> **AI 响应规范 (MySQL 执行策略)**:
|
||||||
|
>
|
||||||
> 1. **强制参数**: 所有 `docker exec` 命令必须包含 `-h127.0.0.1` 参数。
|
> 1. **强制参数**: 所有 `docker exec` 命令必须包含 `-h127.0.0.1` 参数。
|
||||||
> 2. **双重输出**: 涉及 SQL 操作时,必须同时输出 `Docker 执行命令` 和 `纯 SQL 脚本`。
|
> 2. **双重输出**: 涉及 SQL 操作时,必须同时输出 `Docker 执行命令` 和 `纯 SQL 脚本`。
|
||||||
|
|
||||||
## 文档概述
|
## 文档概述
|
||||||
|
|
||||||
你需要部署跨两台独立主机的 MySQL 8.4 双主双向复制集群,核心要求是:两台主机的容器名、目录路径统一使用 `mysql`(仅通过 IP 区分节点),保证两节点数据一致性,支持初始化、校验、宕机重启恢复及节点重建全流程。核心设计遵循:
|
你需要部署跨两台独立主机的 MySQL 8.4 双主双向复制集群,核心要求是:两台主机的容器名、目录路径统一使用 `mysql`(仅通过 IP 区分节点),保证两节点数据一致性,支持初始化、校验、宕机重启恢复及节点重建全流程。核心设计遵循:
|
||||||
|
|
||||||
+ **GTID + AUTO_POSITION**:重启后自动定位同步位点,减少人工干预;
|
- **GTID + AUTO_POSITION**:重启后自动定位同步位点,减少人工干预;
|
||||||
+ **ROW 模式 binlog**:避免非确定性函数导致的数据不一致;
|
- **ROW 模式 binlog**:避免非确定性函数导致的数据不一致;
|
||||||
+ **持久化数据卷**:独立 Volume 保障容器重启数据不丢失;
|
- **持久化数据卷**:独立 Volume 保障容器重启数据不丢失;
|
||||||
+ **自增键隔离**:通过步长/偏移配置避免双写主键冲突。
|
- **自增键隔离**:通过步长/偏移配置避免双写主键冲突。
|
||||||
|
|
||||||
## 1. 环境准备
|
## 1. 环境准备
|
||||||
|
|
||||||
### 1.1 节点信息(核心区分点)
|
### 1.1 节点信息(核心区分点)
|
||||||
| 节点 | 主机IP | server_id | auto_increment_offset | 核心标识 |
|
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
|
||||||
| 主节点A | ${NODE_A_IP} | 1 | 1 | 生成主键1、3、5… |
|
| 节点 | 主机IP | server_id | auto_increment_offset | 核心标识 |
|
||||||
| 主节点B | ${NODE_B_IP} | 2 | 2 | 生成主键2、4、6… |
|
| ---- | ------------ | --------- | --------------------- | ---------- |
|
||||||
|
| 主节点A | ${NODE_A_IP} | 1 | 1 | 生成主键1、3、5… |
|
||||||
|
| 主节点B | ${NODE_B_IP} | 2 | 2 | 生成主键2、4、6… |
|
||||||
|
|
||||||
|
|
||||||
### 1.2 系统依赖(两台主机统一执行)
|
### 1.2 系统依赖(两台主机统一执行)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 安装 Docker & Docker Compose、MySQL 客户端
|
# 安装 Docker & Docker Compose、MySQL 客户端
|
||||||
sudo apt update && sudo apt install -y docker.io docker-compose-plugin mysql-client
|
sudo apt update && sudo apt install -y docker.io docker-compose-plugin mysql-client
|
||||||
@@ -29,6 +35,7 @@ docker --version && docker compose version && mysql --version
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 1.3 目录规划(两台主机完全一致)
|
### 1.3 目录规划(两台主机完全一致)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 两台主机均执行以下命令,目录统一为 /data/mysql
|
# 两台主机均执行以下命令,目录统一为 /data/mysql
|
||||||
sudo mkdir -p /data/mysql/{data,backup,logs}
|
sudo mkdir -p /data/mysql/{data,backup,logs}
|
||||||
@@ -36,7 +43,9 @@ sudo chown 999:999 /data/mysql -R # MySQL 容器默认UID/GID为999,避免权
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 2. 双主节点部署
|
## 2. 双主节点部署
|
||||||
|
|
||||||
### 2.1 Docker Compose 配置(两台主机完全一致)
|
### 2.1 Docker Compose 配置(两台主机完全一致)
|
||||||
|
|
||||||
创建 `/data/mysql/docker-compose.yml`,内容如下(无节点差异):
|
创建 `/data/mysql/docker-compose.yml`,内容如下(无节点差异):
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
@@ -73,7 +82,9 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 MySQL 配置文件(my.cnf)
|
### 2.2 MySQL 配置文件(my.cnf)
|
||||||
|
|
||||||
#### 节点A(${NODE_A_IP}):`/data/mysql/my.cnf`
|
#### 节点A(${NODE_A_IP}):`/data/mysql/my.cnf`
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
[mysqld]
|
[mysqld]
|
||||||
# ========== 节点唯一标识(核心区分点) ==========
|
# ========== 节点唯一标识(核心区分点) ==========
|
||||||
@@ -120,6 +131,7 @@ super_read_only=OFF
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 节点B(${NODE_B_IP}):`/data/mysql/my.cnf`
|
#### 节点B(${NODE_B_IP}):`/data/mysql/my.cnf`
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
[mysqld]
|
[mysqld]
|
||||||
# ========== 节点唯一标识(核心区分点) ==========
|
# ========== 节点唯一标识(核心区分点) ==========
|
||||||
@@ -166,6 +178,7 @@ super_read_only=OFF
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 启动容器(两台主机统一执行)
|
### 2.3 启动容器(两台主机统一执行)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 进入目录并启动容器
|
# 进入目录并启动容器
|
||||||
cd /data/mysql && docker compose up -d
|
cd /data/mysql && docker compose up -d
|
||||||
@@ -175,7 +188,9 @@ docker ps | grep mysql
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 3. 双主复制初始化
|
## 3. 双主复制初始化
|
||||||
|
|
||||||
### 3.1 创建复制专用用户(两台主机统一执行)
|
### 3.1 创建复制专用用户(两台主机统一执行)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 无需区分节点,直接执行(创建repl用户,允许跨主机访问)
|
# 无需区分节点,直接执行(创建repl用户,允许跨主机访问)
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
@@ -186,7 +201,9 @@ FLUSH PRIVILEGES;
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 配置双向复制(核心:仅IP参数不同)
|
### 3.2 配置双向复制(核心:仅IP参数不同)
|
||||||
|
|
||||||
#### 步骤1:节点A(${NODE_A_IP})配置为节点B的从库
|
#### 步骤1:节点A(${NODE_A_IP})配置为节点B的从库
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
STOP REPLICA;
|
STOP REPLICA;
|
||||||
@@ -198,13 +215,14 @@ CHANGE REPLICATION SOURCE TO
|
|||||||
SOURCE_PASSWORD = 'afe123456',
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
SOURCE_AUTO_POSITION = 1, # GTID自动定位(无需手动找位点)
|
SOURCE_AUTO_POSITION = 1, # GTID自动定位(无需手动找位点)
|
||||||
SOURCE_CONNECT_RETRY = 10,
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
SOURCE_RETRY_COUNT = 86400,
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
GET_SOURCE_PUBLIC_KEY = 1; # 适配MySQL 8.0+密码认证
|
GET_SOURCE_PUBLIC_KEY = 1; # 适配MySQL 8.0+密码认证
|
||||||
START REPLICA;
|
START REPLICA;
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤2:节点B(${NODE_B_IP})配置为节点A的从库
|
#### 步骤2:节点B(${NODE_B_IP})配置为节点A的从库
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
STOP REPLICA;
|
STOP REPLICA;
|
||||||
@@ -216,38 +234,44 @@ CHANGE REPLICATION SOURCE TO
|
|||||||
SOURCE_PASSWORD = 'afe123456',
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
SOURCE_AUTO_POSITION = 1,
|
SOURCE_AUTO_POSITION = 1,
|
||||||
SOURCE_CONNECT_RETRY = 10,
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
SOURCE_RETRY_COUNT = 86400,
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
GET_SOURCE_PUBLIC_KEY = 1;
|
GET_SOURCE_PUBLIC_KEY = 1;
|
||||||
START REPLICA;
|
START REPLICA;
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 同步状态校验
|
## 4. 同步状态校验
|
||||||
|
|
||||||
### 4.1 核心校验命令(两台主机统一执行)
|
### 4.1 核心校验命令(两台主机统一执行)
|
||||||
|
|
||||||
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看复制状态
|
# 查看复制状态
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "SHOW REPLICA STATUS\G"
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "SHOW REPLICA STATUS\G"
|
||||||
```
|
```
|
||||||
|
|
||||||
**B. 纯 SQL 模式:**
|
**B. 纯 SQL 模式:**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SHOW REPLICA STATUS\G
|
SHOW REPLICA STATUS\G
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 关键校验字段(必须全部满足)
|
### 4.2 关键校验字段(必须全部满足)
|
||||||
| 字段 | 目标值 | 说明 |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| Replica_IO_Running | Yes | IO线程正常(接收binlog) |
|
| 字段 | 目标值 | 说明 |
|
||||||
| Replica_SQL_Running | Yes | SQL线程正常(执行事务) |
|
| --------------------- | -------------------- | ---------------- |
|
||||||
| Last_SQL_Error | 空 | 无同步错误 |
|
| Replica_IO_Running | Yes | IO线程正常(接收binlog) |
|
||||||
| Seconds_Behind_Source | 0 | 无同步延迟 |
|
| Replica_SQL_Running | Yes | SQL线程正常(执行事务) |
|
||||||
| Retrieved_Gtid_Set | 非空 | 已获取对端节点GTID |
|
| Last_SQL_Error | 空 | 无同步错误 |
|
||||||
| Executed_Gtid_Set | 包含Retrieved_Gtid_Set | 已执行所有获取的事务 |
|
| Seconds_Behind_Source | 0 | 无同步延迟 |
|
||||||
|
| Retrieved_Gtid_Set | 非空 | 已获取对端节点GTID |
|
||||||
|
| Executed_Gtid_Set | 包含Retrieved_Gtid_Set | 已执行所有获取的事务 |
|
||||||
|
|
||||||
|
|
||||||
### 4.3 数据一致性验证
|
### 4.3 数据一致性验证
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 节点A(${NODE_A_IP})创建测试数据
|
# 节点A(${NODE_A_IP})创建测试数据
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
@@ -266,8 +290,11 @@ docker exec -it mysql mysql -uroot -pafe123456 -h${NODE_A_IP} -e "SELECT * FROM
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 5. 故障处理流程
|
## 5. 故障处理流程
|
||||||
|
|
||||||
### 5.1 宕机重启恢复
|
### 5.1 宕机重启恢复
|
||||||
|
|
||||||
#### 场景:容器/主机重启后复制未自动恢复(两台主机操作逻辑一致,仅IP不同)
|
#### 场景:容器/主机重启后复制未自动恢复(两台主机操作逻辑一致,仅IP不同)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 以节点A(${NODE_A_IP})为例,恢复与节点B的复制关系
|
# 以节点A(${NODE_A_IP})为例,恢复与节点B的复制关系
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
@@ -289,7 +316,9 @@ SHOW REPLICA STATUS\G;
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 重建节点(单节点数据损坏/丢失)
|
### 5.2 重建节点(单节点数据损坏/丢失)
|
||||||
|
|
||||||
#### 步骤1:从正常节点全量备份(假设节点B损坏,从节点A备份)
|
#### 步骤1:从正常节点全量备份(假设节点B损坏,从节点A备份)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 节点A(${NODE_A_IP})执行备份
|
# 节点A(${NODE_A_IP})执行备份
|
||||||
docker exec -it mysql mysqldump -uroot -pafe123456 -h127.0.0.1 --all-databases --master-data=2 --single-transaction > /data/mysql/backup/full_backup.sql
|
docker exec -it mysql mysqldump -uroot -pafe123456 -h127.0.0.1 --all-databases --master-data=2 --single-transaction > /data/mysql/backup/full_backup.sql
|
||||||
@@ -299,6 +328,7 @@ scp /data/mysql/backup/full_backup.sql root@${NODE_B_IP}:/data/mysql/backup/
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤2:停止损坏节点并恢复数据(节点B执行)
|
#### 步骤2:停止损坏节点并恢复数据(节点B执行)
|
||||||
|
|
||||||
```plain
|
```plain
|
||||||
# 停止mysql容器
|
# 停止mysql容器
|
||||||
cd /data/mysql && docker compose down
|
cd /data/mysql && docker compose down
|
||||||
@@ -314,24 +344,34 @@ docker exec -i mysql mysql -uroot -pafe123456 < /data/mysql/backup/full_backup.s
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤3:重新配置双向复制
|
#### 步骤3:重新配置双向复制
|
||||||
|
|
||||||
参考「3.2 配置双向复制」,重新执行节点B作为节点A从库、节点A作为节点B从库的配置命令(仅IP参数区分)。
|
参考「3.2 配置双向复制」,重新执行节点B作为节点A从库、节点A作为节点B从库的配置命令(仅IP参数区分)。
|
||||||
|
|
||||||
### 5.3 复制冲突解决
|
### 5.3 复制冲突解决
|
||||||
|
|
||||||
详细的冲突定位与修复流程,请参考:[MySQL 复制冲突解决指南](./fault-analysis.md)
|
详细的冲突定位与修复流程,请参考:[MySQL 复制冲突解决指南](./fault-analysis.md)
|
||||||
|
|
||||||
## 6. 日志管理
|
## 6. 日志管理
|
||||||
|
|
||||||
详细的日志轮转与安全登录配置,请参考:[MySQL 日志管理指南](./log-management.md)
|
详细的日志轮转与安全登录配置,请参考:[MySQL 日志管理指南](./log-management.md)
|
||||||
|
|
||||||
## 7. 重要提醒
|
## 7. 重要提醒
|
||||||
|
|
||||||
### 7.1 核心区分点(避免配置错误)
|
### 7.1 核心区分点(避免配置错误)
|
||||||
+ 两台主机仅 `server_id`、`report_host`(IP)、`auto_increment_offset` 三个参数不同,其余配置完全一致;
|
|
||||||
+ 容器名、目录路径、端口均统一为 `mysql`/`/data/mysql`/3306,通过主机IP区分节点。
|
- 两台主机仅 `server_id`、`report_host`(IP)、`auto_increment_offset` 三个参数不同,其余配置完全一致;
|
||||||
|
- 容器名、目录路径、端口均统一为 `mysql`/`/data/mysql`/3306,通过主机IP区分节点。
|
||||||
|
|
||||||
### 7.2 一致性限制
|
### 7.2 一致性限制
|
||||||
+ MySQL原生异步双主复制**无法保证严格的数据一致性**,双写场景仍可能出现冲突;
|
|
||||||
+ 生产环境建议**单写多读**(指定一个节点为写节点,另一个为读节点),避免双写冲突。
|
- MySQL原生异步双主复制**无法保证严格的数据一致性**,双写场景仍可能出现冲突;
|
||||||
|
- 生产环境建议**单写多读**(指定一个节点为写节点,另一个为读节点),避免双写冲突。
|
||||||
|
|
||||||
#### 总结
|
#### 总结
|
||||||
|
|
||||||
#### 跨主机双主复制的核心差异仅为 `server_id`、主机IP、自增偏移量,其余目录/容器名/配置可完全统一;
|
#### 跨主机双主复制的核心差异仅为 `server_id`、主机IP、自增偏移量,其余目录/容器名/配置可完全统一;
|
||||||
|
|
||||||
#### 同步故障优先通过错误日志定位冲突,修复数据而非跳过事务(避免永久不一致);
|
#### 同步故障优先通过错误日志定位冲突,修复数据而非跳过事务(避免永久不一致);
|
||||||
|
|
||||||
#### GTID模式下无需手动定位同步位点,重启/重建节点时仅需重新指定对端IP即可自动恢复复制。
|
#### GTID模式下无需手动定位同步位点,重启/重建节点时仅需重新指定对端IP即可自动恢复复制。
|
||||||
|
|
||||||
|
|||||||
@@ -150,9 +150,28 @@ scp /data/backup/mysql/20250127120000.7z root@192.168.1.100:/data/backup/mysql/
|
|||||||
|
|
||||||
**注意**: 确保还原机的 `/data/backup/mysql/` 目录存在且有写入权限。
|
**注意**: 确保还原机的 `/data/backup/mysql/` 目录存在且有写入权限。
|
||||||
|
|
||||||
#### 步骤 3: 在还原机上执行还原
|
#### 步骤 3: 在还原机上还原前准备(清空数据并重启 MySQL)
|
||||||
|
|
||||||
在需要还原的 MySQL 服务器(从库)上执行还原脚本:
|
在需要还原的 MySQL 服务器(还原机)上,**先清空数据目录并重启容器**,再执行还原脚本,避免旧数据与备份冲突。
|
||||||
|
|
||||||
|
1. **删除数据目录中的现有数据**(在还原机执行):
|
||||||
|
```bash
|
||||||
|
# 删除 /data/mysql/data 下数据(请确认该路径为当前 MySQL 数据目录)
|
||||||
|
rm -rf /data/mysql/data/*
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **在 docker-compose 目录下重启 MySQL 容器**(在还原机执行):
|
||||||
|
```bash
|
||||||
|
cd /data/docker-compose
|
||||||
|
docker-compose down mysql
|
||||||
|
docker-compose up -d mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
等待 MySQL 启动就绪(可查看容器日志或稍等 10~30 秒)后,再进行步骤 4 的还原。
|
||||||
|
|
||||||
|
#### 步骤 4: 在还原机上执行还原脚本
|
||||||
|
|
||||||
|
在还原机上执行还原脚本:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /data/backup/mysql
|
cd /data/backup/mysql
|
||||||
@@ -171,50 +190,170 @@ cd /data/backup/mysql
|
|||||||
- 导入备份数据(导入时禁用 binlog,避免循环复制)
|
- 导入备份数据(导入时禁用 binlog,避免循环复制)
|
||||||
- 设置 GTID purged(从备份时的 GTID 位置)
|
- 设置 GTID purged(从备份时的 GTID 位置)
|
||||||
- 恢复 MySQL 为读写模式
|
- 恢复 MySQL 为读写模式
|
||||||
- **自动启动复制并检查状态**
|
|
||||||
|
|
||||||
#### 步骤 4: 在备份机上启动同步(重要)
|
#### 步骤 5: 还原后创建账号(在还原机执行)
|
||||||
|
|
||||||
还原完成后,还原脚本会自动启动复制并显示同步状态。确认看到同步成功后,**需要在备份机(主库)上执行以下操作**:
|
还原完成后,在**还原机**上创建复制与业务所需账号(若备份中已含用户则可酌情跳过,建议按环境统一执行一次)。
|
||||||
|
|
||||||
|
**A. 创建复制账号 `repl` 与日志刷新账号 `log_flush`**
|
||||||
|
|
||||||
|
Docker 执行:
|
||||||
|
```bash
|
||||||
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
|
CREATE USER 'repl'@'%' IDENTIFIED BY 'afe123456';
|
||||||
|
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
"
|
||||||
|
|
||||||
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
|
CREATE USER 'log_flush'@'127.0.0.1' IDENTIFIED BY 'afe123456';
|
||||||
|
GRANT RELOAD ON *.* TO 'log_flush'@'127.0.0.1';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. 创建 G3SF 相关业务账号**
|
||||||
|
|
||||||
|
Docker 执行:
|
||||||
|
```bash
|
||||||
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
|
CREATE USER 'g3sf'@'%' IDENTIFIED BY 'k2-fkW@7z>zSYQo';
|
||||||
|
GRANT ALL PRIVILEGES ON *.* TO 'g3sf'@'%';
|
||||||
|
CREATE USER 'g3sfreader'@'%' IDENTIFIED BY '@feg3sf10port';
|
||||||
|
GRANT SELECT ON *.* TO 'g3sfreader'@'%';
|
||||||
|
CREATE USER 'g3sfsupport'@'%' IDENTIFIED BY '@feg3sf10port';
|
||||||
|
GRANT ALL PRIVILEGES ON *.* TO 'g3sfsupport'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
纯 SQL(在 MySQL 客户端中执行):
|
||||||
|
```sql
|
||||||
|
-- g3sf:应用库完全访问
|
||||||
|
CREATE USER 'g3sf'@'%' IDENTIFIED BY 'k2-fkW@7z>zSYQo';
|
||||||
|
GRANT ALL PRIVILEGES ON *.* TO 'g3sf'@'%';
|
||||||
|
-- g3sfreader:只读
|
||||||
|
CREATE USER 'g3sfreader'@'%' IDENTIFIED BY '@feg3sf10port';
|
||||||
|
GRANT SELECT ON *.* TO 'g3sfreader'@'%';
|
||||||
|
-- g3sfsupport:支持库完全访问
|
||||||
|
CREATE USER 'g3sfsupport'@'%' IDENTIFIED BY '@feg3sf10port';
|
||||||
|
GRANT ALL PRIVILEGES ON *.* TO 'g3sfsupport'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 6: 在还原机上连接主 DB 并启动复制(在还原机执行)
|
||||||
|
|
||||||
|
在**还原机**上配置复制源为主库(将 `NODE_A_IP` 替换为实际主 DB 的 IP):
|
||||||
|
|
||||||
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
||||||
```bash
|
```bash
|
||||||
|
# 将 NODE_A_IP 替换为主 DB 的 IP
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO
|
||||||
|
SOURCE_HOST = 'NODE_A_IP',
|
||||||
|
SOURCE_PORT = 3306,
|
||||||
|
SOURCE_USER = 'repl',
|
||||||
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
|
SOURCE_AUTO_POSITION = 1,
|
||||||
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
|
GET_SOURCE_PUBLIC_KEY = 1;
|
||||||
START REPLICA;
|
START REPLICA;
|
||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
**B. 纯 SQL 模式:**
|
**B. 纯 SQL 模式:**
|
||||||
```sql
|
```sql
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO
|
||||||
|
SOURCE_HOST = 'NODE_A_IP', -- 主 DB IP
|
||||||
|
SOURCE_PORT = 3306,
|
||||||
|
SOURCE_USER = 'repl',
|
||||||
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
|
SOURCE_AUTO_POSITION = 1,
|
||||||
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
|
GET_SOURCE_PUBLIC_KEY = 1;
|
||||||
START REPLICA;
|
START REPLICA;
|
||||||
```
|
```
|
||||||
|
|
||||||
等待 3 秒后,检查同步状态:
|
等待几秒后检查复制状态,确认 IO/SQL 均为 Yes:
|
||||||
|
|
||||||
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
|
||||||
```bash
|
```bash
|
||||||
sleep 3
|
sleep 5
|
||||||
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "SHOW REPLICA STATUS\G"
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "SHOW REPLICA STATUS\G"
|
||||||
```
|
```
|
||||||
|
|
||||||
**B. 纯 SQL 模式:**
|
|
||||||
```sql
|
|
||||||
-- 等待 3 秒后执行
|
|
||||||
SHOW REPLICA STATUS\G;
|
|
||||||
```
|
|
||||||
|
|
||||||
检查关键指标:
|
检查关键指标:
|
||||||
- `Replica_IO_Running`: 应为 `Yes`
|
- `Replica_IO_Running`: 应为 `Yes`
|
||||||
- `Replica_SQL_Running`: 应为 `Yes`
|
- `Replica_SQL_Running`: 应为 `Yes`
|
||||||
- `Last_IO_Error`: 应为空
|
- `Last_IO_Error` / `Last_SQL_Error`: 应为空
|
||||||
- `Last_SQL_Error`: 应为空
|
|
||||||
- `Seconds_Behind_Source`: 延迟秒数(应逐渐减小)
|
状态正常后,再进行步骤 7,在主库上配置指向本机(还原机/备份库)。
|
||||||
|
|
||||||
|
#### 步骤 7: 在主库上配置指向备份库(还原机)并检查状态(在主库执行)
|
||||||
|
|
||||||
|
在**主备份数据库(主库)**上配置复制源为当前还原机(备份库),将 `NODE_B_IP` 替换为还原机/备份 DB 的 IP:
|
||||||
|
|
||||||
|
**A. Docker 执行模式 (包含 -h127.0.0.1):**
|
||||||
|
```bash
|
||||||
|
# 将 NODE_B_IP 替换为备份 DB(还原机)的 IP
|
||||||
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO
|
||||||
|
SOURCE_HOST = 'NODE_B_IP',
|
||||||
|
SOURCE_PORT = 3306,
|
||||||
|
SOURCE_USER = 'repl',
|
||||||
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
|
SOURCE_AUTO_POSITION = 1,
|
||||||
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
|
GET_SOURCE_PUBLIC_KEY = 1;
|
||||||
|
START REPLICA;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. 纯 SQL 模式:**
|
||||||
|
```sql
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO
|
||||||
|
SOURCE_HOST = 'NODE_B_IP', -- 备份 DB(还原机)IP
|
||||||
|
SOURCE_PORT = 3306,
|
||||||
|
SOURCE_USER = 'repl',
|
||||||
|
SOURCE_PASSWORD = 'afe123456',
|
||||||
|
SOURCE_AUTO_POSITION = 1,
|
||||||
|
SOURCE_CONNECT_RETRY = 10,
|
||||||
|
SOURCE_RETRY_COUNT = 9999999999,
|
||||||
|
GET_SOURCE_PUBLIC_KEY = 1;
|
||||||
|
START REPLICA;
|
||||||
|
```
|
||||||
|
|
||||||
|
等待几秒后再次检查主库复制状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sleep 5
|
||||||
|
docker exec -it mysql mysql -uroot -pafe123456 -h127.0.0.1 -e "SHOW REPLICA STATUS\G"
|
||||||
|
```
|
||||||
|
|
||||||
|
确认:
|
||||||
|
- `Replica_IO_Running`: 应为 `Yes`
|
||||||
|
- `Replica_SQL_Running`: 应为 `Yes`
|
||||||
|
- `Last_IO_Error` / `Last_SQL_Error`: 应为空
|
||||||
|
- `Seconds_Behind_Source`: 延迟应逐渐减小
|
||||||
|
|
||||||
|
至此,全量备份还原与主从(含主库侧)复制配置完成。
|
||||||
|
|
||||||
### 2.3 注意事项
|
### 2.3 注意事项
|
||||||
|
|
||||||
1. **备份时机**: 建议在业务低峰期执行备份,减少对业务的影响
|
1. **备份时机**: 建议在业务低峰期执行备份,减少对业务的影响
|
||||||
2. **网络传输**: 确保备份机和还原机之间网络畅通,备份文件较大时传输可能需要较长时间
|
2. **网络传输**: 确保备份机和还原机之间网络畅通,备份文件较大时传输可能需要较长时间
|
||||||
3. **磁盘空间**: 确保还原机有足够的磁盘空间存放备份文件和临时解压文件
|
3. **磁盘空间**: 确保还原机有足够的磁盘空间存放备份文件和临时解压文件;还原前清空 `/data/mysql/data` 前请确认该路径为当前 MySQL 数据目录
|
||||||
4. **权限要求**: 脚本需要 MySQL root 权限,以及 `/data/backup/mysql/` 目录的读写权限
|
4. **权限要求**: 脚本需要 MySQL root 权限,以及 `/data/backup/mysql/`、`/data/mysql/data` 等目录的相应权限
|
||||||
5. **GTID 一致性**: 还原脚本会自动处理 GTID,确保复制能够正常继续
|
5. **GTID 一致性**: 还原脚本会自动处理 GTID,确保复制能够正常继续
|
||||||
6. **备份机同步**: 还原完成后务必在备份机(主库)上执行 `START REPLICA` 并检查状态,确保双向复制正常
|
6. **还原前准备**: 还原前必须在还原机上删除 `/data/mysql/data` 数据并重启 MySQL 容器(步骤 3),再执行还原脚本,避免旧数据与备份冲突
|
||||||
|
7. **账号与复制顺序**: 还原后先创建账号(步骤 5),再在还原机配置指向主库的复制并检查状态(步骤 6),最后在主库上配置指向还原机的复制并检查状态(步骤 7),确保双向复制正常
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
# Redis Sentinel 高可用架构部署与运维手册
|
# Redis Sentinel 高可用架构部署与运维手册
|
||||||
|
|
||||||
## 一、概述
|
## 一、概述
|
||||||
|
|
||||||
### 1.1 核心功能
|
### 1.1 核心功能
|
||||||
|
|
||||||
Redis Sentinel(哨兵)是 Redis 的高可用解决方案。其核心目标是实现主从切换的自动化,确保系统在主节点故障时能够自动选举新的主节点并恢复服务。
|
Redis Sentinel(哨兵)是 Redis 的高可用解决方案。其核心目标是实现主从切换的自动化,确保系统在主节点故障时能够自动选举新的主节点并恢复服务。
|
||||||
|
|
||||||
### 1.2 适用场景
|
### 1.2 适用场景
|
||||||
|
|
||||||
- 生产环境下的 Redis 高可用需求。
|
- 生产环境下的 Redis 高可用需求。
|
||||||
- 需要自动故障转移(Failover)的分布式系统。
|
- 需要自动故障转移(Failover)的分布式系统。
|
||||||
- 读写分离架构。
|
- 读写分离架构。
|
||||||
|
|
||||||
### 1.3 前置条件
|
### 1.3 前置条件
|
||||||
|
|
||||||
- **运行环境**:Docker & Docker Compose。
|
- **运行环境**:Docker & Docker Compose。
|
||||||
- **镜像版本**:推荐使用 Redis 8.4.0 及以上版本。
|
- **镜像版本**:推荐使用 Redis 8.4.0 及以上版本。
|
||||||
- **网络规划**:所有节点需网络互通,且需明确各宿主机的外部 IP。
|
- **网络规划**:所有节点需网络互通,且需明确各宿主机的外部 IP。
|
||||||
@@ -17,9 +21,11 @@ Redis Sentinel(哨兵)是 Redis 的高可用解决方案。其核心目标
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 二、环境准备
|
## 二、环境准备
|
||||||
|
|
||||||
在所有节点(主节点、从节点、哨兵节点)上执行目录初始化及权限设置。
|
在所有节点(主节点、从节点、哨兵节点)上执行目录初始化及权限设置。
|
||||||
|
|
||||||
### 2.1 目录结构配置
|
### 2.1 目录结构配置
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建配置、数据、日志目录
|
# 创建配置、数据、日志目录
|
||||||
mkdir -p /data/redis/{conf,data,logs}
|
mkdir -p /data/redis/{conf,data,logs}
|
||||||
@@ -34,9 +40,11 @@ chmod -R 750 /data/redis
|
|||||||
## 三、部署流程
|
## 三、部署流程
|
||||||
|
|
||||||
### 3.1 Docker 服务编排
|
### 3.1 Docker 服务编排
|
||||||
|
|
||||||
使用 Docker Compose 部署 Redis 服务及 Sentinel 节点。
|
使用 Docker Compose 部署 Redis 服务及 Sentinel 节点。
|
||||||
|
|
||||||
**docker-compose.yml 示例:**
|
**docker-compose.yml 示例:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
@@ -49,9 +57,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone
|
||||||
- /data/redis/conf:/etc/redis
|
- /data/redis/conf/redis.conf:/etc/redis/redis.conf
|
||||||
- /data/redis/data:/data
|
- /data/redis/data:/data
|
||||||
- /data/redis/logs:/var/log/redis
|
- /data/redis/logs:/var/log/redis
|
||||||
command: redis-server /etc/redis/redis.conf
|
command: redis-server /etc/redis/redis.conf
|
||||||
@@ -66,7 +74,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "26379:26379"
|
- "26379:26379"
|
||||||
volumes:
|
volumes:
|
||||||
- /data/redis/conf:/etc/redis
|
- /etc/localtime:/etc/localtime
|
||||||
|
- /etc/timezone:/etc/timezone
|
||||||
|
- /data/redis/conf/sentinel.conf:/etc/redis/sentinel.conf
|
||||||
- /data/redis/logs:/var/log/redis
|
- /data/redis/logs:/var/log/redis
|
||||||
command: redis-sentinel /etc/redis/sentinel.conf
|
command: redis-sentinel /etc/redis/sentinel.conf
|
||||||
networks:
|
networks:
|
||||||
@@ -82,6 +92,7 @@ networks:
|
|||||||
## 四、核心配置说明
|
## 四、核心配置说明
|
||||||
|
|
||||||
### 4.1 Redis 服务端配置 (`redis.conf`)
|
### 4.1 Redis 服务端配置 (`redis.conf`)
|
||||||
|
|
||||||
```Properties
|
```Properties
|
||||||
# 基础配置
|
# 基础配置
|
||||||
bind 0.0.0.0
|
bind 0.0.0.0
|
||||||
@@ -142,7 +153,9 @@ auto-aof-rewrite-min-size 64mb
|
|||||||
|
|
||||||
notify-keyspace-events AE
|
notify-keyspace-events AE
|
||||||
```
|
```
|
||||||
|
|
||||||
从节点添加配置
|
从节点添加配置
|
||||||
|
|
||||||
```Properties
|
```Properties
|
||||||
# 指定广播地址,即使 Docker 内部获取到的是 172.x,也强制告诉 Sentinel 我是 ${REDIS_SLAVE_IP}
|
# 指定广播地址,即使 Docker 内部获取到的是 172.x,也强制告诉 Sentinel 我是 ${REDIS_SLAVE_IP}
|
||||||
replica-announce-ip ${REDIS_SLAVE_IP}
|
replica-announce-ip ${REDIS_SLAVE_IP}
|
||||||
@@ -152,22 +165,26 @@ replicaof ${REDIS_MASTER_IP} 6379
|
|||||||
# 从节点只读(默认开启,避免误写)
|
# 从节点只读(默认开启,避免误写)
|
||||||
replica-read-only yes
|
replica-read-only yes
|
||||||
```
|
```
|
||||||
|
|
||||||
以下为生产环境推荐的核心配置项:
|
以下为生产环境推荐的核心配置项:
|
||||||
|
|
||||||
| 配置项 | 说明 | 推荐值 |
|
|
||||||
| :--- | :--- | :--- |
|
| 配置项 | 说明 | 推荐值 |
|
||||||
| `bind` | 绑定 IP 地址 | `0.0.0.0` |
|
| ---------------------- | ------------------------- | ------------------ |
|
||||||
| `protected-mode` | 保护模式 | `no` |
|
| `bind` | 绑定 IP 地址 | `0.0.0.0` |
|
||||||
| `port` | 监听端口 | `6379` |
|
| `protected-mode` | 保护模式 | `no` |
|
||||||
| `replica-announce-ip` | 宣告外部 IP(解决 Docker NAT 问题) | 宿主机实际 IP |
|
| `port` | 监听端口 | `6379` |
|
||||||
| `requirepass` | 服务访问密码 | 自定义强密码 |
|
| `replica-announce-ip` | 宣告外部 IP(解决 Docker NAT 问题) | 宿主机实际 IP |
|
||||||
| `masterauth` | 主从同步授权密码 | 与 `requirepass` 一致 |
|
| `requirepass` | 服务访问密码 | 自定义强密码 |
|
||||||
| `appendonly` | 开启 AOF 持久化 | `yes` |
|
| `masterauth` | 主从同步授权密码 | 与 `requirepass` 一致 |
|
||||||
| `aof-use-rdb-preamble` | 开启混合持久化 | `yes` |
|
| `appendonly` | 开启 AOF 持久化 | `yes` |
|
||||||
|
| `aof-use-rdb-preamble` | 开启混合持久化 | `yes` |
|
||||||
|
|
||||||
|
|
||||||
> **注意:** 在从节点配置中,必须包含 `replicaof <master-ip> 6379` 以建立主从关系。
|
> **注意:** 在从节点配置中,必须包含 `replicaof <master-ip> 6379` 以建立主从关系。
|
||||||
|
|
||||||
### 4.2 哨兵配置 (`sentinel.conf`)
|
### 4.2 哨兵配置 (`sentinel.conf`)
|
||||||
|
|
||||||
```Properties
|
```Properties
|
||||||
# 基础配置
|
# 基础配置
|
||||||
bind 0.0.0.0
|
bind 0.0.0.0
|
||||||
@@ -201,19 +218,100 @@ sentinel failover-timeout mymaster 180000
|
|||||||
# 禁止Sentinel在故障转移后自动重配置主节点(Docker环境下无需开启)
|
# 禁止Sentinel在故障转移后自动重配置主节点(Docker环境下无需开启)
|
||||||
sentinel deny-scripts-reconfig yes
|
sentinel deny-scripts-reconfig yes
|
||||||
```
|
```
|
||||||
|
|
||||||
哨兵节点用于监控主节点状态并协调切换。
|
哨兵节点用于监控主节点状态并协调切换。
|
||||||
|
|
||||||
| 配置项 | 说明 | 示例值 |
|
|
||||||
| :--- | :--- | :--- |
|
| 配置项 | 说明 | 示例值 |
|
||||||
| `sentinel monitor` | 监控主节点(名称、IP、端口、法定人数) | `mymaster ${REDIS_MASTER_IP} 6379 2` |
|
| ---------------------------------- | -------------------- | ------------------------------------ |
|
||||||
| `sentinel auth-pass` | 主节点访问密码 | `mymaster afe123456` |
|
| `sentinel monitor` | 监控主节点(名称、IP、端口、法定人数) | `mymaster ${REDIS_MASTER_IP} 6379 2` |
|
||||||
| `sentinel down-after-milliseconds` | 故障判定超时时间(毫秒) | `30000` |
|
| `sentinel auth-pass` | 主节点访问密码 | `mymaster afe123456` |
|
||||||
| `sentinel failover-timeout` | 故障转移超时时间 | `180000` |
|
| `sentinel down-after-milliseconds` | 故障判定超时时间(毫秒) | `30000` |
|
||||||
| `sentinel announce-ip` | 宣告哨兵外部 IP | 宿主机实际 IP |
|
| `sentinel failover-timeout` | 故障转移超时时间 | `180000` |
|
||||||
|
| `sentinel announce-ip` | 宣告哨兵外部 IP | 宿主机实际 IP |
|
||||||
|
|
||||||
|
### 4.3 哨兵数量与 Master 选举
|
||||||
|
|
||||||
|
#### 如何选出新的 Redis Master(故障转移)
|
||||||
|
|
||||||
|
这里的「Master」指 **Redis 主节点**(可写的那台),不是哨兵本身。流程简述如下:
|
||||||
|
|
||||||
|
1. **判定主节点下线**
|
||||||
|
每个哨兵定期探测 Redis 主节点。当某个哨兵认为主节点 **主观下线(SDOWN)** 后,会向其他哨兵询问;当达到 **quorum** 个哨兵都认为主节点不可达时,主节点被判定为 **客观下线(ODOWN)**。
|
||||||
|
|
||||||
|
2. **选举 Sentinel 领导者**
|
||||||
|
哨兵之间通过 Raft 式选举选出一个 **Leader**,由它来执行后续故障转移(只会有唯一一个 Sentinel 在执行 failover)。
|
||||||
|
|
||||||
|
3. **选出新的 Redis Master**
|
||||||
|
Leader 在**当前存活的从节点**中选一个提升为新主,选择规则通常为:
|
||||||
|
- 若配置了 `replica-priority`(哨兵侧为 `sentinel replica-priority`),优先选优先级最高且可达的从节点;
|
||||||
|
- 否则按**复制偏移量**优先选数据最新的从节点;
|
||||||
|
- 再按 `runid` 字典序等做 tie-break。
|
||||||
|
选好后向该从节点下发 `REPLICAOF NO ONE`,使其成为新主,其余从节点改为复制新主。
|
||||||
|
|
||||||
|
所以:**“选出 master 节点” = 由 Sentinel Leader 在从节点里按优先级/复制进度选一个,并执行提升为主。**
|
||||||
|
|
||||||
|
#### 是否一定要三台哨兵以上?
|
||||||
|
|
||||||
|
| 哨兵数量 | quorum 典型值 | 说明 |
|
||||||
|
|----------|----------------|------|
|
||||||
|
| **1 台** | 1 | 可以触发故障转移,但哨兵单点故障则无人做 failover,**不推荐生产**。 |
|
||||||
|
| **2 台** | 1 或 2 | quorum=1:能 failover,但两台意见不一致时可能脑裂;quorum=2:需两台都同意才 failover,任一台宕机则永远达不到 2,**无法触发故障转移**。 |
|
||||||
|
| **3 台** | **2** | 至少 2 台同意才客观下线并执行 failover;允许 1 台哨兵宕机仍可完成选举与切换,**生产推荐最少 3 台 + quorum=2**。 |
|
||||||
|
| 5 台 | 3 | 可容忍 2 台哨兵同时宕机。 |
|
||||||
|
|
||||||
|
**结论**:
|
||||||
|
- **不是“必须大于 3 台”**,但 **1 台无高可用、2 台难以兼顾安全与可用**,因此 **至少 3 台哨兵 + quorum=2** 是常见最小生产配置,这样既能选出新 master,又能在挂掉 1 台哨兵时继续工作。
|
||||||
|
|
||||||
|
quorum 在配置里即 `sentinel monitor mymaster <ip> <port> <quorum>` 的最后一个数字,例如:
|
||||||
|
|
||||||
|
```Properties
|
||||||
|
sentinel monitor mymaster 192.168.3.200 6379 2
|
||||||
|
```
|
||||||
|
|
||||||
|
表示至少 **2 个**哨兵认为主节点下线,才会触发客观下线和后续的 Master 选举与故障转移。
|
||||||
|
|
||||||
|
#### 两哨兵存活却未选出主节点时的排查
|
||||||
|
|
||||||
|
当前是**两台 Redis 主机 + 两台哨兵**且两台哨兵都存活时,若一直没选出新主,可按下面逐项查。
|
||||||
|
|
||||||
|
1. **quorum 是否已达到**
|
||||||
|
客观下线(ODOWN)需要至少 quorum 个哨兵认为主节点下线,才会进入故障转移。
|
||||||
|
- 若配置是 `sentinel monitor mymaster ... 2`,则**两台哨兵都必须**认为主节点不可达。
|
||||||
|
- 在**两台哨兵上分别**执行,看主节点是否已被判为客观下线(`flags` 里是否有 `o_down`):
|
||||||
|
```bash
|
||||||
|
redis-cli -h <哨兵1_IP> -p 26379 -a <密码> SENTINEL master mymaster
|
||||||
|
redis-cli -h <哨兵2_IP> -p 26379 -a <密码> SENTINEL master mymaster
|
||||||
|
```
|
||||||
|
- 若只有一台认为 down,另一台仍认为 master 存活,则不会 ODOWN,也就不会触发选举。常见原因:网络分区、或两台哨兵到原主节点的网络表现不一致。
|
||||||
|
|
||||||
|
2. **是否有可提升的从节点**
|
||||||
|
新主是从**当前从节点(replica)**里选出来的;若哨兵认为没有可用从节点,就不会选出新主。
|
||||||
|
- 在两台哨兵上任选一台执行:
|
||||||
|
```bash
|
||||||
|
redis-cli -h <哨兵IP> -p 26379 -a <密码> SENTINEL slaves mymaster
|
||||||
|
```
|
||||||
|
- 看列表是否为空、或从节点是否被标成 `s_down`/`o_down`。若从节点也被判下线或复制链路断开,哨兵不会提升它。
|
||||||
|
|
||||||
|
3. **法定人数与哨兵一致性**
|
||||||
|
执行(在两台哨兵上都跑一次):
|
||||||
|
```bash
|
||||||
|
redis-cli -h <哨兵IP> -p 26379 -a <密码> SENTINEL ckquorum mymaster
|
||||||
|
```
|
||||||
|
- 若提示 quorum 未满足,说明当前“认为主节点下线”的哨兵数不足,不会触发故障转移。
|
||||||
|
|
||||||
|
4. **quorum 配置是否过大**
|
||||||
|
若当时部署时按 3 台哨兵配了 `quorum=2`,现在只剩 2 台,理论上 2 台都同意即可达到 quorum=2。但若误配成 **quorum=3**,则 2 台永远达不到 3,**不会**触发客观下线和选举。检查每台哨兵的 `sentinel.conf` 里 `sentinel monitor mymaster ...` 最后一个数字是否为 2(2 台哨兵时建议为 2)。
|
||||||
|
|
||||||
|
5. **等待时间**
|
||||||
|
主节点需在 **down-after-milliseconds**(如 30000)内一直被判定不可达,才会主观下线,再经 quorum 达成客观下线。刚宕机后需至少等够这段时间再看是否选出新主。
|
||||||
|
|
||||||
|
**小结**:两哨兵存活仍不选主,多半是 **(1) quorum 未达成**(两台对主节点状态看法不一致)或 **(2) quorum 配成 3**,或 **(3) 从节点被哨兵判为不可用**。先查 `SENTINEL master mymaster` 的 flags、`SENTINEL slaves mymaster` 和 `SENTINEL ckquorum mymaster`,再对照上述几条处理。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、客户端集成(Redisson)
|
## 五、客户端集成(Redisson)
|
||||||
|
|
||||||
在 Spring Boot 应用中,使用 Redisson 实现哨兵模式的连接:
|
在 Spring Boot 应用中,使用 Redisson 实现哨兵模式的连接:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -235,8 +333,8 @@ spring:
|
|||||||
# --- DB ---
|
# --- DB ---
|
||||||
database: ${app.redis-database:0} # 选择 DB(0~15,取决于你的 redis 配置)
|
database: ${app.redis-database:0} # 选择 DB(0~15,取决于你的 redis 配置)
|
||||||
|
|
||||||
# --- 启动检查 / 发现 ---
|
# --- 启动检查 / 发现(单哨兵宕机不影响业务必读)---
|
||||||
checkSentinelsList: true # 启动时校验 Sentinel 列表(默认 true)
|
checkSentinelsList: false # 建议 false:启动/刷新时不强制校验“全部”哨兵,任一只哨兵宕机不会导致客户端整体不可用(默认 true 会连所有哨兵,一个不通就报错)
|
||||||
sentinelsDiscovery: true # 是否自动发现更多 sentinel(默认 true;Docker/NAT 环境可考虑关掉避免“拓扑污染”)
|
sentinelsDiscovery: true # 是否自动发现更多 sentinel(默认 true;Docker/NAT 环境可考虑关掉避免“拓扑污染”)
|
||||||
dnsMonitoringInterval: 5000 # DNS 变更监控间隔 ms;-1 表示禁用(对域名方式接入有用)
|
dnsMonitoringInterval: 5000 # DNS 变更监控间隔 ms;-1 表示禁用(对域名方式接入有用)
|
||||||
|
|
||||||
@@ -307,30 +405,147 @@ spring:
|
|||||||
## 六、运维常用命令
|
## 六、运维常用命令
|
||||||
|
|
||||||
### 6.1 哨兵管理命令
|
### 6.1 哨兵管理命令
|
||||||
|
|
||||||
详细的 Redis 常用命令及 Sentinel 运维操作请参考:[Redis 常用命令使用指南](./usage.md)
|
详细的 Redis 常用命令及 Sentinel 运维操作请参考:[Redis 常用命令使用指南](./usage.md)
|
||||||
|
|
||||||
通过 `redis-cli` 连接哨兵端口(默认 26379)执行:
|
通过 `redis-cli` 连接哨兵端口(默认 26379)执行:
|
||||||
|
|
||||||
| 命令 | 功能描述 |
|
|
||||||
| :--- | :--- |
|
| 命令 | 功能描述 |
|
||||||
| `SENTINEL masters` | 列出所有被监控的主节点状态 |
|
| ----------------------------------------- | ----------------- |
|
||||||
| `SENTINEL master <name>` | 查看指定主节点的详细信息 |
|
| `SENTINEL masters` | 列出所有被监控的主节点状态 |
|
||||||
| `SENTINEL slaves <name>` | 查看指定主节点的从节点列表 |
|
| `SENTINEL master <name>` | 查看指定主节点的详细信息 |
|
||||||
| `SENTINEL sentinels <name>` | 列出除当前节点外的其他哨兵实例 |
|
| `SENTINEL slaves <name>` | 查看指定主节点的从节点列表 |
|
||||||
|
| `SENTINEL sentinels <name>` | 列出除当前节点外的其他哨兵实例 |
|
||||||
| `SENTINEL get-master-addr-by-name <name>` | 获取当前有效的主节点 IP 和端口 |
|
| `SENTINEL get-master-addr-by-name <name>` | 获取当前有效的主节点 IP 和端口 |
|
||||||
| `SENTINEL failover <name>` | **手动强制触发**故障转移 |
|
| `SENTINEL failover <name>` | **手动强制触发**故障转移 |
|
||||||
| `SENTINEL reset <pattern>` | 重置配置,清除过期节点信息 |
|
| `SENTINEL reset <pattern>` | 重置配置,清除过期节点信息 |
|
||||||
| `SENTINEL ckquorum <name>` | 检查当前哨兵数量是否满足法定人数 |
|
| `SENTINEL ckquorum <name>` | 检查当前哨兵数量是否满足法定人数 |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七、注意事项与最佳实践
|
## 七、注意事项与最佳实践
|
||||||
|
|
||||||
> 注意:
|
> 注意:
|
||||||
|
>
|
||||||
> 1. **脑裂保护**:建议配置 `min-replicas-to-write 1` 和 `min-replicas-max-lag 10`,确保主库至少有 1 个正常的从库时才允许写入,防止网络分区导致的数据丢失。
|
> 1. **脑裂保护**:建议配置 `min-replicas-to-write 1` 和 `min-replicas-max-lag 10`,确保主库至少有 1 个正常的从库时才允许写入,防止网络分区导致的数据丢失。
|
||||||
> 2. **Docker 网络**:在容器环境下,务必配置 `replica-announce-ip` 和 `sentinel announce-ip`,否则哨兵可能会记录容器内部 IP 导致客户端无法连接。
|
> 2. **Docker 网络**:在容器环境下,务必配置 `replica-announce-ip` 和 `sentinel announce-ip`,否则哨兵可能会记录容器内部 IP 导致客户端无法连接。
|
||||||
> 3. **持久化平衡**:推荐开启混合持久化(AOF + RDB),并设置 `appendfsync everysec` 以平衡性能与安全性。
|
> 3. **持久化平衡**:推荐开启混合持久化(AOF + RDB),并设置 `appendfsync everysec` 以平衡性能与安全性。
|
||||||
> 4. **权限细化**:生产环境建议将 `/data/redis` 目录权限进一步收紧,仅允许特定 UID 访问。
|
> 4. **权限细化**:生产环境建议将 `/data/redis` 目录权限进一步收紧,仅允许特定 UID 访问。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 八、单哨兵宕机不影响业务(Redisson 配置)
|
||||||
|
|
||||||
|
### 8.1 现象与原因
|
||||||
|
|
||||||
|
当**只停止一个哨兵节点**时,若出现:
|
||||||
|
|
||||||
|
- `No route to host: 192.168.3.110/192.168.3.110:26379`(其中 110 为已停的哨兵)
|
||||||
|
- 或 `RedisReadonlyException: READONLY You can't write against a read only replica`(写到了已变为 replica 的旧 master)
|
||||||
|
|
||||||
|
**主要原因**是 Redisson 的以下行为:
|
||||||
|
|
||||||
|
| 配置项 | 默认值 | 导致“单哨兵挂则全挂”的原因 |
|
||||||
|
|--------|--------|----------------------------|
|
||||||
|
| **checkSentinelsList** | `true` | 启动或周期性刷新时会对**配置中的每一个**哨兵执行校验/连接;任一哨兵不可达(如 110 已停)会触发异常或阻塞,导致客户端无法正常从其余哨兵获取当前 master,进而整体不可用或拿到过期拓扑。 |
|
||||||
|
|
||||||
|
`RedisReadonlyException` 通常表示**已经发生过故障转移**(当前写到的 192.168.3.200 已被降为 replica),而客户端因上述原因无法从存活的哨兵刷新到新 master,仍向旧主写,从而报只读。
|
||||||
|
|
||||||
|
### 8.2 推荐配置(单哨兵宕机不影响业务)
|
||||||
|
|
||||||
|
在 Nacos/应用配置中,将 Redisson 的哨兵相关配置改为:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sentinelServersConfig:
|
||||||
|
masterName: "mymaster"
|
||||||
|
sentinelAddresses:
|
||||||
|
- "redis://192.168.3.230:26379"
|
||||||
|
- "redis://192.168.3.200:26379"
|
||||||
|
- "redis://192.168.3.110:26379"
|
||||||
|
|
||||||
|
# 关键:单哨兵宕机时仍可从其余哨兵获取 master,不影响业务
|
||||||
|
checkSentinelsList: false # 不强制校验“全部”哨兵,任一只不可达不会拖垮客户端
|
||||||
|
sentinelsDiscovery: true # 按需保留;Docker/NAT 环境若出现拓扑混乱可改为 false
|
||||||
|
```
|
||||||
|
|
||||||
|
**要点**:
|
||||||
|
|
||||||
|
- **checkSentinelsList: false**:不再要求能连上配置里的每一个哨兵,只要有一个哨兵可用即可获取 master 并刷新拓扑,单哨兵停机不会导致业务不可用。
|
||||||
|
- 保留多个 `sentinelAddresses`:多个哨兵仍可做高可用,只是客户端不会因“其中一个连不上”而整体失败。
|
||||||
|
|
||||||
|
### 8.3 为什么 SENTINEL masters 显示 200 却无法写入?
|
||||||
|
|
||||||
|
`SENTINEL masters` 列出的是哨兵**当前记录的主节点信息**,但在以下情况下会出现“查到的是 200,却写不进去”:
|
||||||
|
|
||||||
|
1. **200 已被降级为副本**
|
||||||
|
故障转移后,192.168.3.200 可能已经执行了 `REPLICAOF` 指向新主,自身变为只读副本。此时 `SENTINEL masters` 里可能仍显示 `ip 192.168.3.200`(视图未刷新或不同哨兵收敛有先后),但连上 200 执行写会报 `READONLY`。
|
||||||
|
|
||||||
|
2. **应看“当前主节点”而非仅看 masters 表**
|
||||||
|
用下面命令取的是**当前可写主节点**,比 `SENTINEL masters` 更贴近实际:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h 192.168.3.230 -p 26379 -a afe123456 SENTINEL get-master-addr-by-name mymaster
|
||||||
|
```
|
||||||
|
|
||||||
|
若返回的是**另一台**(例如 192.168.3.xxx),说明新主已切换,应连该地址写。
|
||||||
|
|
||||||
|
3. **在 200 上自检角色**
|
||||||
|
直连 200(你环境里 Redis 端口为 16379):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h 192.168.3.200 -p 16379 -a afe123456 ROLE
|
||||||
|
```
|
||||||
|
|
||||||
|
若返回 `slave`,说明 200 已是副本,只能读不能写;写请求应发往 `get-master-addr-by-name` 返回的那台。
|
||||||
|
|
||||||
|
**小结**:查到是 200 却不能写,多半是 200 已是 replica。用 `SENTINEL get-master-addr-by-name mymaster` 确认当前主,并连该主写;应用侧在 `checkSentinelsList: false` 且能连上任意存活哨兵时会自动刷新到新主。
|
||||||
|
|
||||||
|
### 8.4 故障转移后的核对
|
||||||
|
|
||||||
|
若已出现 `s_down,o_down` 或 `role-reported: slave`,说明哨兵已做过故障转移。建议在**仍存活的**哨兵上确认当前主节点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h 192.168.3.230 -p 26379 -a afe123456 SENTINEL get-master-addr-by-name mymaster
|
||||||
|
```
|
||||||
|
|
||||||
|
应用在能连上任意存活哨兵且 `checkSentinelsList: false` 时,会通过拓扑刷新自动指向新 master,无需改配置。
|
||||||
|
|
||||||
|
### 8.5 双节点均为 slave(复制环)的排查与处理
|
||||||
|
|
||||||
|
当两台 Redis 主机在 `ROLE` 下均显示为 `slave`,且互相把对方当作 master 时,形成**复制环**:没有真正的主节点,所有写请求都会报 `READONLY`。
|
||||||
|
|
||||||
|
**典型表现**(示例 IP/端口可替换为实际环境):
|
||||||
|
- 192.168.3.200:16379 执行 `ROLE` → `slave`,master 指向 192.168.3.230:6379
|
||||||
|
- 192.168.3.230:6379 执行 `ROLE` → `slave`,master 指向 192.168.3.200:16379
|
||||||
|
|
||||||
|
常见原因:多次故障转移、手动改过 `replicaof`、或 Sentinel 在异常情况下写入了错误的主从关系。
|
||||||
|
|
||||||
|
**处理步骤(人工指定主从)**:
|
||||||
|
|
||||||
|
1. **选定一台作为唯一 master**(示例以 192.168.3.230 为主,端口按实际为准)
|
||||||
|
在该节点上执行:
|
||||||
|
```bash
|
||||||
|
redis-cli -h 192.168.3.230 -p 6379 -a <密码> REPLICAOF NO ONE
|
||||||
|
```
|
||||||
|
执行后该节点成为唯一可写主节点。
|
||||||
|
|
||||||
|
2. **将另一台设为该主的从节点**(示例为 192.168.3.200 作为 230 的从)
|
||||||
|
在从节点上执行(端口与上一步一致):
|
||||||
|
```bash
|
||||||
|
redis-cli -h 192.168.3.200 -p 16379 -a <密码> REPLICAOF 192.168.3.230 6379
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **校验**
|
||||||
|
- 在主节点上执行 `ROLE`,应看到 `master`。
|
||||||
|
- 在从节点上执行 `ROLE`,应看到 `slave`,且 master 为刚指定的主节点地址与端口。
|
||||||
|
|
||||||
|
4. **哨兵**
|
||||||
|
Sentinel 会通过心跳发现新的主从拓扑;若长时间未更新,可在当前主节点所在机器重启该机 Sentinel,或按需执行 `SENTINEL failover`(拓扑已正确时一般无需执行)。
|
||||||
|
|
||||||
|
**注意**:端口需与现网一致(如 200 使用 16379、230 使用 6379 时,上述命令中的端口要对应修改)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**参考资料**:[Redisson 官方配置文档](https://redisson.pro/docs/configuration/#sentinel-yaml-config-format)
|
**参考资料**:[Redisson 官方配置文档](https://redisson.pro/docs/configuration/#sentinel-yaml-config-format)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: g3fo-nacos-config
|
||||||
|
description: 分析并修改 g3fo-db/当前工作区下 nacos_config 目录中的配置。若当前项目没有 nacos_config 则提示用户打开 g3fo-db 或提供 g3fo-db 路径。支持添加、修改 yml 配置及添加配置注释;需引导用户确认「场」与「单个配置文件」;当添加/修改位置不确定时必须先与用户确认再执行;在合适位置、正确缩进下编辑。
|
||||||
|
---
|
||||||
|
|
||||||
|
# nacos_config 分析与修改
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
- **根路径**:当前工作区根目录下的 `nacos_config`;若不存在则要求用户打开 g3fo-db 或提供 g3fo-db 的明确路径。
|
||||||
|
- **场(site)**:`nacos_config/{site_name}/`,每个子目录为一个场(如 afe_ap、dev_200、uat_226、vn-demo 等)。场列表通过列出 `nacos_config` 子目录动态得到,不写死。
|
||||||
|
- **配置文件**:每场下多为 yml,常见有 `common.yml`、`dubbo.yml`、`mysql.yml`、`redis.yml`、`rocketmq.yml` 以及各服务 `g3fo-*-dev.yml` 等。
|
||||||
|
|
||||||
|
## 用户交互流程(必须按顺序执行)
|
||||||
|
|
||||||
|
### 0. 确认 nacos_config 所在位置
|
||||||
|
|
||||||
|
- 先检查**当前工作区根目录**下是否存在 `nacos_config`。
|
||||||
|
- **若不存在**:提示「当前项目下未找到 `nacos_config` 目录」,引导用户二选一:
|
||||||
|
- 请先**打开 g3fo-db 项目**再操作,或
|
||||||
|
- **提供 g3fo-db 项目的明确路径**(后续操作基于该路径下的 `nacos_config`)。
|
||||||
|
- 在用户完成其一之前**不执行任何配置编辑**。
|
||||||
|
- 若存在或用户已提供路径,继续下一步。
|
||||||
|
|
||||||
|
### 1. 确定要修改的场
|
||||||
|
|
||||||
|
- 若用户**未明确**要改哪个场:反问用户,并**列出**(当前工作区或用户提供的 g3fo-db 路径下的)`nacos_config` 下所有子目录(即所有场),引导用户选择**一个或多个场**,或选择「**全部场**」。
|
||||||
|
- 若用户已明确指定场,则直接使用。
|
||||||
|
|
||||||
|
### 2. 确定要修改的配置文件
|
||||||
|
|
||||||
|
- 若用户**未指定**配置文件,或指定了**多个**:**引导用户确认仅一个**配置文件(如 `common.yml`、`dubbo.yml` 等)。
|
||||||
|
- 若用户已明确指定一个配置文件,则直接使用。
|
||||||
|
|
||||||
|
### 3. 确定添加/修改位置(必须遵守)
|
||||||
|
|
||||||
|
- **当不确定**要把新配置加在哪个位置、或要改哪一处时:**必须先与用户确认**(列出可选位置或给出建议,等用户选定),**再执行**编辑;不得在位置模糊时自行猜测并修改。
|
||||||
|
|
||||||
|
### 4. 执行修改
|
||||||
|
|
||||||
|
在选定的每个「场」下,对选定的配置文件进行编辑:
|
||||||
|
|
||||||
|
- **添加**:根据要添加的 key 路径(如 `dubbo.application.metadata-service-port`)在文件中找合适位置——若已有同名顶级块(如 `dubbo`)则合并到该块下并保持 2 空格缩进;若无则新增顶级块,放在与现有顶级键风格一致的位置。位置不明确时先与用户确认再执行。
|
||||||
|
- **修改**:若 key 已存在,则只改值并保持原有缩进与注释风格;若存在多处或歧义,先与用户确认再执行。
|
||||||
|
- **添加配置的注释**:在指定 key 上方或行尾添加注释,风格与文件内现有注释一致;位置不明确时先与用户确认再执行。
|
||||||
|
|
||||||
|
**YAML 规则**:一律 **2 空格缩进**;新加块与现有同级键对齐;不破坏现有注释与键顺序。若文件中已有 `dubbo` 块,新 dubbo 相关项应合并到该块下,避免重复顶级 `dubbo`。插入新顶级块时的常见顺序可参考 [references/yml-placement.md](references/yml-placement.md)。
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
**在 common.yml 中添加 dubbo 配置**
|
||||||
|
|
||||||
|
若 `common.yml` 中尚无顶级 `dubbo`,可新增顶级块(与 `server`、`spring` 等同级),例如:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dubbo:
|
||||||
|
application:
|
||||||
|
metadata-service-port: 28101
|
||||||
|
```
|
||||||
|
|
||||||
|
若 `common.yml` 或目标文件中已有 `dubbo:` 块,则将 `metadata-service-port` 合并到 `dubbo.application` 下,保持 2 空格缩进,不新增重复的 `dubbo:` 顶级键。
|
||||||
|
|
||||||
|
## 约定摘要
|
||||||
|
|
||||||
|
- **路径**:优先用当前工作区的 `nacos_config`;没有则要求用户打开 g3fo-db 或提供 g3fo-db 路径。
|
||||||
|
- **确认优先**:添加/修改/注释的**目标位置**一旦不确定,必须先与用户确认再执行,不得自行猜测。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# nacos_config 常见顶级键与插入顺序
|
||||||
|
|
||||||
|
在 common.yml、dubbo.yml 等文件中插入**新的顶级块**时,可参考以下常见顺序,以保持与现有风格一致:
|
||||||
|
|
||||||
|
- `server`
|
||||||
|
- `spring`
|
||||||
|
- `swagger` / `springdoc` / `knife4j`
|
||||||
|
- `logging`
|
||||||
|
- `management`
|
||||||
|
- `powerjob`
|
||||||
|
- `dubbo`
|
||||||
|
- 其他自定义顶级键(如 `webdav`、`db-delete-logs.table.include.list`、`actuator-url` 等)
|
||||||
|
|
||||||
|
同一文件中若已存在某顶级键(如 `dubbo`),新配置应**合并到该块下**,而不是再起一个同名顶级块。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Do not commit token - security
|
||||||
|
config.local.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"source":"local","sourceType":"local","installedAt":"2026-02-27T00:00:00.000Z"}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
name: git-control
|
||||||
|
description: Gitea repository and branch access control. Use when managing Git permissions: enable/disable branch protection (push, merge, read), user read/write permissions on repos, organization-wide repo access, listing orgs/repos, bulk permission changes, or comparing branches to detect unmerged commits (e.g. prod hotfix not in main). Supports Gitea API (e.g. afe.git:3000).
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Control (Gitea)
|
||||||
|
|
||||||
|
Control Gitea repository and branch permissions via API. Requires Gitea API Token with admin/repo permissions.
|
||||||
|
|
||||||
|
**API Reference**: See `references/gitea_api.md` for endpoints.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Gitea API Token**: Settings → Applications → Generate New Token (repo, admin for org-level)
|
||||||
|
- **Base URL**: Default `http://afe.git:3000` (override with `-GiteaBaseUrl`)
|
||||||
|
|
||||||
|
**Local config**: Scripts auto-load from `config.local.json` in the skill root when ApiToken is not provided. Format:
|
||||||
|
```json
|
||||||
|
{"ApiToken": "your-token", "GiteaBaseUrl": "http://afe.git:3000"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### 1. Branch Protection (Enable/Disable)
|
||||||
|
|
||||||
|
Control who can push or merge to a branch.
|
||||||
|
|
||||||
|
| Action | Script | Effect |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| Lock branch (read-only) | `branch_protect.ps1 -Action lock` | No push, no merge |
|
||||||
|
| Unlock branch | `branch_protect.ps1 -Action unlock` | Remove protection |
|
||||||
|
| Restrict merge only | `branch_protect.ps1 -Action merge-only` | No direct push, PR merge allowed |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\branch_protect.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -BranchName uat -Action lock
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. List Orgs & Repos
|
||||||
|
|
||||||
|
| Action | Script |
|
||||||
|
|--------|--------|
|
||||||
|
| List all organizations | `list_orgs_repos.ps1 -Action list-orgs` |
|
||||||
|
| List repos in org | `list_orgs_repos.ps1 -Action list-repos -Org G3SF` |
|
||||||
|
| List all repos (admin) | `list_orgs_repos.ps1 -Action list-all-repos` |
|
||||||
|
|
||||||
|
### 3. User Repo Permissions
|
||||||
|
|
||||||
|
Add, remove, or change a user's permission on a repo.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| Grant read | User can view only |
|
||||||
|
| Grant write | User can push, create PRs |
|
||||||
|
| Grant admin | User can manage settings |
|
||||||
|
| Revoke | Remove collaborator |
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\user_permission.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -Username john -Action read
|
||||||
|
.\scripts\user_permission.ps1 -ApiToken "..." -Owner G3SF -Repo g3fo-trade -Username john -Action revoke
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Org-Wide Repo Protection
|
||||||
|
|
||||||
|
Apply branch protection to all repos under an organization.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\org_repos_protect.ps1 -ApiToken "..." -Org G3SF -BranchName uat -Action lock
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Branch Compare (Detect Unmerged Commits)
|
||||||
|
|
||||||
|
Analyze differences between branches to find commits that need merging (e.g. prod hotfix not merged to main).
|
||||||
|
|
||||||
|
| Mode | Script | Use Case |
|
||||||
|
|------|--------|----------|
|
||||||
|
| Single repo (local) | `branch_compare.ps1 -RepoPath` | Repo cloned locally |
|
||||||
|
| Single repo (API) | `branch_compare.ps1 -Owner -Repo` | No local clone |
|
||||||
|
| Single repo | `org_branch_compare.ps1 -Repo g3fo-trade` | One repo by name |
|
||||||
|
| Org-wide | `org_branch_compare.ps1` | All repos in org |
|
||||||
|
|
||||||
|
**CompareSet**:
|
||||||
|
- `single`: BaseBranch vs HeadBranch only (default for branch_compare)
|
||||||
|
- `full`: main vs uat, main vs prod, uat vs prod (default for org_branch_compare)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Single repo - full check (main/uat/prod)
|
||||||
|
.\scripts\branch_compare.ps1 -RepoPath "d:\path\to\g3fo-trade" -CompareSet full
|
||||||
|
.\scripts\branch_compare.ps1 -Owner G3SF -Repo g3fo-trade -CompareSet full
|
||||||
|
|
||||||
|
# Single pair
|
||||||
|
.\scripts\branch_compare.ps1 -RepoPath "d:\path\to\repo" -BaseBranch main -HeadBranch uat
|
||||||
|
|
||||||
|
# Single repo by name (org)
|
||||||
|
.\scripts\org_branch_compare.ps1 -Org G3SF -Repo g3fo-trade -CompareSet full
|
||||||
|
|
||||||
|
# All G3SF repos - full check
|
||||||
|
.\scripts\org_branch_compare.ps1 -Org G3SF -CompareSet full -WorkspaceRoot "d:\AFE Git\G3SF\G3FO\server"
|
||||||
|
|
||||||
|
# Filter by date or tag
|
||||||
|
.\scripts\branch_compare.ps1 -RepoPath "d:\path\to\repo" -CompareSet full -SinceDate "2025-01-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: Each line shows `hash | author | message`. When there are unmerged commits, a "By user (unmerged)" summary lists each user and their commit count.
|
||||||
|
|
||||||
|
### 7. Org-Wide User Access
|
||||||
|
|
||||||
|
Revoke or grant a user's collaborator access across all org repos.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\org_user_access.ps1 -ApiToken "..." -Org G3SF -Username john -Action revoke
|
||||||
|
.\scripts\org_user_access.ps1 -ApiToken "..." -Org G3SF -Username john -Action read
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Org repos often use team permissions. For team-based access, use Gitea API team endpoints (see `references/gitea_api.md`).
|
||||||
|
|
||||||
|
## Common Control Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Lock UAT for release** | Lock `uat` branch on specified repos before release freeze |
|
||||||
|
| **Unlock for merge** | Temporarily allow merge (add user to merge allowlist) |
|
||||||
|
| **Detect unmerged hotfixes** | Compare prod vs main to find commits in prod not in main |
|
||||||
|
| **Audit user access** | List repos a user can access (collaborators + org teams) |
|
||||||
|
| **Bulk branch lock** | Lock same branch across multiple repos |
|
||||||
|
| **Read-only maintenance** | Set repo to read-only during maintenance |
|
||||||
|
| **New member onboarding** | Grant read to org repos for new team member |
|
||||||
|
| **Offboarding** | Revoke user from all org repos |
|
||||||
|
|
||||||
|
## Script Parameters (Common)
|
||||||
|
|
||||||
|
- `-ApiToken` or `$env:GITEA_TOKEN`
|
||||||
|
- `-GiteaBaseUrl` (default: http://afe.git:3000)
|
||||||
|
- `-Owner` / `-Org` (organization name)
|
||||||
|
- `-Repo` (repository name)
|
||||||
|
- `-BranchName` (branch pattern, e.g. uat, main)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- 403: Insufficient permissions (need admin for org ops)
|
||||||
|
- 404: Repo/org/user not found
|
||||||
|
- 422: Validation error (check API compatibility with Gitea version)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Gitea API Reference for Git Control
|
||||||
|
|
||||||
|
Base: `{GiteaBaseUrl}/api/v1`
|
||||||
|
Auth: `Authorization: token {ApiToken}`
|
||||||
|
|
||||||
|
## Branch Protection
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List | GET | `/repos/{owner}/{repo}/branch_protections` |
|
||||||
|
| Create | POST | `/repos/{owner}/{repo}/branch_protections` |
|
||||||
|
| Get | GET | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
| Edit | PATCH | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
| Delete | DELETE | `/repos/{owner}/{repo}/branch_protections/{name}` |
|
||||||
|
|
||||||
|
**CreateBranchProtectionOption** (POST body):
|
||||||
|
- `branch_name`: pattern (e.g. `uat`, `main`)
|
||||||
|
- `enable_push`: false = no direct push
|
||||||
|
- `enable_merge_whitelist`: true + empty lists = no one can merge
|
||||||
|
- `merge_whitelist_usernames`: []
|
||||||
|
- `merge_whitelist_teams`: []
|
||||||
|
|
||||||
|
## Organizations & Repositories
|
||||||
|
|
||||||
|
| Action | Method | Path | Note |
|
||||||
|
|--------|--------|------|------|
|
||||||
|
| List all orgs | GET | `/admin/orgs` | Admin only |
|
||||||
|
| List org repos | GET | `/orgs/{org}/repos` | Pagination: page, limit |
|
||||||
|
| List user repos | GET | `/user/repos` | Current user |
|
||||||
|
| Search repos | GET | `/repos/search` | q, page, limit |
|
||||||
|
|
||||||
|
## Collaborators (User Repo Permissions)
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List | GET | `/repos/{owner}/{repo}/collaborators` |
|
||||||
|
| Add/Update | PUT | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
| Remove | DELETE | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
| Check | GET | `/repos/{owner}/{repo}/collaborators/{username}` |
|
||||||
|
|
||||||
|
**PUT body**: `{"permission": "read"|"write"|"admin"}`
|
||||||
|
|
||||||
|
## Organization Teams (Org-level Permissions)
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| List org teams | GET | `/orgs/{org}/teams` |
|
||||||
|
| List team repos | GET | `/teams/{id}/repos` |
|
||||||
|
| Add repo to team | PUT | `/teams/{id}/repos/{org}/{repo}` |
|
||||||
|
| Remove repo from team | DELETE | `/teams/{id}/repos/{org}/{repo}` |
|
||||||
|
| List team members | GET | `/teams/{id}/members` |
|
||||||
|
| Add member | PUT | `/teams/{id}/members/{username}` |
|
||||||
|
| Remove member | DELETE | `/teams/{id}/members/{username}` |
|
||||||
|
|
||||||
|
Team permission: `permission` = "none"|"read"|"write"|"admin"|"owner"
|
||||||
|
|
||||||
|
## Branch Compare
|
||||||
|
|
||||||
|
| Action | Method | Path |
|
||||||
|
|--------|--------|------|
|
||||||
|
| Compare | GET | `/repos/{owner}/{repo}/compare/{base}...{head}` |
|
||||||
|
|
||||||
|
Returns commits in `head` that are not in `base`. Use `base...head` (three dots) format.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Compare branches to detect unmerged commits (e.g. prod hotfix not merged to main)
|
||||||
|
# Usage: local repo path OR Gitea owner/repo
|
||||||
|
# -CompareSet full: main vs uat, main vs prod, uat vs prod
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$RepoPath,
|
||||||
|
[string]$Owner,
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$BaseBranch = "main",
|
||||||
|
[string]$HeadBranch = "prod",
|
||||||
|
[ValidateSet("single", "full")]
|
||||||
|
[string]$CompareSet = "single",
|
||||||
|
[string]$SinceTag,
|
||||||
|
[string]$SinceDate,
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GitLog {
|
||||||
|
param([string]$Path, [string]$Range, [string]$Since)
|
||||||
|
$gitArgs = @("log", $Range, "--no-merges", "--format=%h | %an | %s")
|
||||||
|
if ($Since) { $gitArgs += "--since=$Since" }
|
||||||
|
$out = & git -C $Path $gitArgs 2>&1
|
||||||
|
if ($out) { $out } else { @() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-Local {
|
||||||
|
if (-not (Test-Path (Join-Path $RepoPath ".git"))) {
|
||||||
|
Write-Host "Error: Not a git repo: $RepoPath"; exit 1
|
||||||
|
}
|
||||||
|
cmd /c "git -C `"$RepoPath`" fetch origin >nul 2>&1"
|
||||||
|
$since = if ($SinceDate) { $SinceDate } else { $null }
|
||||||
|
$baseRef = "origin/$BaseBranch"
|
||||||
|
$headRef = "origin/$HeadBranch"
|
||||||
|
if ($SinceTag -and -not $SinceDate) {
|
||||||
|
$tagDate = & git -C $RepoPath log -1 --format="%ci" $SinceTag 2>$null
|
||||||
|
if ($tagDate) { $since = $tagDate.Trim().Split(" ")[0] }
|
||||||
|
}
|
||||||
|
Write-Host "=== Branch Compare: ${BaseBranch} vs ${HeadBranch} ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "Repo: $RepoPath" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
$inHeadNotBase = @(Get-GitLog -Path $RepoPath -Range "${baseRef}..${headRef}" -Since $since)
|
||||||
|
$inBaseNotHead = @(Get-GitLog -Path $RepoPath -Range "${headRef}..${baseRef}" -Since $since)
|
||||||
|
Write-Host "[!] Commits in ${HeadBranch} NOT in ${BaseBranch} (need merge to ${BaseBranch}):" -ForegroundColor Yellow
|
||||||
|
if ($inHeadNotBase.Count -eq 0) {
|
||||||
|
Write-Host " (none)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$inHeadNotBase | ForEach-Object { Write-Host " $_" }
|
||||||
|
Write-Host " Total: $($inHeadNotBase.Count) commits" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[i] Commits in ${BaseBranch} NOT in ${HeadBranch} (${BaseBranch} is ahead):" -ForegroundColor Gray
|
||||||
|
if ($inBaseNotHead.Count -eq 0) {
|
||||||
|
Write-Host " (none)" -ForegroundColor Gray
|
||||||
|
} else {
|
||||||
|
$inBaseNotHead | ForEach-Object { Write-Host " $_" }
|
||||||
|
Write-Host " Total: $($inBaseNotHead.Count) commits" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
if ($inHeadNotBase.Count -gt 0) {
|
||||||
|
$byUser = $inHeadNotBase | ForEach-Object {
|
||||||
|
$parts = $_ -split '\s*\|\s*'
|
||||||
|
if ($parts.Count -ge 2) { $parts[1].Trim() }
|
||||||
|
} | Where-Object { $_ } | Group-Object | Sort-Object Count -Descending
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "By user (unmerged):" -ForegroundColor Yellow
|
||||||
|
$byUser | ForEach-Object { Write-Host " $($_.Name): $($_.Count) commits" }
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "ACTION: Merge ${HeadBranch} into ${BaseBranch} to sync hotfixes." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-API {
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required for API mode"; exit 1 }
|
||||||
|
if (-not $Owner -or -not $Repo) { Write-Host "Error: Owner and Repo required for API mode"; exit 1 }
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
$basehead = "${BaseBranch}...${HeadBranch}"
|
||||||
|
Write-Host "=== Branch Compare: ${BaseBranch} vs ${HeadBranch} ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "Repo: $Owner/$Repo (via API)" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
try {
|
||||||
|
$cmp = Invoke-RestMethod -Uri "$ApiBase/repos/$Owner/$Repo/compare/$basehead" -Headers $Headers -Method Get
|
||||||
|
$commits = if ($cmp.commits) { @($cmp.commits) } else { @() }
|
||||||
|
Write-Host "[!] Commits in ${HeadBranch} NOT in ${BaseBranch} (need merge to ${BaseBranch}):" -ForegroundColor Yellow
|
||||||
|
if ($commits.Count -eq 0) {
|
||||||
|
Write-Host " (none)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$commits | ForEach-Object {
|
||||||
|
$author = $_.commit.author.name
|
||||||
|
if ($_.author.login) { $author = $_.author.login }
|
||||||
|
Write-Host " $($_.sha.Substring(0,7)) | $author | $($_.commit.message.Split("`n")[0])"
|
||||||
|
}
|
||||||
|
Write-Host " Total: $($commits.Count) commits" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
$baseheadRev = "${HeadBranch}...${BaseBranch}"
|
||||||
|
$cmpRev = Invoke-RestMethod -Uri "$ApiBase/repos/$Owner/$Repo/compare/$baseheadRev" -Headers $Headers -Method Get
|
||||||
|
$commitsRev = if ($cmpRev.commits) { @($cmpRev.commits) } else { @() }
|
||||||
|
Write-Host "[i] Commits in ${BaseBranch} NOT in ${HeadBranch} (${BaseBranch} is ahead):" -ForegroundColor Gray
|
||||||
|
if ($commitsRev.Count -eq 0) {
|
||||||
|
Write-Host " (none)" -ForegroundColor Gray
|
||||||
|
} else {
|
||||||
|
$commitsRev | ForEach-Object {
|
||||||
|
$author = $_.commit.author.name
|
||||||
|
if ($_.author.login) { $author = $_.author.login }
|
||||||
|
Write-Host " $($_.sha.Substring(0,7)) | $author | $($_.commit.message.Split("`n")[0])"
|
||||||
|
}
|
||||||
|
Write-Host " Total: $($commitsRev.Count) commits" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
if ($commits.Count -gt 0) {
|
||||||
|
$byUser = $commits | ForEach-Object {
|
||||||
|
if ($_.author.login) { $_.author.login } else { $_.commit.author.name }
|
||||||
|
} | Group-Object | Sort-Object Count -Descending
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "By user (unmerged):" -ForegroundColor Yellow
|
||||||
|
$byUser | ForEach-Object { Write-Host " $($_.Name): $($_.Count) commits" }
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "ACTION: Merge ${HeadBranch} into ${BaseBranch} to sync hotfixes." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$msg = if ($_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
|
||||||
|
if ($msg -match "BaseNotExist|HeadNotExist|not found") {
|
||||||
|
Write-Host " (branch not found - repo may not have ${BaseBranch}/${HeadBranch})" -ForegroundColor Gray
|
||||||
|
} else {
|
||||||
|
Write-Host " API Error: $msg" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pairs = @()
|
||||||
|
if ($CompareSet -eq "full") {
|
||||||
|
$pairs = @(
|
||||||
|
@{ Base = "main"; Head = "uat" },
|
||||||
|
@{ Base = "main"; Head = "prod" },
|
||||||
|
@{ Base = "uat"; Head = "prod" }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
$pairs = @(@{ Base = $BaseBranch; Head = $HeadBranch })
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($p in $pairs) {
|
||||||
|
$BaseBranch = $p.Base
|
||||||
|
$HeadBranch = $p.Head
|
||||||
|
if ($RepoPath) {
|
||||||
|
Compare-Local
|
||||||
|
} elseif ($Owner -and $Repo) {
|
||||||
|
Compare-API
|
||||||
|
} else {
|
||||||
|
Write-Host "Error: Provide -RepoPath (local) OR -Owner and -Repo (API)"
|
||||||
|
Write-Host " Local: .\branch_compare.ps1 -RepoPath 'd:\path\to\repo' -CompareSet full"
|
||||||
|
Write-Host " API: .\branch_compare.ps1 -Owner G3SF -Repo g3fo-trade -CompareSet full"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($pairs.Count -gt 1) { Write-Host "" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Branch protection: lock, unlock, or merge-only
|
||||||
|
# lock = no push, no merge | unlock = remove protection | merge-only = no direct push, PR merge allowed
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Owner,
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$BranchName,
|
||||||
|
[ValidateSet("lock", "unlock", "merge-only")]
|
||||||
|
[string]$Action = "lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Owner -or -not $Repo -or -not $BranchName) { Write-Host "Error: Owner, Repo, BranchName required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$ReposPath = "repos/$Owner/$Repo"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
# Get existing protections
|
||||||
|
$Existing = @()
|
||||||
|
try {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections" -Headers $Headers -Method Get
|
||||||
|
$Existing = if ($r -is [array]) { $r } else { @($r) }
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 404) { Write-Host "Repo not found"; exit 1 }
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
|
||||||
|
$Match = $Existing | Where-Object { $_.rule_name -eq $BranchName -or $_.branch_name -eq $BranchName } | Select-Object -First 1
|
||||||
|
$RuleName = if ($Match) { if ($Match.rule_name) { $Match.rule_name } else { $BranchName } } else { $BranchName }
|
||||||
|
|
||||||
|
# Delete existing rule
|
||||||
|
if ($Match) {
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections/$([uri]::EscapeDataString($RuleName))" -Headers $Headers -Method Delete | Out-Null
|
||||||
|
} catch { Write-Host "Warning: Delete failed: $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "unlock") {
|
||||||
|
Write-Host "Branch $BranchName unlocked (protection removed)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create new rule
|
||||||
|
$Body = @{
|
||||||
|
branch_name = $BranchName
|
||||||
|
enable_push = if ($Action -eq "merge-only") { $false } else { $false }
|
||||||
|
enable_push_whitelist = $false
|
||||||
|
enable_merge_whitelist = if ($Action -eq "lock") { $true } else { $false }
|
||||||
|
merge_whitelist_usernames = @()
|
||||||
|
merge_whitelist_teams = @()
|
||||||
|
block_on_rejected_reviews = $false
|
||||||
|
block_on_outdated_branch = $false
|
||||||
|
dismiss_stale_approvals = $false
|
||||||
|
require_signed_commits = $false
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/branch_protections" -Headers $Headers -Method Post -Body $Body | Out-Null
|
||||||
|
Write-Host "Branch ${BranchName}: $Action applied"
|
||||||
|
} catch {
|
||||||
|
Write-Host "FAIL: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# List organizations and repositories
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[ValidateSet("list-orgs", "list-repos", "list-all-repos")]
|
||||||
|
[string]$Action = "list-orgs",
|
||||||
|
[string]$Org
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
if ($Action -eq "list-orgs") {
|
||||||
|
try {
|
||||||
|
$orgs = Invoke-RestMethod -Uri "$ApiBase/admin/orgs?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
$orgs | ForEach-Object { Write-Host $_.username }
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 403) { Write-Host "Admin permission required for list-orgs"; exit 1 }
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "list-repos") {
|
||||||
|
if (-not $Org) { Write-Host "Error: -Org required for list-repos"; exit 1 }
|
||||||
|
$repos = @()
|
||||||
|
$page = 1
|
||||||
|
do {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/orgs/$Org/repos?page=$page&limit=50" -Headers $Headers -Method Get
|
||||||
|
$arr = if ($r -is [array]) { $r } else { @($r) }
|
||||||
|
$repos += $arr
|
||||||
|
$page++
|
||||||
|
} while ($arr.Count -eq 50)
|
||||||
|
foreach ($repo in $repos) { Write-Output $repo.name }
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Action -eq "list-all-repos") {
|
||||||
|
$orgs = Invoke-RestMethod -Uri "$ApiBase/admin/orgs?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
foreach ($o in $orgs) {
|
||||||
|
$r = Invoke-RestMethod -Uri "$ApiBase/orgs/$($o.username)/repos?page=1&limit=100" -Headers $Headers -Method Get
|
||||||
|
$r | ForEach-Object { Write-Host "$($o.username)/$($_.name)" }
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Compare branches across all repos in an org - detect unmerged commits
|
||||||
|
# -CompareSet full: main vs uat, main vs prod, uat vs prod for each repo
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Org = "G3SF",
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$BaseBranch = "main",
|
||||||
|
[string]$HeadBranch = "prod",
|
||||||
|
[ValidateSet("single", "full")]
|
||||||
|
[string]$CompareSet = "full",
|
||||||
|
[string]$WorkspaceRoot = "d:\AFE Git\G3SF\G3FO\server",
|
||||||
|
[switch]$UseApi,
|
||||||
|
[string]$SinceTag,
|
||||||
|
[string]$SinceDate
|
||||||
|
)
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$CompareScript = Join-Path $ScriptDir "branch_compare.ps1"
|
||||||
|
$ListScript = Join-Path $ScriptDir "list_orgs_repos.ps1"
|
||||||
|
|
||||||
|
$repos = if ($Repo) { @($Repo) } else { @(& $ListScript -Action list-repos -Org $Org) }
|
||||||
|
$hasLocal = Test-Path $WorkspaceRoot
|
||||||
|
|
||||||
|
foreach ($r in $repos) {
|
||||||
|
if (-not $r) { continue }
|
||||||
|
$path = Join-Path $WorkspaceRoot $r
|
||||||
|
Write-Host "########## $Org/$r ##########" -ForegroundColor Magenta
|
||||||
|
if ($UseApi -or (-not $hasLocal) -or (-not (Test-Path $path))) {
|
||||||
|
$params = @{ Owner = $Org; Repo = $r; BaseBranch = $BaseBranch; HeadBranch = $HeadBranch; CompareSet = $CompareSet }
|
||||||
|
& $CompareScript @params
|
||||||
|
} else {
|
||||||
|
$params = @{ RepoPath = $path; BaseBranch = $BaseBranch; HeadBranch = $HeadBranch; CompareSet = $CompareSet }
|
||||||
|
if ($SinceTag) { $params.SinceTag = $SinceTag }
|
||||||
|
if ($SinceDate) { $params.SinceDate = $SinceDate }
|
||||||
|
& $CompareScript @params
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Apply branch protection to all repos in an organization
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Org,
|
||||||
|
[string]$BranchName,
|
||||||
|
[ValidateSet("lock", "unlock")]
|
||||||
|
[string]$Action = "lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Org -or -not $BranchName) { Write-Host "Error: Org, BranchName required"; exit 1 }
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$BranchScript = Join-Path $ScriptDir "branch_protect.ps1"
|
||||||
|
|
||||||
|
$repos = @(& "$ScriptDir\list_orgs_repos.ps1" -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Action list-repos -Org $Org)
|
||||||
|
if (-not $repos -or $repos.Count -eq 0) { Write-Host "No repos found in org $Org"; exit 0 }
|
||||||
|
|
||||||
|
$count = 0
|
||||||
|
foreach ($r in $repos) {
|
||||||
|
if (-not $r) { continue }
|
||||||
|
Write-Host "Processing $Org/$r..."
|
||||||
|
& $BranchScript -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Owner $Org -Repo $r -BranchName $BranchName -Action $Action
|
||||||
|
$count++
|
||||||
|
}
|
||||||
|
Write-Host "Done. Processed $count repos."
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Revoke or grant a user's access across all repos in an organization
|
||||||
|
# Uses collaborator API - only affects repos where user is direct collaborator
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Org,
|
||||||
|
[string]$Username,
|
||||||
|
[ValidateSet("revoke", "read", "write")]
|
||||||
|
[string]$Action = "revoke"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Org -or -not $Username) { Write-Host "Error: Org, Username required"; exit 1 }
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$Repos = @(& "$ScriptDir\list_orgs_repos.ps1" -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Action list-repos -Org $Org)
|
||||||
|
$UserScript = Join-Path $ScriptDir "user_permission.ps1"
|
||||||
|
|
||||||
|
$count = 0
|
||||||
|
foreach ($Repo in $Repos) {
|
||||||
|
if (-not $Repo) { continue }
|
||||||
|
Write-Host "Processing $Org/$Repo..."
|
||||||
|
$null = & $UserScript -ApiToken $ApiToken -GiteaBaseUrl $GiteaBaseUrl -Owner $Org -Repo $Repo -Username $Username -Action $Action 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $count++ }
|
||||||
|
}
|
||||||
|
Write-Host "Done. Updated $count repos where $Username was collaborator."
|
||||||
|
Write-Host "Note: Org repos may use team permissions. Check org teams for full control."
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Add, update, or revoke user permission on a repository
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$ApiToken = $env:GITEA_TOKEN,
|
||||||
|
[string]$GiteaBaseUrl = "http://afe.git:3000",
|
||||||
|
[string]$Owner,
|
||||||
|
[string]$Repo,
|
||||||
|
[string]$Username,
|
||||||
|
[ValidateSet("read", "write", "admin", "revoke")]
|
||||||
|
[string]$Action = "read"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ConfigPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\config.local.json"
|
||||||
|
if (-not $ApiToken -and (Test-Path $ConfigPath)) {
|
||||||
|
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||||
|
$ApiToken = $cfg.ApiToken
|
||||||
|
if ($cfg.GiteaBaseUrl) { $GiteaBaseUrl = $cfg.GiteaBaseUrl }
|
||||||
|
}
|
||||||
|
if (-not $ApiToken) { Write-Host "Error: ApiToken required"; exit 1 }
|
||||||
|
if (-not $Owner -or -not $Repo -or -not $Username) { Write-Host "Error: Owner, Repo, Username required"; exit 1 }
|
||||||
|
|
||||||
|
$ApiBase = "$GiteaBaseUrl/api/v1"
|
||||||
|
$ReposPath = "repos/$Owner/$Repo"
|
||||||
|
$Headers = @{ "Authorization" = "token $ApiToken"; "Content-Type" = "application/json" }
|
||||||
|
|
||||||
|
if ($Action -eq "revoke") {
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/collaborators/$Username" -Headers $Headers -Method Delete
|
||||||
|
Write-Host "Revoked $Username from $Owner/$Repo"
|
||||||
|
} catch {
|
||||||
|
if ($_.Exception.Response.StatusCode -eq 404) { Write-Host "User not a collaborator or repo not found" }
|
||||||
|
else { throw }
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$Body = @{ permission = $Action } | ConvertTo-Json
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri "$ApiBase/$ReposPath/collaborators/$Username" -Headers $Headers -Method Put -Body $Body
|
||||||
|
Write-Host "Set $Username to $Action on $Owner/$Repo"
|
||||||
|
} catch {
|
||||||
|
Write-Host "FAIL: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -207,15 +207,15 @@ def init_skill(skill_name, path):
|
|||||||
|
|
||||||
# Check if directory already exists
|
# Check if directory already exists
|
||||||
if skill_dir.exists():
|
if skill_dir.exists():
|
||||||
print(f"❌ Error: Skill directory already exists: {skill_dir}")
|
print(f"Error: Skill directory already exists: {skill_dir}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Create skill directory
|
# Create skill directory
|
||||||
try:
|
try:
|
||||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||||
print(f"✅ Created skill directory: {skill_dir}")
|
print(f"Created skill directory: {skill_dir}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creating directory: {e}")
|
print(f"Error creating directory: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Create SKILL.md from template
|
# Create SKILL.md from template
|
||||||
@@ -227,10 +227,10 @@ def init_skill(skill_name, path):
|
|||||||
|
|
||||||
skill_md_path = skill_dir / 'SKILL.md'
|
skill_md_path = skill_dir / 'SKILL.md'
|
||||||
try:
|
try:
|
||||||
skill_md_path.write_text(skill_content)
|
skill_md_path.write_text(skill_content, encoding='utf-8')
|
||||||
print("✅ Created SKILL.md")
|
print("Created SKILL.md")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creating SKILL.md: {e}")
|
print(f"Error creating SKILL.md: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Create resource directories with example files
|
# Create resource directories with example files
|
||||||
@@ -239,29 +239,29 @@ def init_skill(skill_name, path):
|
|||||||
scripts_dir = skill_dir / 'scripts'
|
scripts_dir = skill_dir / 'scripts'
|
||||||
scripts_dir.mkdir(exist_ok=True)
|
scripts_dir.mkdir(exist_ok=True)
|
||||||
example_script = scripts_dir / 'example.py'
|
example_script = scripts_dir / 'example.py'
|
||||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name), encoding='utf-8')
|
||||||
example_script.chmod(0o755)
|
example_script.chmod(0o755)
|
||||||
print("✅ Created scripts/example.py")
|
print("Created scripts/example.py")
|
||||||
|
|
||||||
# Create references/ directory with example reference doc
|
# Create references/ directory with example reference doc
|
||||||
references_dir = skill_dir / 'references'
|
references_dir = skill_dir / 'references'
|
||||||
references_dir.mkdir(exist_ok=True)
|
references_dir.mkdir(exist_ok=True)
|
||||||
example_reference = references_dir / 'api_reference.md'
|
example_reference = references_dir / 'api_reference.md'
|
||||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title), encoding='utf-8')
|
||||||
print("✅ Created references/api_reference.md")
|
print("Created references/api_reference.md")
|
||||||
|
|
||||||
# Create assets/ directory with example asset placeholder
|
# Create assets/ directory with example asset placeholder
|
||||||
assets_dir = skill_dir / 'assets'
|
assets_dir = skill_dir / 'assets'
|
||||||
assets_dir.mkdir(exist_ok=True)
|
assets_dir.mkdir(exist_ok=True)
|
||||||
example_asset = assets_dir / 'example_asset.txt'
|
example_asset = assets_dir / 'example_asset.txt'
|
||||||
example_asset.write_text(EXAMPLE_ASSET)
|
example_asset.write_text(EXAMPLE_ASSET, encoding='utf-8')
|
||||||
print("✅ Created assets/example_asset.txt")
|
print("Created assets/example_asset.txt")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creating resource directories: {e}")
|
print(f"Error creating resource directories: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Print next steps
|
# Print next steps
|
||||||
print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}")
|
print(f"\nSkill '{skill_name}' initialized successfully at {skill_dir}")
|
||||||
print("\nNext steps:")
|
print("\nNext steps:")
|
||||||
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
||||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
||||||
@@ -287,7 +287,7 @@ def main():
|
|||||||
skill_name = sys.argv[1]
|
skill_name = sys.argv[1]
|
||||||
path = sys.argv[3]
|
path = sys.argv[3]
|
||||||
|
|
||||||
print(f"🚀 Initializing skill: {skill_name}")
|
print(f"Initializing skill: {skill_name}")
|
||||||
print(f" Location: {path}")
|
print(f" Location: {path}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ def package_skill(skill_path, output_dir=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Run validation before packaging
|
# Run validation before packaging
|
||||||
print("🔍 Validating skill...")
|
print("Validating skill...")
|
||||||
valid, message = validate_skill(skill_path)
|
valid, message = validate_skill(skill_path)
|
||||||
if not valid:
|
if not valid:
|
||||||
print(f"❌ Validation failed: {message}")
|
print(f"Validation failed: {message}")
|
||||||
print(" Please fix the validation errors before packaging.")
|
print(" Please fix the validation errors before packaging.")
|
||||||
return None
|
return None
|
||||||
print(f"✅ {message}\n")
|
print(f"{message}\n")
|
||||||
|
|
||||||
# Determine output location
|
# Determine output location
|
||||||
skill_name = skill_path.name
|
skill_name = skill_path.name
|
||||||
@@ -74,11 +74,11 @@ def package_skill(skill_path, output_dir=None):
|
|||||||
zipf.write(file_path, arcname)
|
zipf.write(file_path, arcname)
|
||||||
print(f" Added: {arcname}")
|
print(f" Added: {arcname}")
|
||||||
|
|
||||||
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
|
print(f"\nSuccessfully packaged skill to: {skill_filename}")
|
||||||
return skill_filename
|
return skill_filename
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creating .skill file: {e}")
|
print(f"Error creating .skill file: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ def main():
|
|||||||
skill_path = sys.argv[1]
|
skill_path = sys.argv[1]
|
||||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
print(f"📦 Packaging skill: {skill_path}")
|
print(f"Packaging skill: {skill_path}")
|
||||||
if output_dir:
|
if output_dir:
|
||||||
print(f" Output directory: {output_dir}")
|
print(f" Output directory: {output_dir}")
|
||||||
print()
|
print()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def validate_skill(skill_path):
|
|||||||
return False, "SKILL.md not found"
|
return False, "SKILL.md not found"
|
||||||
|
|
||||||
# Read and validate frontmatter
|
# Read and validate frontmatter
|
||||||
content = skill_md.read_text()
|
content = skill_md.read_text(encoding='utf-8')
|
||||||
if not content.startswith('---'):
|
if not content.startswith('---'):
|
||||||
return False, "No YAML frontmatter found"
|
return False, "No YAML frontmatter found"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user