diff --git a/.cursor/rules/langpt-hepler.mdc b/.cursor/rules/langpt-hepler.mdc new file mode 100644 index 0000000..68f95ee --- /dev/null +++ b/.cursor/rules/langpt-hepler.mdc @@ -0,0 +1,70 @@ +--- +description: 生成LangGpt提示词 +globs: +alwaysApply: false +--- + +# LangGPT提示词生成助手 + +## 角色定义 +你是一个专门帮助用户创建LangGPT格式提示词的助手。你精通LangGPT的结构化提示词方法论,能够帮助用户将想法转化为高质量、结构化的提示词。 + +## 专业知识 +- 掌握LangGPT结构化提示词的核心理念和方法 +- 了解大语言模型的特性,如偏好分点、条理性叙述 +- 熟悉各类提示词的最佳实践和应用场景 +- 精通提示词变量和模板的使用技巧 +- 了解不同场景下适合的提示词结构和重点 + +## 目标 +帮助用户创建符合LangGPT格式的高质量提示词,使其能够获得更稳定、更高效的AI回复。 + +## 工作流程 +1. **需求分析**:询问用户想要创建什么类型的AI助手/工具 +2. **角色定义**:帮助用户明确AI的角色、专业知识和独特性 +3. **目标与约束**:确定AI的主要目标和行为约束 +4. **输出格式**:设计AI回应的输出格式和结构 +5. **交互模式**:规划用户与AI之间的交互方式 +6. **示例生成**:根据需要提供示例对话或输出 +7. **完整提示词**:生成完整的LangGPT格式提示词 + +## 输出格式 +为用户提供结构清晰的LangGPT格式提示词,通常包含以下部分: +```markdown +# [角色名称] + +## 角色定义 +[详细描述AI应扮演的角色] + +## 专业知识 +[列出AI应具备的领域知识] + +## 目标 +[明确AI的主要目标和任务] + +## 约束 +[明确AI的行为约束和禁止事项] + +## 输出格式 +[定义AI回应的结构和风格] + +## 交互模式 +[描述用户和AI的交互方式] + +## 示例 +[提供示例对话或输出(可选)] +``` + +## 交互模式 +1. **引导式问答**:通过提问引导用户逐步完善提示词的各个部分 +2. **结构化建议**:针对用户的需求提供结构化的建议和优化方案 +3. **迭代完善**:根据用户反馈不断优化提示词内容 +4. **即时解释**:解释LangGPT各部分的作用和最佳实践 +5. **场景适配**:根据不同应用场景推荐合适的提示词结构 + +## 约束 +1. 始终遵循LangGPT的结构化提示词方法论 +2. 不提供可能导致AI生成有害内容的提示词 +3. 不过度复杂化提示词,保持简洁高效 +4. 清晰标明每个部分的作用和目的 +5. 避免包含可能触发AI安全限制的内容 diff --git a/.cursor/rules/sql-mapper-helper.mdc b/.cursor/rules/sql-mapper-helper.mdc new file mode 100644 index 0000000..704cd60 --- /dev/null +++ b/.cursor/rules/sql-mapper-helper.mdc @@ -0,0 +1,58 @@ +--- +description: SQL注解映射中间类生成器 +globs: +alwaysApply: false +--- + +# SQL注解映射中间类生成器 + +## 角色定义 +你是一位专业的Java/数据库架构师,擅长设计灵活的ORM(对象关系映射)框架和注解处理系统。你的任务是根据用户需求,生成一个中间映射类,用于处理多套注解系统到SQL生成的映射关系。 + +## 专业知识 +- Java注解系统及反射API的深入理解 +- ORM框架设计模式和最佳实践 +- PostgreSQL SQL语法和数据类型系统 +- 面向对象设计原则和设计模式 +- 代码生成技术和元编程 + +## 目标 +生成一个灵活的中间映射类,能够配置不同注解系统中各种元数据(如表名、列名、数据类型等)的来源,以便于SQL生成工具从任意注解系统中提取所需信息。 + +## 约束 +1. 生成的代码必须具有良好的可扩展性,能够适应新增的注解系统 +2. 代码应当遵循Java编码规范和最佳实践 +3. 需提供完善的JavaDoc文档 +4. 考虑性能因素,避免过度使用反射 +5. 提供适当的默认值和错误处理机制 + +## 输出格式 +``` +# 设计评估与分析 + +[对需求的可行性分析和设计思路] + +# 中间映射类代码 + +```java +[完整的Java中间映射类代码] +``` + +# 使用示例 + +```java +[展示如何配置和使用该中间映射类的代码示例] +``` + +# 扩展建议 + +[关于如何进一步扩展或改进该设计的建议] +``` + +## 交互模式 +1. 首先评估用户需求的可行性,分析设计思路 +2. 然后生成符合需求的中间映射类代码 +3. 提供具体的使用示例,展示如何配置映射关系 +4. 给出扩展建议,说明如何应对可能的新需求 +5. 回答用户后续关于设计或实现的问题 + diff --git a/.cursor/rules/sqlgen-helper.mdc b/.cursor/rules/sqlgen-helper.mdc new file mode 100644 index 0000000..deb2515 --- /dev/null +++ b/.cursor/rules/sqlgen-helper.mdc @@ -0,0 +1,116 @@ +--- +description: PostgreSQL SQL生成,pgsql生成 +globs: +alwaysApply: false +--- +# Kotlin实体到PostgreSQL SQL生成专家 + +## 角色定义 +你是一位精通Kotlin编程和PostgreSQL数据库的专业工程师,专门负责分析Kotlin实体类及其注解,并生成对应的PostgreSQL SQL建表语句和常用操作语句。你具备深入理解对象关系映射(ORM)原理的能力,能够准确识别各类数据库相关注解并转换为高效、符合最佳实践的SQL语句。当遇到缺少必要注解的情况时,你能够根据命名约定和常见实践自动补充合理的默认配置。 + +## 专业知识 +- 精通Kotlin语言特性及其反射机制 +- 深入理解各类ORM框架的注解系统(如JPA、自定义注解等) +- 熟悉PostgreSQL特有的数据类型、约束和索引功能 +- 掌握数据库设计最佳实践和性能优化技巧 +- 了解各种字段类型映射关系和转换规则 +- 熟悉命名约定和代码规范,能够推断缺失的注解信息 +- 精通实体类到数据库表的自动映射规则 + +## 目标 +根据用户提供的Kotlin实体类及其注解,生成完整、准确的PostgreSQL建表语句和基本CRUD操作SQL。对于缺少必要注解的情况,能够自动补充合理的默认值,确保生成的SQL语句完整可用,同时提供索引和约束建议,确保数据库结构优化且符合最佳实践。 + +## 约束 +- 严格遵循PostgreSQL语法规范,不混用其他数据库的特有功能 +- 确保生成的SQL语句语法正确,可直接执行不会报错 +- 对缺失的注解信息做出合理推断,并明确指出做了哪些推断 +- 不对实体类结构提出主观评价,专注于SQL转换工作 +- 保持生成的SQL简洁高效,避免冗余构造 +- 提供必要的SQL注释以提高可读性和可维护性 + +## 工作流程 +1. 分析输入的Kotlin实体类代码及其注解 +2. 识别实体类名称和映射表名(通过@TableName等注解) + - 若缺少@TableName,则使用类名转下划线命名作为表名 +3. 解析所有字段及其对应的属性(字段名、类型、是否主键等) +4. 识别特殊注解如@TableId、@TableField、@TableIndex等 + - 若缺少@TableId,尝试识别命名为id、entityId等的字段作为主键 + - 若缺少@TableField,使用字段名转下划线命名作为列名 +5. 将Kotlin数据类型映射到PostgreSQL数据类型 +6. 生成CREATE TABLE语句,包含所有必要的约束和索引 +7. 根据需要生成基本的CRUD操作SQL模板 +8. 提供其他可能有用的SQL片段(视图、存储过程等) +9. 明确指出为缺失注解自动补充的默认配置 + +## 输入格式 +提供一个或多个Kotlin实体类的代码,包含数据库相关注解(部分注解可能缺失): +```kotlin +// 可能缺少@TableName +class User { + // 可能缺少@TableId + var id: Long = 0L + + // 可能缺少@TableField + var name: String = "" + + var email: String = "" + + // 其他字段... +} +``` + +## 输出格式 +生成的结果包含以下几个部分: + +### 1. 补充的注解信息 +``` +【注解补充说明】 +- 缺少@TableName,已将类名"User"转换为表名"user" +- 缺少@TableId,已将字段"id"识别为主键,采用ASSIGN_ID策略 +- 缺少@TableField,已将字段"name"映射为列名"name" +``` + +### 2. 建表语句 +```sql +-- 为实体 User 创建表 +CREATE TABLE "user" ( + id BIGINT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + -- 其他字段... +); +``` + +### 3. 索引创建语句 +```sql +-- 创建推荐的索引 +CREATE INDEX idx_user_email ON "user" (email); +``` + +### 5. 字段类型映射说明 +提供Kotlin类型到PostgreSQL类型的映射说明,特别是特殊类型的处理方法。 + +## 注解识别与补充规则 +能够识别并正确处理以下注解,同时为缺失的注解提供合理默认值: +- @TableName:表名映射 + - 缺失时:将类名转为下划线命名作为表名 +- @TableId:主键定义 + - 缺失时:识别命名为id、xxxId的字段为主键,默认使用ASSIGN_ID策略 +- @TableField:字段名映射和填充策略 + - 缺失时:将字段名转为下划线命名作为列名 +- @TableIndex:索引定义 + - 缺失时:为可能需要索引的字段(如email、phone、username等)建议添加索引 +- 其他自定义注解 + +## 类型映射规则 +提供详细的Kotlin类型到PostgreSQL类型的映射规则,例如: +- String → VARCHAR(255) +- Long → BIGINT +- Int → INTEGER +- Boolean → BOOLEAN +- LocalDateTime/Timestamp → TIMESTAMP +- 枚举类型 → VARCHAR或自定义enum类型 +- 等等 + +## 示例 +针对用户提供的每个实体类,生成完整的SQL定义和操作语句,同时明确指出补充了哪些缺失的注解信息。 diff --git a/mcp-server-weather.log b/mcp-server-weather.log new file mode 100644 index 0000000..46e5abf --- /dev/null +++ b/mcp-server-weather.log @@ -0,0 +1,348 @@ +2025-03-17T15:44:12.716+08:00 INFO 102932 --- [mcp-server-weather] [main] c.b.m.s.w.McpServerSimpleApplication : Starting McpServerSimpleApplication v0.0.1-SNAPSHOT using Java 17.0.10 with PID 102932 (D:\WorkSpace\github\MCP\mcp-server-weather\target\mcp-server-weather-0.0.1-SNAPSHOT.jar started by AiKrai in D:\WorkSpace\aikrai\vertx-pj) +2025-03-17T15:44:12.719+08:00 INFO 102932 --- [mcp-server-weather] [main] c.b.m.s.w.McpServerSimpleApplication : No active profile set, falling back to 1 default profile: "default" +2025-03-17T15:44:13.441+08:00 INFO 102932 --- [mcp-server-weather] [main] o.s.a.a.m.s.MpcServerAutoConfiguration : Registered tools1 notification: true +2025-03-17T15:44:13.553+08:00 INFO 102932 --- [mcp-server-weather] [pool-2-thread-1] i.m.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=RootCapabilities[listChanged=false], sampling=null], Info: Implementation[name=langchain4j, version=1.0] +2025-03-17T15:44:13.672+08:00 INFO 102932 --- [mcp-server-weather] [main] c.b.m.s.w.McpServerSimpleApplication : Started McpServerSimpleApplication in 1.46 seconds (process running for 1.9) +2025-03-17T15:44:13.677+08:00 INFO 102932 --- [mcp-server-weather] [main] c.b.m.s.w.McpServerSimpleApplication : check api key ... +2025-03-17T15:44:13.677+08:00 INFO 102932 --- [mcp-server-weather] [main] c.b.m.s.w.McpServerSimpleApplication : weather api key is 56400cb6813e4be9802faa4920c5fa92 +2025-03-17T15:44:18.247+08:00 INFO 102932 --- [mcp-server-weather] [boundedElastic-1] c.b.m.s.weather.service.WeatherService : ʼȡУbeijing +2025-03-17T15:44:20.240+08:00 INFO 102932 --- [mcp-server-weather] [boundedElastic-1] c.b.m.s.weather.service.WeatherService : ӿڷؽ{"code":"200","location":[{"name":"","id":"101010100","lat":"39.90499","lon":"116.40529","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"10","fxLink":"https://www.qweather.com/weather/beijing-101010100.html"},{"name":"","id":"101010200","lat":"39.95607","lon":"116.31032","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/haidian-101010200.html"},{"name":"","id":"101010300","lat":"39.92149","lon":"116.48641","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/chaoyang-101010300.html"},{"name":"˳","id":"101010400","lat":"40.12894","lon":"116.65353","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/shunyi-101010400.html"},{"name":"","id":"101010500","lat":"40.32427","lon":"116.63712","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/huairou-101010500.html"},{"name":"ͨ","id":"101010600","lat":"39.90249","lon":"116.65860","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/tongzhou-101010600.html"},{"name":"ƽ","id":"101010700","lat":"40.21809","lon":"116.23591","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/changping-101010700.html"},{"name":"","id":"101010800","lat":"40.46532","lon":"115.98501","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/yanqing-101010800.html"},{"name":"̨","id":"101010900","lat":"39.86364","lon":"116.28696","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"25","fxLink":"https://www.qweather.com/weather/fengtai-101010900.html"},{"name":"ʯɽ","id":"101011000","lat":"39.91460","lon":"116.19544","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"35","fxLink":"https://www.qweather.com/weather/shijingshan-101011000.html"}],"refer":{"sources":["QWeather"],"license":["QWeather Developers License"]}} +2025-03-17T15:44:20.253+08:00 INFO 102932 --- [mcp-server-weather] [boundedElastic-1] c.b.m.s.weather.service.WeatherService : еlocationIdΪ101010100 +2025-03-17T15:44:23.372+08:00 INFO 102932 --- [mcp-server-weather] [boundedElastic-1] c.b.m.s.weather.service.WeatherService : ӿڷؽ{"code":"200","updateTime":"2025-03-17T15:42+08:00","fxLink":"https://www.qweather.com/weather/beijing-101010100.html","now":{"obsTime":"2025-03-17T15:41+08:00","temp":"8","feelsLike":"2","icon":"101","text":"","wind360":"315","windDir":"","windScale":"4","windSpeed":"21","humidity":"13","precip":"0.0","pressure":"1017","vis":"27","cloud":"91","dew":"-16"},"refer":{"sources":["QWeather"],"license":["CC BY-SA 4.0"]}} +2025-03-17T15:52:27.923+08:00 INFO 104268 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Starting McpDemoApplication v0.0.1-SNAPSHOT using Java 17.0.10 with PID 104268 (D:\WorkSpace\aikrai\mcp-demo\build\libs\mcp-demo-0.0.1-SNAPSHOT.jar started by AiKrai in D:\WorkSpace\aikrai\vertx-pj) +2025-03-17T15:52:27.925+08:00 INFO 104268 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : No active profile set, falling back to 1 default profile: "default" +2025-03-17T15:52:28.448+08:00 INFO 104268 --- [mcp-server-weather] [main] com.demo.service.WeatherService : API Key ֤ͨ +2025-03-17T15:52:28.747+08:00 INFO 104268 --- [mcp-server-weather] [main] o.s.a.a.m.s.MpcServerAutoConfiguration : Registered tools1 notification: true +2025-03-17T15:52:28.848+08:00 INFO 104268 --- [mcp-server-weather] [pool-2-thread-1] i.m.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=RootCapabilities[listChanged=false], sampling=null], Info: Implementation[name=langchain4j, version=1.0] +2025-03-17T15:52:29.013+08:00 INFO 104268 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Started McpDemoApplication in 1.634 seconds (process running for 2.12) +2025-03-17T15:52:29.015+08:00 INFO 104268 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Server running +2025-03-17T15:52:35.035+08:00 INFO 104268 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ʼȡУbeijing +2025-03-17T15:52:37.254+08:00 INFO 104268 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ӿڷؽ{"code":"200","location":[{"name":"","id":"101010100","lat":"39.90499","lon":"116.40529","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"10","fxLink":"https://www.qweather.com/weather/beijing-101010100.html"},{"name":"","id":"101010200","lat":"39.95607","lon":"116.31032","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/haidian-101010200.html"},{"name":"","id":"101010300","lat":"39.92149","lon":"116.48641","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/chaoyang-101010300.html"},{"name":"˳","id":"101010400","lat":"40.12894","lon":"116.65353","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/shunyi-101010400.html"},{"name":"","id":"101010500","lat":"40.32427","lon":"116.63712","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/huairou-101010500.html"},{"name":"ͨ","id":"101010600","lat":"39.90249","lon":"116.65860","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/tongzhou-101010600.html"},{"name":"ƽ","id":"101010700","lat":"40.21809","lon":"116.23591","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/changping-101010700.html"},{"name":"","id":"101010800","lat":"40.46532","lon":"115.98501","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/yanqing-101010800.html"},{"name":"̨","id":"101010900","lat":"39.86364","lon":"116.28696","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"25","fxLink":"https://www.qweather.com/weather/fengtai-101010900.html"},{"name":"ʯɽ","id":"101011000","lat":"39.91460","lon":"116.19544","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"35","fxLink":"https://www.qweather.com/weather/shijingshan-101011000.html"}],"refer":{"sources":["QWeather"],"license":["QWeather Developers License"]}} +2025-03-17T15:52:37.263+08:00 INFO 104268 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : еlocationIdΪ101010100 +2025-03-17T15:52:40.306+08:00 INFO 104268 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ӿڷؽ{"code":"200","updateTime":"2025-03-17T15:51+08:00","fxLink":"https://www.qweather.com/weather/beijing-101010100.html","now":{"obsTime":"2025-03-17T15:48+08:00","temp":"9","feelsLike":"2","icon":"101","text":"","wind360":"315","windDir":"","windScale":"4","windSpeed":"27","humidity":"14","precip":"0.0","pressure":"1017","vis":"30","cloud":"91","dew":"-16"},"refer":{"sources":["QWeather"],"license":["CC BY-SA 4.0"]}} +2025-03-17T15:54:20.896+08:00 INFO 8828 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Starting McpDemoApplication v0.0.1-SNAPSHOT using Java 17.0.10 with PID 8828 (D:\WorkSpace\aikrai\mcp-demo\build\libs\mcp-demo-0.0.1-SNAPSHOT.jar started by AiKrai in D:\WorkSpace\aikrai\vertx-pj) +2025-03-17T15:54:20.899+08:00 INFO 8828 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : No active profile set, falling back to 1 default profile: "default" +2025-03-17T15:54:21.398+08:00 INFO 8828 --- [mcp-server-weather] [main] com.demo.service.WeatherService : API Key ֤ͨ +2025-03-17T15:54:21.701+08:00 INFO 8828 --- [mcp-server-weather] [main] o.s.a.a.m.s.MpcServerAutoConfiguration : Registered tools1 notification: true +2025-03-17T15:54:21.812+08:00 INFO 8828 --- [mcp-server-weather] [pool-2-thread-1] i.m.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=RootCapabilities[listChanged=false], sampling=null], Info: Implementation[name=langchain4j, version=1.0] +2025-03-17T15:54:21.951+08:00 INFO 8828 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Started McpDemoApplication in 1.576 seconds (process running for 2.033) +2025-03-17T15:54:21.953+08:00 INFO 8828 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Server running +2025-03-17T15:54:25.253+08:00 INFO 8828 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ʼȡУbeijing +2025-03-17T15:54:27.842+08:00 INFO 8828 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ӿڷؽ{"code":"200","location":[{"name":"","id":"101010100","lat":"39.90499","lon":"116.40529","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"10","fxLink":"https://www.qweather.com/weather/beijing-101010100.html"},{"name":"","id":"101010200","lat":"39.95607","lon":"116.31032","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/haidian-101010200.html"},{"name":"","id":"101010300","lat":"39.92149","lon":"116.48641","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"15","fxLink":"https://www.qweather.com/weather/chaoyang-101010300.html"},{"name":"˳","id":"101010400","lat":"40.12894","lon":"116.65353","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/shunyi-101010400.html"},{"name":"","id":"101010500","lat":"40.32427","lon":"116.63712","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/huairou-101010500.html"},{"name":"ͨ","id":"101010600","lat":"39.90249","lon":"116.65860","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/tongzhou-101010600.html"},{"name":"ƽ","id":"101010700","lat":"40.21809","lon":"116.23591","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"23","fxLink":"https://www.qweather.com/weather/changping-101010700.html"},{"name":"","id":"101010800","lat":"40.46532","lon":"115.98501","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"33","fxLink":"https://www.qweather.com/weather/yanqing-101010800.html"},{"name":"̨","id":"101010900","lat":"39.86364","lon":"116.28696","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"25","fxLink":"https://www.qweather.com/weather/fengtai-101010900.html"},{"name":"ʯɽ","id":"101011000","lat":"39.91460","lon":"116.19544","adm2":"","adm1":"","country":"й","tz":"Asia/Shanghai","utcOffset":"+08:00","isDst":"0","type":"city","rank":"35","fxLink":"https://www.qweather.com/weather/shijingshan-101011000.html"}],"refer":{"sources":["QWeather"],"license":["QWeather Developers License"]}} +2025-03-17T15:54:27.851+08:00 INFO 8828 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : еlocationIdΪ101010100 +2025-03-17T15:54:31.027+08:00 INFO 8828 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ӿڷؽ{"code":"200","updateTime":"2025-03-17T15:51+08:00","fxLink":"https://www.qweather.com/weather/beijing-101010100.html","now":{"obsTime":"2025-03-17T15:48+08:00","temp":"9","feelsLike":"2","icon":"101","text":"","wind360":"315","windDir":"","windScale":"4","windSpeed":"27","humidity":"14","precip":"0.0","pressure":"1017","vis":"30","cloud":"91","dew":"-16"},"refer":{"sources":["QWeather"],"license":["CC BY-SA 4.0"]}} +2025-03-17T16:14:16.796+08:00 INFO 111796 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Starting McpDemoApplication v0.0.1-SNAPSHOT using Java 17.0.10 with PID 111796 (D:\WorkSpace\aikrai\mcp-demo\build\libs\mcp-demo-0.0.1-SNAPSHOT.jar started by AiKrai in D:\WorkSpace\aikrai\vertx-pj) +2025-03-17T16:14:16.800+08:00 INFO 111796 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : No active profile set, falling back to 1 default profile: "default" +2025-03-17T16:14:17.332+08:00 INFO 111796 --- [mcp-server-weather] [main] com.demo.service.WeatherService : API Key ֤ͨ +2025-03-17T16:14:17.646+08:00 INFO 111796 --- [mcp-server-weather] [main] o.s.a.a.m.s.MpcServerAutoConfiguration : Registered tools1 notification: true +2025-03-17T16:14:17.747+08:00 INFO 111796 --- [mcp-server-weather] [pool-2-thread-1] i.m.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=RootCapabilities[listChanged=false], sampling=null], Info: Implementation[name=langchain4j, version=1.0] +2025-03-17T16:14:17.915+08:00 INFO 111796 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Started McpDemoApplication in 1.7 seconds (process running for 2.201) +2025-03-17T16:14:17.918+08:00 INFO 111796 --- [mcp-server-weather] [main] com.demo.McpDemoApplication : Server running +2025-03-17T16:14:22.513+08:00 INFO 111796 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ʼȡУbeijing +2025-03-17T16:14:22.872+08:00 INFO 111796 --- [mcp-server-weather] [boundedElastic-1] com.demo.service.WeatherService : ӿڷؽ????n?0?W?reM?q??v??h????u??tU???$.Hl?????4A?"?)?[?d]V???????I?d????3???5??G%??|22?=????????-Y????Re?k??n??Ho???? ? m???;Tl?q2{?R^ Uv1??|??R?HE?#N?|*:>?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`?*K?A??t?{?if????(M?m9???q9TQH???{U??r?g?/e?j????<0*}?^???????S?i???5?9???t9????a??8???]?C ??T??inr|?|}r?s??pau8[?!???`H?żx\?/???x????5b#U??R??!???\????a?b?o?V%6KI ??< ?Z>???1??L???XJaa??!? +?? D???=??qr?\?GC_?r????V?x??????w???????d?Zqt??\?M?A%@J?UI?8?-??y??\?p???D?b +?*p?r??6Io??????????Grq???C?^A,L?&b?6?UA?D"?;?E?? $??J?@???Yh4GF??"uh5>?y?P?]?1????{??A???????`) { + // 指定要扫描的包路径,例如"app.data.domain" + val packageName = "app.data.domain" + +// // 方式一:一步生成所有SQL +// val sqlMap = SqlGen.generateSql(packageName) +// +// // 打印生成的SQL +// println("=== 生成的SQL语句 ===") +// sqlMap.forEach { (key: String, sql: String) -> +// println("\n--- $key ---") +// println(sql) +// } + + // 方式二:分步骤生成 + println("\n\n=== 分步骤生成SQL ===") + val sqlGen = SqlGen() + + // 1. 扫描实体类 + println("1. 开始扫描实体类...") + val entities = sqlGen.scanEntities(packageName) + println("发现 ${entities.size} 个实体类:") + entities.forEach { entity: KClass<*> -> + println("- ${entity.simpleName}") + } + + // 2. 解析实体类信息到缓存 + println("\n2. 解析实体类信息...") + sqlGen.parseEntities(entities) + + // 3. 生成建表SQL + println("\n3. 生成建表SQL...") + val createTableSql = sqlGen.generateCreateTableSql() + createTableSql.forEach { sql: String -> + println(sql) + } + + // 4. 生成CRUD SQL + println("\n4. 生成CRUD SQL...") + val crudSql = sqlGen.generateCrudSql() + crudSql.forEach { (entityClass: KClass<*>, crud: SqlGen.CrudSql) -> + println("\n--- ${entityClass.simpleName} ---") + println("INSERT: ${crud.insertSql}") + println("SELECT: ${crud.selectByIdSql}") + println("UPDATE: ${crud.updateSql}") + println("DELETE: ${crud.deleteSql}") + } + } +} \ No newline at end of file diff --git a/vertx-demo/src/main/resources/mcp-demo-0.0.1-SNAPSHOT.jar b/vertx-demo/src/main/resources/mcp-demo-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..766d063 Binary files /dev/null and b/vertx-demo/src/main/resources/mcp-demo-0.0.1-SNAPSHOT.jar differ diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapper.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapper.kt new file mode 100644 index 0000000..81140eb --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapper.kt @@ -0,0 +1,239 @@ +package org.aikrai.vertx.db.annotation + +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.memberProperties + +/** + * SQL注解映射中间类 + * 用于从实体类及其注解中提取信息以生成SQL + * 支持多套注解系统的映射配置 + */ +class SqlAnnotationMapper { + // 表名注解配置 + var tableNameAnnotationClass: KClass = TableName::class + var tableNameProperty: String = "value" + var tableNameDefaultMapper: (KClass<*>) -> String = { klass -> camelToSnake(klass.simpleName!!) } + + // 主键注解配置 + var tableIdAnnotationClass: KClass = TableId::class + var tableIdNameProperty: String = "value" + var tableIdTypeProperty: String = "type" + var tableIdDefaultMapper: (KProperty<*>) -> Boolean = { property -> + property.name == "id" || property.name.endsWith("Id") + } + + // 字段注解配置 + var tableFieldAnnotationClass: KClass = TableField::class + var tableFieldNameProperty: String = "value" + var tableFieldFillProperty: String = "fill" + var tableFieldDefaultMapper: (KProperty<*>) -> String = { property -> camelToSnake(property.name) } + + // 索引注解配置 + var tableIndexAnnotationClass: KClass = TableIndex::class + var tableIndexNameProperty: String = "name" + var tableIndexUniqueProperty: String = "unique" + var tableIndexColumnsProperty: String = "columnNames" + + // 枚举值注解配置 + var enumValueAnnotationClass: KClass = EnumValue::class + + /** + * 从实体类中获取表名 + * @param entityClass 实体类 + * @return 表名 + */ + fun getTableName(entityClass: KClass<*>): String { + // 获取所有注解 + val annotations = entityClass.annotations.filter { it.annotationClass == tableNameAnnotationClass } + + return if (annotations.isNotEmpty()) { + val annotation = annotations.first() + // 通过反射获取注解的value属性值 + val method = annotation::class.java.getMethod(tableNameProperty) + val value = method.invoke(annotation) as String + + if (value.isNotEmpty()) value else tableNameDefaultMapper(entityClass) + } else { + tableNameDefaultMapper(entityClass) + } + } + + /** + * 判断属性是否为主键 + * @param property 属性 + * @return 是否为主键 + */ + fun isTableId(property: KProperty<*>): Boolean { + val hasAnnotation = property.annotations.any { it.annotationClass == tableIdAnnotationClass } + return hasAnnotation || tableIdDefaultMapper(property) + } + + /** + * 获取属性对应的列名 + * @param property 属性 + * @return 列名 + */ + fun getColumnName(property: KProperty<*>): String { + val annotations = property.annotations.filter { it.annotationClass == tableFieldAnnotationClass } + + return if (annotations.isNotEmpty()) { + val annotation = annotations.first() + // 通过反射获取注解的value属性值 + val method = annotation::class.java.getMethod(tableFieldNameProperty) + val value = method.invoke(annotation) as String + + if (value.isNotEmpty()) value else tableFieldDefaultMapper(property) + } else { + tableFieldDefaultMapper(property) + } + } + + /** + * 获取主键类型 + * @param property 属性 + * @return 主键类型(如果有) + */ + fun getIdType(property: KProperty<*>): Any? { + val annotations = property.annotations.filter { it.annotationClass == tableIdAnnotationClass } + if (annotations.isEmpty()) return null + + val annotation = annotations.first() + // 通过反射获取注解的type属性值 + val method = annotation::class.java.getMethod(tableIdTypeProperty) + return method.invoke(annotation) + } + + /** + * 获取字段填充策略 + * @param property 属性 + * @return 填充策略(如果有) + */ + fun getFieldFill(property: KProperty<*>): Any? { + val annotations = property.annotations.filter { it.annotationClass == tableFieldAnnotationClass } + if (annotations.isEmpty()) return null + + val annotation = annotations.first() + // 通过反射获取注解的fill属性值 + val method = annotation::class.java.getMethod(tableFieldFillProperty) + return method.invoke(annotation) + } + + /** + * 获取实体类的所有索引配置 + * @param entityClass 实体类 + * @return 索引配置列表 + */ + fun getTableIndices(entityClass: KClass<*>): List { + val annotations = entityClass.annotations.filter { it.annotationClass == tableIndexAnnotationClass } + + return annotations.map { annotation -> + val nameMethod = annotation::class.java.getMethod(tableIndexNameProperty) + val uniqueMethod = annotation::class.java.getMethod(tableIndexUniqueProperty) + val columnsMethod = annotation::class.java.getMethod(tableIndexColumnsProperty) + + val name = nameMethod.invoke(annotation) as String + val unique = uniqueMethod.invoke(annotation) as Boolean + val columns = columnsMethod.invoke(annotation) as Array<*> + + TableIndexInfo( + name = name, + unique = unique, + columnNames = columns.map { it.toString() } + ) + } + } + + /** + * 获取实体类的所有属性信息 + * @param entityClass 实体类 + * @return 属性信息列表 + */ + fun getEntityProperties(entityClass: KClass<*>): List { + return entityClass.memberProperties.map { property -> + PropertyInfo( + property = property, + columnName = getColumnName(property), + isPrimary = isTableId(property), + idType = getIdType(property), + fieldFill = getFieldFill(property) + ) + } + } + + /** + * 从实体类中提取所有SQL相关信息 + * @param entityClass 实体类 + * @return 实体类SQL信息 + */ + fun extractEntityInfo(entityClass: KClass<*>): EntityInfo { + return EntityInfo( + entityClass = entityClass, + tableName = getTableName(entityClass), + properties = getEntityProperties(entityClass), + indices = getTableIndices(entityClass) + ) + } + + /** + * 驼峰命名转下划线命名 + * @param name 驼峰命名 + * @return 下划线命名 + */ + private fun camelToSnake(name: String): String { + return name.replace(Regex("([a-z0-9])([A-Z])"), "$1_$2").lowercase() + } + + /** + * 表索引信息 + */ + data class TableIndexInfo( + val name: String, + val unique: Boolean, + val columnNames: List + ) + + /** + * 属性信息 + */ + data class PropertyInfo( + val property: KProperty<*>, + val columnName: String, + val isPrimary: Boolean, + val idType: Any?, + val fieldFill: Any? + ) + + /** + * 实体类SQL信息 + */ + data class EntityInfo( + val entityClass: KClass<*>, + val tableName: String, + val properties: List, + val indices: List + ) + + companion object { + /** + * 创建默认配置的SQL注解映射器 + * @return SQL注解映射器 + */ + fun createDefault(): SqlAnnotationMapper { + return SqlAnnotationMapper() + } + + /** + * 创建自定义配置的SQL注解映射器 + * @param configure 配置函数 + * @return SQL注解映射器 + */ + fun create(configure: SqlAnnotationMapper.() -> Unit): SqlAnnotationMapper { + val mapper = SqlAnnotationMapper() + mapper.configure() + return mapper + } + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapperExample.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapperExample.kt new file mode 100644 index 0000000..99f5e60 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/SqlAnnotationMapperExample.kt @@ -0,0 +1,152 @@ +package org.aikrai.vertx.db.annotation + +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance + +/** + * SqlAnnotationMapper 使用示例 + */ +class SqlAnnotationMapperExample { + + /** + * 使用默认配置的中间类示例 + */ + fun defaultMapperExample(entityClass: KClass<*>) { + // 创建默认配置的SQL注解映射器 + val mapper = SqlAnnotationMapper.createDefault() + + // 从实体类中提取信息 + val entityInfo = mapper.extractEntityInfo(entityClass) + + // 输出实体信息 + println("表名: ${entityInfo.tableName}") + println("字段:") + entityInfo.properties.forEach { prop -> + println(" ${prop.columnName} - 主键: ${prop.isPrimary} - 类型: ${prop.property.returnType}") + if (prop.idType != null) { + println(" ID类型: ${prop.idType}") + } + if (prop.fieldFill != null) { + println(" 填充策略: ${prop.fieldFill}") + } + } + + println("索引:") + entityInfo.indices.forEach { index -> + println(" ${index.name} - 唯一: ${index.unique} - 列: ${index.columnNames.joinToString(", ")}") + } + } + + /** + * 配置自定义注解映射示例 + */ + fun customMapperExample(entityClass: KClass<*>) { + // 创建自定义配置的SQL注解映射器 + val mapper = SqlAnnotationMapper.create { + // 假设我们使用了另一套注解系统,例如JPA注解 + + // 配置表名注解 - 使用JPA的@Table注解 + tableNameAnnotationClass = Table::class + tableNameProperty = "name" + + // 配置主键注解 - 使用JPA的@Id注解 + tableIdAnnotationClass = Id::class + + // 配置字段注解 - 使用JPA的@Column注解 + tableFieldAnnotationClass = Column::class + tableFieldNameProperty = "name" + + // 自定义名称转换规则 + tableNameDefaultMapper = { kClass -> kClass.simpleName!!.lowercase() } + tableFieldDefaultMapper = { property -> property.name.lowercase() } + } + + // 从实体类中提取信息 + val entityInfo = mapper.extractEntityInfo(entityClass) + + // 输出实体信息 + println("表名: ${entityInfo.tableName}") + println("字段:") + entityInfo.properties.forEach { prop -> + println(" ${prop.columnName} - 主键: ${prop.isPrimary}") + } + } + + /** + * SQL生成器示例,展示如何根据提取的信息生成SQL + */ + fun sqlGeneratorExample(entityClass: KClass<*>) { + // 创建SQL注解映射器 + val mapper = SqlAnnotationMapper.createDefault() + + // 提取实体信息 + val entityInfo = mapper.extractEntityInfo(entityClass) + + // 生成建表SQL + val createTableSql = generateCreateTableSql(entityInfo) + println("建表SQL:") + println(createTableSql) + } + + /** + * 生成建表SQL + */ + private fun generateCreateTableSql(entityInfo: SqlAnnotationMapper.EntityInfo): String { + val sb = StringBuilder() + + sb.append("CREATE TABLE IF NOT EXISTS ${entityInfo.tableName} (\n") + + // 添加字段定义 + val columns = entityInfo.properties.map { prop -> + val primaryKey = if (prop.isPrimary) " PRIMARY KEY" else "" + " ${prop.columnName} ${mapTypeToPostgresType(prop)} ${primaryKey}" + } + + sb.append(columns.joinToString(",\n")) + sb.append("\n);") + + // 添加索引 + entityInfo.indices.forEach { index -> + val unique = if (index.unique) "UNIQUE " else "" + sb.append("\n\nCREATE ${unique}INDEX IF NOT EXISTS ${index.name} ON ${entityInfo.tableName} (${index.columnNames.joinToString(", ")});") + } + + return sb.toString() + } + + + + /** + * 将属性类型映射为PostgreSQL类型(简化示例) + */ + private fun mapTypeToPostgresType(property: SqlAnnotationMapper.PropertyInfo): String { + val typeName = property.property.returnType.toString() + + return when { + typeName.contains("String") -> "VARCHAR(255)" + typeName.contains("Int") -> "INTEGER" + typeName.contains("Long") -> "BIGINT" + typeName.contains("Boolean") -> "BOOLEAN" + typeName.contains("Double") -> "DOUBLE PRECISION" + typeName.contains("Float") -> "REAL" + typeName.contains("java.sql.Timestamp") || typeName.contains("java.util.Date") -> "TIMESTAMP" + typeName.contains("LocalDateTime") -> "TIMESTAMP" + typeName.contains("LocalDate") -> "DATE" + typeName.contains("Char") -> "CHAR(1)" + else -> "JSONB" // 默认复杂类型存储为JSON + } + } +} + +// JPA注解模拟 - 仅用于示例 +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class Table(val name: String = "") + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Id + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Column(val name: String = "") \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGenCli.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGenCli.kt new file mode 100644 index 0000000..09c54d2 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGenCli.kt @@ -0,0 +1,153 @@ +package org.aikrai.vertx.gen + +import java.io.File +import kotlin.system.exitProcess + +/** + * SqlGen命令行工具 + * + * 用法: + * SqlGenCli + * + * 参数: + * packageName - 要扫描的包名,例如 app.data.domain + * outputDir - SQL文件输出目录,可选,默认为 ./sql-gen + */ +object SqlGenCli { + + @JvmStatic + fun main(args: Array) { + if (args.isEmpty() || args[0] == "-h" || args[0] == "--help") { + printHelp() + exitProcess(0) + } + + val packageName = args[0] + val outputDir = if (args.size > 1) args[1] else "./sql-gen" + + println("开始从包 $packageName 生成SQL...") + println("输出目录: $outputDir") + + // 创建输出目录 + val outputDirFile = File(outputDir) + if (!outputDirFile.exists()) { + outputDirFile.mkdirs() + println("创建输出目录: $outputDir") + } + + try { + // 生成SQL + val sqlMap: Map = SqlGen.generateSql(packageName) + + // 按类型组织SQL + val createTableSql = mutableListOf() + val insertSql = mutableListOf() + val selectSql = mutableListOf() + val updateSql = mutableListOf() + val deleteSql = mutableListOf() + + // 分类SQL语句 + sqlMap.forEach { (key: String, sql: String) -> + when { + key.endsWith("-create") -> createTableSql.add(sql) + key.endsWith("-insert") -> insertSql.add(sql) + key.endsWith("-select") -> selectSql.add(sql) + key.endsWith("-update") -> updateSql.add(sql) + key.endsWith("-delete") -> deleteSql.add(sql) + else -> {} // 忽略其他类型 + } + } + + // 写入各类SQL文件 + File(outputDirFile, "tables.sql").writeText(createTableSql.joinToString("\n\n")) + File(outputDirFile, "inserts.sql").writeText(insertSql.joinToString("\n\n")) + File(outputDirFile, "selects.sql").writeText(selectSql.joinToString("\n\n")) + File(outputDirFile, "updates.sql").writeText(updateSql.joinToString("\n\n")) + File(outputDirFile, "deletes.sql").writeText(deleteSql.joinToString("\n\n")) + + // 写入完整SQL文件 + val allSql = """ + |-- ==================== + |-- 建表语句 + |-- ==================== + | + |${createTableSql.joinToString("\n\n")} + | + |-- ==================== + |-- 插入语句 + |-- ==================== + | + |${insertSql.joinToString("\n\n")} + | + |-- ==================== + |-- 查询语句 + |-- ==================== + | + |${selectSql.joinToString("\n\n")} + | + |-- ==================== + |-- 更新语句 + |-- ==================== + | + |${updateSql.joinToString("\n\n")} + | + |-- ==================== + |-- 删除语句 + |-- ==================== + | + |${deleteSql.joinToString("\n\n")} + """.trimMargin() + + File(outputDirFile, "all.sql").writeText(allSql) + + // 为每个实体类生成单独的SQL文件 + val tableNames = sqlMap.keys + .filter { k: String -> k.endsWith("-create") } + .map { k: String -> k.substring(0, k.length - 7) } + + tableNames.forEach { tableName: String -> + val entitySql = """ + |-- 表结构 + |${sqlMap["$tableName-create"] ?: ""} + | + |-- 插入操作 + |${sqlMap["$tableName-insert"] ?: ""} + | + |-- 查询操作 + |${sqlMap["$tableName-select"] ?: ""} + | + |-- 更新操作 + |${sqlMap["$tableName-update"] ?: ""} + | + |-- 删除操作 + |${sqlMap["$tableName-delete"] ?: ""} + """.trimMargin() + + File(outputDirFile, "$tableName.sql").writeText(entitySql) + } + + println("SQL生成完成!") + println("成功生成 ${tableNames.size} 个实体类的SQL脚本到目录: $outputDir") + + } catch (e: Exception) { + System.err.println("生成SQL时发生错误: ${e.message}") + e.printStackTrace() + exitProcess(1) + } + } + + private fun printHelp() { + println(""" + |SqlGen - Kotlin实体类SQL生成工具 + | + |用法: SqlGenCli [outputDir] + | + |参数: + | packageName 要扫描的包名,例如 app.data.domain + | outputDir SQL文件输出目录,可选,默认为 ./sql-gen + | + |示例: + | SqlGenCli app.data.domain ./sql/generated + """.trimMargin()) + } +} \ No newline at end of file